diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..8e715372 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,53 @@ +name: Tests + +on: + push: + branches: [main, master, develop] + pull_request: + branches: [main, master, develop] + +jobs: + tests: + runs-on: ubuntu-latest + + strategy: + fail-fast: true + matrix: + php: [8.2, 8.3, 8.4] + + name: PHP ${{ matrix.php }} + + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, intl, pdo_sqlite + coverage: none + ini-values: memory_limit=512M + + - name: Install October CMS + run: | + composer create-project october/october october --no-interaction --no-progress + + - name: Configure environment + run: | + cd october + mkdir -p database && touch database/database.sqlite + sed -i 's/DB_CONNECTION=.*/DB_CONNECTION=sqlite/' .env + sed -i 's|DB_DATABASE=.*|DB_DATABASE=database/database.sqlite|' .env + + - name: Migrate core tables + run: | + cd october + php artisan october:migrate --no-interaction + + - name: Checkout plugin + uses: actions/checkout@v4 + with: + path: october/plugins/rainlab/pages + + - name: Run tests + run: | + cd october + php vendor/bin/phpunit --configuration plugins/rainlab/pages/phpunit.xml diff --git a/DOCS.md b/DOCS.md index 85cfe3f6..eca57bec 100644 --- a/DOCS.md +++ b/DOCS.md @@ -201,31 +201,33 @@ To prevent a placeholder from appearing in the editor set the `type` attribute t ## Creating New Menu Item Types -Plugins can extend the Static Pages plugin with new menu item types. Please refer to the [Blog plugin](https://octobercms.com/plugin/rainlab-blog) for the integration example. New item types are registered with the API events triggered by the Static Pages plugin. The event handlers should be defined in the `boot()` method of the [plugin registration file](https://octobercms.com/docs/plugin/registration#registration-file). There are three events that should be handled in the plugin. +Plugins can extend the Static Pages plugin with new menu item types. Please refer to the [Blog plugin](https://octobercms.com/plugin/rainlab-blog) for the integration example. New item types are registered with the core page lookup API events, shared with the CMS `pagefinder` form widget — one registration makes a type available in both the menu editor and the page finder. The event handlers should be defined in the `boot()` method of the [plugin registration file](https://octobercms.com/docs/plugin/registration#registration-file). There are three events that should be handled in the plugin. -- `pages.menuitem.listType` event handler should return a list of new menu item types supported by the plugin. -- `pages.menuitem.getTypeInfo` event handler returns detailed information about a menu item type. -- `pages.menuitem.resolveItem` event handler "resolves" a menu item information and returns the actual item URL, title, an indicator whether the item is currently active, and subitems, if any. +- `cms.pageLookup.listTypes` event handler should return a list of new menu item types supported by the plugin. +- `cms.pageLookup.getTypeInfo` event handler returns detailed information about a menu item type. +- `cms.pageLookup.resolveItem` event handler "resolves" a menu item information and returns the actual item URL, title, an indicator whether the item is currently active, and subitems, if any. + +> **Note**: earlier versions of this plugin used `pages.menuitem.*` events for this purpose; these are no longer fired and plugins should migrate to the `cms.pageLookup.*` events above (same handler signatures). The next example shows an event handler registration code for the Blog plugin. The Blog plugin registers two item types. As you can see, the Blog plugin uses the Category class to handle the events. That's a recommended approach. ```php public function boot() { - Event::listen('pages.menuitem.listTypes', function() { + Event::listen('cms.pageLookup.listTypes', function() { return [ 'blog-category'=>'Blog category', 'all-blog-categories'=>'All blog categories', ]; }); - Event::listen('pages.menuitem.getTypeInfo', function($type) { + Event::listen('cms.pageLookup.getTypeInfo', function($type) { if ($type == 'blog-category' || $type == 'all-blog-categories') { return Category::getMenuTypeInfo($type); } }); - Event::listen('pages.menuitem.resolveItem', function($type, $item, $url, $theme) { + Event::listen('cms.pageLookup.resolveItem', function($type, $item, $url, $theme) { if ($type == 'blog-category' || $type == 'all-blog-categories') { return Category::resolveMenuItem($item, $url, $theme); } @@ -235,17 +237,25 @@ public function boot() ### Registering New Menu Item Types -New menu item types are registered with the `pages.menuitem.listTypes` event handlers. The handler should return an associative array with the type codes in indexes and type names in values. It's highly recommended to use the plugin name in the type codes, to avoid conflicts with other menu item type providers. Example: +New menu item types are registered with the `cms.pageLookup.listTypes` event handlers. The handler should return an associative array with the type codes in indexes and type names in values. It's highly recommended to use the plugin name in the type codes, to avoid conflicts with other menu item type providers. Example: + +```php +[ + 'my-plugin-item-type' => 'My plugin menu item type' +] +``` + +Types that generate nested items can use the extended label format so that single-URL contexts (such as the page finder in single mode) can exclude them: ```php [ - `my-plugin-item-type` => 'My plugin menu item type' + 'all-my-plugin-items' => ['label' => 'All my plugin items', 'nesting' => true] ] ``` ### Returning Information About an Item Type -Plugins should provide detailed information about the supported menu item types with the `pages.menuitem.getTypeInfo` event handlers. The handler gets a single parameter - the menu item type code (one of the codes you registered with the `pages.menuitem.listTypes` handler). The handler code must check whether the requested item type code belongs to the plugin. The handler should return an associative array in the following format: +Plugins should provide detailed information about the supported menu item types with the `cms.pageLookup.getTypeInfo` event handlers. The handler gets a single parameter - the menu item type code (one of the codes you registered with the `cms.pageLookup.listTypes` handler). The handler code must check whether the requested item type code belongs to the plugin. The handler should return an associative array in the following format: ``` Array ( @@ -287,7 +297,7 @@ The format for references with subitems is ['item-key' => ['title'=>'Item title', 'items'=>[...]]] ``` -The reference keys should reflect the object identifier they represent. For blog categories keys match the category identifiers. A plugin should be able to load an object by its key in the `pages.menuitem.resolveItem` event handler. The references element is optional, it is required only if a menu item type supports the Reference drop-down, or, in other words, if the user should be able to select an object the menu item refers to. +The reference keys should reflect the object identifier they represent. For blog categories keys match the category identifiers. A plugin should be able to load an object by its key in the `cms.pageLookup.resolveItem` event handler. The references element is optional, it is required only if a menu item type supports the Reference drop-down, or, in other words, if the user should be able to select an object the menu item refers to. #### cmsPages element @@ -320,10 +330,10 @@ return $result; ### Resolving Menu Items -When the Static Pages plugin generates a menu on the front-end, every menu item should **resolved** by the plugin that supplies the menu item type. The process of resolving involves generating the real item URL, determining whether the menu item is active, and generating the subitems (if required). Plugins should register the `pages.menuitem.resolveItem` event handler in order to resolve menu items. The event handler takes four arguments: +When the Static Pages plugin generates a menu on the front-end, every menu item should **resolved** by the plugin that supplies the menu item type. The process of resolving involves generating the real item URL, determining whether the menu item is active, and generating the subitems (if required). Plugins should register the `cms.pageLookup.resolveItem` event handler in order to resolve menu items. The event handler takes four arguments: * `$type` - the item type name. Plugins must only handle item types they provide and ignore other types. -* `$item` - the menu item object (RainLab\Pages\Classes\MenuItem). The menu item object represents the menu item configuration provided by the user. The object has the following properties: `title`, `type`, `reference`, `cmsPage`, `nesting`. +* `$item` - the item object (`RainLab\Pages\Classes\MenuItem` when resolving a menu, or `Cms\Models\PageLookupItem` when resolving a page finder link). The item object represents the configuration provided by the user. Both objects expose the following properties: `title`, `type`, `reference`, `cmsPage`, `nesting`. * `$url` - specifies the current absolute URL, in lower case. Always use the `Url::to()` helper to generate menu item links and compare them with the current URL. * `$theme` - the current theme object (`Cms\Classes\Theme`). diff --git a/Plugin.php b/Plugin.php index 0d68b943..3588347c 100644 --- a/Plugin.php +++ b/Plugin.php @@ -2,26 +2,43 @@ use Event; use Backend; -use RainLab\Pages\Classes\Controller; use RainLab\Pages\Classes\Page as StaticPage; use RainLab\Pages\Classes\Router; use Cms\Classes\Theme; -use Cms\Classes\Controller as CmsController; use System\Classes\PluginBase; +/** + * Plugin Information File + */ class Plugin extends PluginBase { + /** + * register the Editor extension for the backend Pages editor. + */ + public function register() + { + Event::listen('editor.extension.register', function () { + return \RainLab\Pages\Classes\EditorExtension::class; + }); + } + + /** + * pluginDetails returns information about this plugin. + */ public function pluginDetails() { return [ - 'name' => 'rainlab.pages::lang.plugin.name', - 'description' => 'rainlab.pages::lang.plugin.description', + 'name' => 'Pages', + 'description' => 'Pages & menus features.', 'author' => 'Alexey Bobkov, Samuel Georges', 'icon' => 'icon-files-o', 'homepage' => 'https://github.com/rainlab/pages-plugin' ]; } + /** + * registerComponents used by the frontend. + */ public function registerComponents() { return [ @@ -32,169 +49,61 @@ public function registerComponents() ]; } + /** + * registerFormWidgets available for backend forms. + */ + public function registerFormWidgets() + { + return [ + \RainLab\Pages\FormWidgets\PagePicker::class => 'staticpagepicker', + \RainLab\Pages\FormWidgets\MenuPicker::class => 'staticmenupicker', + ]; + } + + /** + * registerPermissions available for backend users. + */ public function registerPermissions() { return [ 'rainlab.pages.manage_pages' => [ - 'tab' => 'rainlab.pages::lang.page.tab', + 'tab' => 'Pages', 'order' => 200, - 'label' => 'rainlab.pages::lang.page.manage_pages' + 'label' => 'Manage static pages' ], 'rainlab.pages.manage_menus' => [ - 'tab' => 'rainlab.pages::lang.page.tab', + 'tab' => 'Pages', 'order' => 200, - 'label' => 'rainlab.pages::lang.page.manage_menus' - ], + 'label' => 'Manage static menus' + ], 'rainlab.pages.manage_content' => [ - 'tab' => 'rainlab.pages::lang.page.tab', + 'tab' => 'Pages', 'order' => 200, - 'label' => 'rainlab.pages::lang.page.manage_content' + 'label' => 'Manage static content' ] ]; } + /** + * registerNavigation for the backend, a single item hosting the Vue Editor shell. + */ public function registerNavigation() { return [ 'pages' => [ - 'label' => 'rainlab.pages::lang.plugin.name', - 'url' => Backend::url('rainlab/pages'), + 'label' => 'Pages', + 'url' => Backend::url('rainlab/pages/index'), 'icon' => 'icon-files-o', 'iconSvg' => 'plugins/rainlab/pages/assets/images/pages-icon.svg', 'permissions' => ['rainlab.pages.*'], 'order' => 200, - 'useDropdown' => false, - - 'sideMenu' => [ - 'pages' => [ - 'label' => 'rainlab.pages::lang.page.menu_label', - 'icon' => 'icon-files-o', - 'url' => 'javascript:;', - 'attributes' => ['data-menu-item'=>'pages'], - 'permissions' => ['rainlab.pages.manage_pages'] - ], - 'menus' => [ - 'label' => 'rainlab.pages::lang.menu.menu_label', - 'icon' => 'icon-sitemap', - 'url' => 'javascript:;', - 'attributes' => ['data-menu-item'=>'menus'], - 'permissions' => ['rainlab.pages.manage_menus'] - ], - 'content' => [ - 'label' => 'rainlab.pages::lang.content.menu_label', - 'icon' => 'icon-file-text-o', - 'url' => 'javascript:;', - 'attributes' => ['data-menu-item'=>'content'], - 'permissions' => ['rainlab.pages.manage_content'] - ] - ] + 'useDropdown' => false ] ]; } - public function registerFormWidgets() - { - return [ - FormWidgets\PagePicker::class => 'staticpagepicker', - FormWidgets\MenuPicker::class => 'staticmenupicker', - ]; - } - - public function boot() - { - Event::listen('cms.router.beforeRoute', function($url) { - return Controller::instance()->initCmsPage($url); - }); - - Event::listen('cms.page.beforeRenderPage', function($controller, $page) { - // Before twig renders - $twig = $controller->getTwig(); - $loader = $controller->getLoader(); - Controller::instance()->injectPageTwig($page, $loader, $twig); - - // Get rendered content - $contents = Controller::instance()->getPageContents($page); - if ($contents && strlen($contents)) { - return $contents; - } - }); - - Event::listen('cms.block.render', function($blockName, $blockContents) { - $page = CmsController::getController()->getPage(); - - if (!isset($page->apiBag['staticPage'])) { - return; - } - - $contents = Controller::instance()->getPlaceholderContents($page, $blockName, $blockContents); - if ($contents && strlen($contents)) { - return $contents; - } - }); - - Event::listen('cms.pageLookup.listTypes', function() { - return [ - 'static-page' => 'rainlab.pages::lang.menuitem.static_page', - 'all-static-pages' => ['rainlab.pages::lang.menuitem.all_static_pages', true] - ]; - }); - - Event::listen('pages.menuitem.listTypes', function() { - return [ - 'static-page' => 'rainlab.pages::lang.menuitem.static_page', - 'all-static-pages' => 'rainlab.pages::lang.menuitem.all_static_pages' - ]; - }); - - Event::listen(['cms.pageLookup.getTypeInfo', 'pages.menuitem.getTypeInfo'], function($type) { - if ($type == 'url') { - return []; - } - - if ($type == 'static-page'|| $type == 'all-static-pages') { - return StaticPage::getMenuTypeInfo($type); - } - }); - - Event::listen(['cms.pageLookup.resolveItem', 'pages.menuitem.resolveItem'], function($type, $item, $url, $theme) { - if ($type == 'static-page' || $type == 'all-static-pages') { - return StaticPage::resolveMenuItem($item, $url, $theme); - } - }); - - Event::listen('cms.template.save', function($controller, $template, $type) { - Plugin::clearCache(); - }); - - Event::listen('cms.template.processTwigContent', function($template, $dataHolder) { - if ($template instanceof \Cms\Classes\Layout) { - $dataHolder->content = Controller::instance()->parseSyntaxFields($dataHolder->content); - } - }); - - Event::listen('backend.richeditor.listTypes', function () { - return [ - 'static-page' => 'rainlab.pages::lang.menuitem.static_page', - ]; - }); - - Event::listen('backend.richeditor.getTypeInfo', function ($type) { - if ($type === 'static-page') { - return StaticPage::getRichEditorTypeInfo($type); - } - }); - - Event::listen('system.console.theme.sync.getAvailableModelClasses', function () { - return [ - Classes\Menu::class, - Classes\Page::class, - ]; - }); - } - /** - * Register new Twig variables - * @return array + * registerMarkupTags adds the staticPage filter. */ public function registerMarkupTags() { @@ -205,6 +114,17 @@ public function registerMarkupTags() ]; } + /** + * boot the plugin events. + */ + public function boot() + { + Event::subscribe(\RainLab\Pages\Classes\ExtendCmsModule::class); + } + + /** + * clearCache flushes the router and menu caches for the edit theme. + */ public static function clearCache() { $theme = Theme::getEditTheme(); @@ -213,6 +133,5 @@ public static function clearCache() $router->clearCache(); StaticPage::clearMenuCache($theme); - // SnippetManager::clearCache($theme); } } diff --git a/assets/css/editor.css b/assets/css/editor.css new file mode 100644 index 00000000..fe666829 --- /dev/null +++ b/assets/css/editor.css @@ -0,0 +1,197 @@ +/* Content-region tabs use the shared backend-tabs component with the + "primary" style (modules/backend/vuecomponents/tabs). No plugin CSS needed. */ + +/* Edit Menu Item modal: reuses the standard backend popup markup (.modal-content / + .modal-header / .btn-close / .modal-footer) so it inherits the admin popup styling; + only the overlay positioning + footer layout are added here. */ +.pages-menu-modal-overlay { + position: fixed; + inset: 0; + z-index: 600; + overflow: auto; + background: rgba(0, 0, 0, .4); + + .modal-dialog { + margin: 60px auto; + max-width: 640px; + } + + .modal-content { + display: flex; + flex-direction: column; + max-height: calc(100vh - 120px); + } + + .modal-header { + position: relative; + } + + .modal-body { + flex: 1 1 auto; + overflow: auto; + } + + .modal-footer { + display: flex; + align-items: center; + gap: 8px; + padding-top: 15px; + border-top: var(--oc-popup-border); + } + + .pages-menu-modal-footer-spacer { + flex: 1 1 auto; + } + + /* Keeps the modal body height stable while the item form loads. */ + .pages-menu-modal-loading { + min-height: 220px; + } + + .pages-menu-delete-btn:hover { + color: var(--oc-danger-color, #cc3300); + } +} + +/* Menu editor: item list + item form panel. */ +.pages-menu-editor { + .menu-item-row { + position: relative; + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + border: 1px solid transparent; + border-radius: 4px; + cursor: pointer; + user-select: none; + + &:hover { + background: var(--oc-list-hover-bg, rgba(0, 0, 0, .03)); + } + + &.selected { + background: var(--oc-primary-color, #6a70f2); + color: #fff; + + .menu-item-subtitle { + color: rgba(255, 255, 255, .8); + } + + .menu-item-actions { + opacity: 1; + } + + /* Keep actions legible on the dark selected background. */ + .menu-item-action { + color: rgba(255, 255, 255, .8); + + &:hover { + background: rgba(255, 255, 255, .18); + color: #fff; + } + } + } + + &:hover .menu-item-actions { + opacity: 1; + } + + /* Drop cues (mirror the sidebar page tree). before/after = a drop line to + reorder; inside = a border around the row you'd nest into. */ + &.drop-before::before, + &.drop-after::after { + content: ''; + position: absolute; + left: 0; + right: 0; + height: 2px; + background: var(--oc-primary-color, #6a70f2); + } + + &.drop-before::before { + top: -1px; + } + + &.drop-after::after { + bottom: -1px; + } + + &.drop-inside { + border-color: var(--oc-primary-color, #6a70f2); + background: var(--oc-primary-color-light, rgba(106, 112, 242, .08)); + } + } + + .menu-item-icon { + flex: 0 0 auto; + width: 18px; + text-align: center; + opacity: .7; + } + + .menu-item-drag { + flex: 0 0 auto; + cursor: grab; + opacity: .35; + } + + .menu-item-label { + flex: 1 1 auto; + min-width: 0; + } + + .menu-item-title { + display: block; + font-size: 14px; + line-height: 18px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .menu-item-subtitle { + display: block; + font-size: 12px; + line-height: 15px; + color: var(--oc-text-muted-color, #97a1ab); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + /* Flat, subtle icon actions matching the editor toolbar (transparent, muted icon, + light hover), revealed on row hover to keep the list clean at rest. */ + .menu-item-actions { + display: flex; + flex: 0 0 auto; + gap: 2px; + opacity: 0; + transition: opacity .12s; + } + + .menu-item-action { + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + padding: 0; + border: none; + border-radius: 4px; + background: transparent; + color: var(--oc-text-muted, #72809d); + font-size: 13px; + cursor: pointer; + box-shadow: none; + + &:hover { + background: var(--oc-list-hover-bg, rgba(0, 0, 0, .06)); + color: var(--oc-text-color, #33414f); + } + + &.menu-item-action-danger:hover { + color: var(--oc-danger-color, #cc3300); + } + } +} diff --git a/assets/css/pages.css b/assets/css/pages.css deleted file mode 100644 index 4e5bf28c..00000000 --- a/assets/css/pages.css +++ /dev/null @@ -1 +0,0 @@ -.control-filelist.menu-list li>a{position:relative}.control-filelist.menu-list li>a:before{background-image:url(../images/menu-icons.png);background-position:0 0;background-repeat:no-repeat;background-size:36px auto;content:" ";height:18px;left:17px;position:absolute;top:18px;width:18px}.control-filelist.menu-list li.active>a:before,.control-filelist.menu-list li>a:hover:before{background-position:0 -60px}.control-filelist.content li>a{position:relative}.control-filelist.content li>a:before{background-image:url(../images/content-icons.png);background-position:0 0;background-repeat:no-repeat;background-size:34px auto;content:" ";height:22px;left:18px;position:absolute;top:10px;width:18px}.control-filelist.content li.active>a:before,.control-filelist.content li>a:hover:before{background-position:0 -27px}.control-filelist.content li.group ul li>a:before{left:34px}.control-filelist.snippet-list li>a{color:#808c8d;position:relative}.control-filelist.snippet-list li>a:before{background-image:url(../images/snippet-icons.png);background-position:0 0;background-repeat:no-repeat;background-size:34px auto;content:" ";height:19px;left:18px;position:absolute;top:13px;top:12px;width:17px}.control-filelist.snippet-list li>a:hover:before{background-position:0 -21px}.control-filelist.snippet-list li.group ul li>a:before{left:34px}@media only screen and (-moz-min-device-pixel-ratio:1.5),only screen and (-webkit-min-device-pixel-ratio:1.5),only screen and (min-devicepixel-ratio:1.5),only screen and (min-resolution:1.5dppx){.control-filelist.menu-list li>a:before{background-position:0 -11px;background-size:18px auto}.control-filelist.menu-list li.active>a:before,.control-filelist.menu-list li>a:hover:before{background-position:0 -40px}.control-filelist.content li a:before{background-position:0 -27px;background-size:17px auto}.control-filelist.content li a:hover:before,.control-filelist.content li.active a:before{background-position:0 -52px}.control-filelist.snippet-list li a:before{background-position:0 -21px;background-size:17px auto}.control-filelist.snippet-list li a:hover:before{background-position:0 -41px}}.fancy-layout .pagesTextEditor{border-left:1px solid #cfd7e1!important}.control-richeditor [data-snippet]:before{content:attr(data-name)}.control-richeditor [data-snippet]:after{background-image:url(../images/snippet-icons.png);background-position:0 0;background-repeat:no-repeat;background-size:34px auto;content:" ";height:19px;left:18px;left:11px;position:absolute;top:13px;top:12px;width:17px}.control-richeditor [data-snippet].loading:after{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite;background-image:url(../images/loader-transparent.svg);background-position:50% 50%;background-size:15px 15px;content:" ";height:15px;position:absolute;top:13px;width:15px}@media only screen and (-moz-min-device-pixel-ratio:1.5),only screen and (-webkit-min-device-pixel-ratio:1.5),only screen and (min-devicepixel-ratio:1.5),only screen and (min-resolution:1.5dppx){.control-richeditor [data-snippet]:after{background-position:0 -21px;background-size:17px auto}} diff --git a/assets/css/treeview.css b/assets/css/treeview.css deleted file mode 100644 index 796865e0..00000000 --- a/assets/css/treeview.css +++ /dev/null @@ -1 +0,0 @@ -.control-treeview{margin-bottom:40px}.control-treeview ol{background:var(--oc-primary-bg,#fff);list-style:none;margin:0;padding:0}.control-treeview ol>li{transition:width 1s}.control-treeview ol>li>div{background:var(--oc-primary-bg,#fff);border-bottom:1px solid var(--bs-tertiary-bg);font-size:14px;font-weight:400;position:relative}.control-treeview ol>li>div>a{box-sizing:border-box;color:var(--oc-primary-color,#2b3e50);display:block;line-height:150%;padding:11px 45px 10px 61px;text-decoration:none}.control-treeview ol>li>div:before{background-image:url(../images/treeview-icons.png);background-position:0 -28px;background-repeat:no-repeat;background-size:42px auto;content:" ";height:22px;left:28px;position:absolute;top:15px;width:21px}.control-treeview ol>li>div span.comment{color:#95a5a6;display:block;font-size:13px;font-weight:400;margin-top:2px;overflow:hidden;text-overflow:ellipsis}.control-treeview ol>li>div>span.expand{background-color:transparent;border:0;color:transparent;color:#bdc3c7;cursor:pointer;display:none;font:0/0 a;height:20px;left:2px;position:absolute;text-shadow:none;top:19px;transition:transform .1s ease;width:20px}.control-treeview ol>li>div>span.expand:before{speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f0da";font-family:octo-icon!important;font-family:FontAwesome,octo-icon;font-size:15px;font-style:normal;font-variant:normal;font-weight:400;left:8px;line-height:1;line-height:100%;position:relative;text-transform:none;top:2px}.control-treeview ol>li>div>span.drag-handle{background-color:transparent;border:0;bottom:0;color:transparent;color:#bdc3c7;cursor:move;font:0/0 a;height:19px;opacity:0;position:absolute;right:9px;text-shadow:none;transition:opacity .4s;width:18px}.control-treeview ol>li>div>span.drag-handle:before{speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f0c9";font-family:octo-icon!important;font-family:FontAwesome,octo-icon;font-size:18px;font-style:normal;font-variant:normal;font-weight:400;line-height:inherit;text-transform:none}.control-treeview ol>li>div span.borders{font-size:0}.control-treeview ol>li>div>ul.submenu{background-color:#7476f8;border-bottom-left-radius:8px;border-bottom-right-radius:8px;bottom:-26.9px;box-shadow:inset 0 3px 3px -3px rgba(0,0,0,.2);display:none;height:27px;left:20px;list-style:none;margin-left:15px;padding:0;position:absolute;z-index:200}.control-treeview ol>li>div>ul.submenu [data-control=create-object]{padding-left:15px;padding-right:15px}.control-treeview ol>li>div>ul.submenu li{font-size:12px}.control-treeview ol>li>div>ul.submenu li a{color:#fff;display:block;outline:none;padding:4px 3px 0;text-decoration:none}.control-treeview ol>li>div>ul.submenu li a i{margin-right:5px}.control-treeview ol>li>div:hover>ul.submenu{display:block}.control-treeview ol>li>div:active>ul.submenu{background-color:var(--oc-selection,#5254f6)}.control-treeview ol>li>div .checkbox{position:absolute;right:0;top:-2px}.control-treeview ol>li>div .checkbox label{margin-right:0}.control-treeview ol>li>div .checkbox label:before{border-color:#ccc}.control-treeview ol>li>div.popover-highlight{background-color:var(--bs-primary,#6a6cf7)!important}.control-treeview ol>li>div.popover-highlight:before{background-position:0 -80px}.control-treeview ol>li>div.popover-highlight>a{color:#fff!important;cursor:default}.control-treeview ol>li>div.popover-highlight span{color:#fff!important}.control-treeview ol>li>div.popover-highlight>span.drag-handle,.control-treeview ol>li>div.popover-highlight>ul.submenu{display:none!important}.control-treeview ol>li.dragged div,.control-treeview ol>li>div:hover{background-color:var(--bs-primary,#6a6cf7)!important}.control-treeview ol>li.dragged div>a,.control-treeview ol>li>div:hover>a{color:#fff!important}.control-treeview ol>li.dragged div:before,.control-treeview ol>li>div:hover:before{background-position:0 -80px}.control-treeview ol>li.dragged div:after,.control-treeview ol>li>div:hover:after{bottom:0!important;top:0!important}.control-treeview ol>li.dragged div span,.control-treeview ol>li>div:hover span{color:#fff!important}.control-treeview ol>li.dragged div span.drag-handle,.control-treeview ol>li>div:hover span.drag-handle{cursor:move;opacity:1}.control-treeview ol>li.dragged div span.borders,.control-treeview ol>li>div:hover span.borders{display:none}.control-treeview ol>li>div:active{background-color:var(--oc-selection,#5254f6)!important}.control-treeview ol>li>div:active>a{color:#fff!important}.control-treeview ol>li[data-no-drag-mode] div:hover span.drag-handle{cursor:default!important;opacity:.3!important}.control-treeview ol>li.dragged li.has-subitems>div:before,.control-treeview ol>li.dragged.has-subitems>div:before{background-position:0 -52px}.control-treeview ol>li.dragged div>ul.submenu{display:none!important}.control-treeview ol>li>ol{padding-left:20px;padding-right:20px}.control-treeview ol>li[data-status=collapsed]>ol{display:none}.control-treeview ol>li.has-subitems>div:before{background-position:0 0;height:26px;left:26px;width:23px}.control-treeview ol>li.has-subitems>div.popover-highlight:before,.control-treeview ol>li.has-subitems>div:hover:before{background-position:0 -52px}.control-treeview ol>li.has-subitems>div span.expand{display:block}.control-treeview ol>li.placeholder{opacity:.5;position:relative}.control-treeview ol>li.dragged{opacity:.25;position:absolute;z-index:2000}.control-treeview ol>li.dragged>div{border-radius:3px}.control-treeview ol>li.drop-target>div{background-color:#2581b8!important}.control-treeview ol>li.drop-target>div>a,.control-treeview ol>li.drop-target>div>a>span.comment{color:#fff}.control-treeview ol>li.drop-target>div:before{background-position:0 -80px}.control-treeview ol>li.drop-target.has-subitems>div:before{background-position:0 -52px}.control-treeview ol>li[data-status=expanded]>div>span.expand{transform:rotate(90deg) translate(0)}.control-treeview ol>li.drag-ghost{background-color:transparent;box-sizing:content-box}.control-treeview ol>li.active>div{background:var(--oc-selection,#5254f6)}.control-treeview ol>li.active>div>a,.control-treeview ol>li.active>div>a>span.comment,.control-treeview ol>li.active>div>a>span.expand,.control-treeview ol>li.active>div>span.expand{color:#fff}.control-treeview ol>li.active>div>span.borders:after,.control-treeview ol>li.active>div>span.borders:before{background-color:var(--oc-selection,#5254f6);content:" ";display:block;height:1px;left:0;position:absolute;width:100%}.control-treeview ol>li.active>div>span.borders:before{top:-1px}.control-treeview ol>li.active>div>span.borders:after{bottom:-1px}.control-treeview ol>li.active>div:before{background-position:0 -80px}.control-treeview ol>li.active.has-subitems>div:before{background-position:0 -52px}.control-treeview ol>li.no-data{color:#666;font-size:14px;font-weight:400;margin:0;padding:18px 0;text-align:center}.control-treeview ol>li>ol>li>div{margin-left:-20px;margin-right:-20px;padding-left:71px}.control-treeview ol>li>ol>li>div>a{margin-left:-71px;padding-left:71px}.control-treeview ol>li>ol>li>div:before{margin-left:10px}.control-treeview ol>li>ol>li>div>span.expand{left:12px}.control-treeview ol>li>ol>li>ol>li>div{margin-left:-40px;margin-right:-40px;padding-left:81px}.control-treeview ol>li>ol>li>ol>li>div>a{margin-left:-81px;padding-left:81px}.control-treeview ol>li>ol>li>ol>li>div:before{margin-left:20px}.control-treeview ol>li>ol>li>ol>li>div>span.expand{left:22px}.control-treeview ol>li>ol>li>ol>li>ol>li>div{margin-left:-60px;margin-right:-60px;padding-left:91px}.control-treeview ol>li>ol>li>ol>li>ol>li>div>a{margin-left:-91px;padding-left:91px}.control-treeview ol>li>ol>li>ol>li>ol>li>div:before{margin-left:30px}.control-treeview ol>li>ol>li>ol>li>ol>li>div>span.expand{left:32px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>div{margin-left:-80px;margin-right:-80px;padding-left:101px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>div>a{margin-left:-101px;padding-left:101px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>div:before{margin-left:40px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>div>span.expand{left:42px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div{margin-left:-100px;margin-right:-100px;padding-left:111px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div>a{margin-left:-111px;padding-left:111px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div:before{margin-left:50px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div>span.expand{left:52px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div{margin-left:-120px;margin-right:-120px;padding-left:121px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div>a{margin-left:-121px;padding-left:121px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div:before{margin-left:60px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div>span.expand{left:62px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div{margin-left:-140px;margin-right:-140px;padding-left:131px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div>a{margin-left:-131px;padding-left:131px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div:before{margin-left:70px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div>span.expand{left:72px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div{margin-left:-160px;margin-right:-160px;padding-left:141px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div>a{margin-left:-141px;padding-left:141px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div:before{margin-left:80px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div>span.expand{left:82px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div{margin-left:-180px;margin-right:-180px;padding-left:151px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div>a{margin-left:-151px;padding-left:151px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div:before{margin-left:90px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div>span.expand{left:92px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div{margin-left:-200px;margin-right:-200px;padding-left:161px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div>a{margin-left:-161px;padding-left:161px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div:before{margin-left:100px}.control-treeview ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>div>span.expand{left:102px}.control-treeview p.no-data{color:#666;font-size:14px;font-weight:400;margin:0;padding:18px 0;text-align:center}.control-treeview a.menu-control{border:2px dotted #ebebeb;border-radius:5px;color:#bdc3c7;display:block;font-size:12px;font-weight:600;margin:20px;padding:13px 15px;text-transform:uppercase;vertical-align:middle}.control-treeview a.menu-control:focus,.control-treeview a.menu-control:hover{background-color:var(--bs-primary,#6a6cf7);border:none;color:#fff;padding:15px 17px;text-decoration:none}.control-treeview a.menu-control:active{background:var(--oc-selection,#5254f6);color:#fff}.control-treeview a.menu-control i{font-size:14px;margin-right:10px}.control-treeview.treeview-light{margin-bottom:0;margin-top:20px}.control-treeview.treeview-light ol{background-color:transparent}.control-treeview.treeview-light ol>li>div{background-color:transparent;border-bottom:none}.control-treeview.treeview-light ol>li>div:before{top:15px}.control-treeview.treeview-light ol>li>div>a{padding-bottom:10px;padding-top:10px}.control-treeview.treeview-light ol>li>div span.expand{top:19px}.control-treeview.treeview-light ol>li>div>span.drag-handle{background:#8284f8;bottom:auto;height:100%;right:0;top:0;transition:none!important;width:60px}.control-treeview.treeview-light ol>li>div>span.drag-handle:before{left:50%;margin-left:-6px;position:absolute;top:50%}.control-treeview.treeview-light ol>li>div>ul.submenu{background:transparent;bottom:auto;font-size:0;height:100%;left:auto;margin:0;right:60px;top:0;white-space:nowrap}.control-treeview.treeview-light ol>li>div>ul.submenu:after,.control-treeview.treeview-light ol>li>div>ul.submenu:before{display:none}.control-treeview.treeview-light ol>li>div>ul.submenu li{background:#8284f8;border-right:1px solid var(--bs-primary,#6a6cf7);display:inline-block;height:100%}.control-treeview.treeview-light ol>li>div>ul.submenu li p{display:table;height:100%;margin:0;padding:0}.control-treeview.treeview-light ol>li>div>ul.submenu li p a{box-sizing:border-box;display:table-cell;font-size:13px;height:100%;padding:0 20px;vertical-align:middle}.control-treeview.treeview-light ol>li>div>ul.submenu li p a i.control-icon{font-size:22px;margin-right:0}body.dragging .control-treeview ol.dragging,body.dragging .control-treeview ol.dragging ol{background:#ccc;padding-right:20px;transition:padding 1s}body.dragging .control-treeview ol.dragging ol>li>div,body.dragging .control-treeview ol.dragging>li>div{margin-right:0;transition:margin 1s}body.dragging .control-treeview ol.dragging ol>li>div .custom-checkbox,body.dragging .control-treeview ol.dragging>li>div .custom-checkbox{opacity:0;transition:opacity .5s}body.dragging .control-treeview.treeview-light ol.dragging ol>li>div,body.dragging .control-treeview.treeview-light ol.dragging>li>div{background-color:#f9f9f9}@media only screen and (-webkit-min-device-pixel-ratio:1.5),only screen and (min-devicepixel-ratio:1.5),only screen and (min-resolution:1.5dppx){.control-treeview ol>li>div:before{background-position:0 -79px;background-size:21px auto}.control-treeview ol>li.has-subitems>div:before{background-position:0 -52px}.control-treeview ol>li.has-subitems.active>div:before,.control-treeview ol>li.has-subitems>div.popover-highlight:before,.control-treeview ol>li.has-subitems>div:hover:before{background-position:0 -102px}.control-treeview ol>li.active>div:before,.control-treeview ol>li.dragged li>div:before,.control-treeview ol>li.dragged>div:before,.control-treeview ol>li>div.popover-highlight:before,.control-treeview ol>li>div:hover:before{background-position:0 -129px}.control-treeview ol>li.dragged li.has-subitems>div:before,.control-treeview ol>li.dragged.has-subitems>div:before{background-position:0 -102px}.control-treeview ol>li.drop-target>div:before{background-position:0 -129px}.control-treeview ol>li.drop-target.has-subitems>div:before{background-position:0 -102px}} diff --git a/assets/images/content-icons.png b/assets/images/content-icons.png deleted file mode 100644 index 893dc71a..00000000 Binary files a/assets/images/content-icons.png and /dev/null differ diff --git a/assets/images/loader-transparent.svg b/assets/images/loader-transparent.svg deleted file mode 100644 index cf84589e..00000000 --- a/assets/images/loader-transparent.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - -]> - - - - - - - - diff --git a/assets/images/menu-icons.png b/assets/images/menu-icons.png deleted file mode 100644 index f3641e9f..00000000 Binary files a/assets/images/menu-icons.png and /dev/null differ diff --git a/assets/images/snippet-icons.png b/assets/images/snippet-icons.png deleted file mode 100644 index 3d0b66f8..00000000 Binary files a/assets/images/snippet-icons.png and /dev/null differ diff --git a/assets/images/treeview-icons.png b/assets/images/treeview-icons.png deleted file mode 100644 index a08c70b8..00000000 Binary files a/assets/images/treeview-icons.png and /dev/null differ diff --git a/assets/images/treeview-submenu-tabs.png b/assets/images/treeview-submenu-tabs.png deleted file mode 100644 index 6c39867b..00000000 Binary files a/assets/images/treeview-submenu-tabs.png and /dev/null differ diff --git a/assets/js/october.treeview.js b/assets/js/october.treeview.js deleted file mode 100644 index 1ed99120..00000000 --- a/assets/js/october.treeview.js +++ /dev/null @@ -1,449 +0,0 @@ -/* - * TreeView Widget. Represents a sortable and draggable tree view. This widget was first used in the Pages plugin, for the sidebar page tree. - * - * Data attributes: - * - data-group-status-handler - AJAX handler to execute when an item is collapsed or expanded by a user - * - data-reorder-handler - AJAX handler to execute when items are reordered - * - * Events - * - open.oc.treeview - this event is triggered on the list element when an item is clicked. - * - * Dependences: - * - Tree list (october.treelist.js) - * - */ -+function ($) { "use strict"; - var Base = $.oc.foundation.base, - BaseProto = Base.prototype - - var TreeView = function (element, options) { - this.$el = $(element) - this.options = options - this.$allItems = null - this.$scrollbar = null - - Base.call(this) - - $.oc.foundation.controlUtils.markDisposable(element) - this.init() - } - - TreeView.prototype = Object.create(BaseProto) - TreeView.prototype.constructor = TreeView - - TreeView.prototype.init = function () { - this.$allItems = $('ol > li', this.$el) - this.$scrollbar = this.$el.closest('[data-control=scrollbar]') - - /* - * Init the sortable - */ - - this.initSortable() - - /* - * Create expand/collapse icons and drag handles - */ - - this.createItemControls() - - /* - * Bind the click events - */ - - this.$el.on('click.treeview', 'li > div > ul.submenu li a', this.proxy(this.onOpenSubmenu)) - this.$el.on('click.treeview', 'li > div > a', this.proxy(this.onOpen)) - this.$el.on('click.treeview', 'li span.expand', this.proxy(this.onItemExpandClick)) - - /* - * Listen for the AJAX updates and dispose the widget - */ - - this.$el.one('dispose-control', this.proxy(this.dispose)) - - /* - * Mark previously active item, if it was set - */ - var dataId = this.$el.data('oc.active-item') - if (dataId !== undefined) { - this.markActive(dataId) - } - - this.$scrollbar.on('oc.scrollEnd', this.proxy(this.onScroll)) - } - - TreeView.prototype.dispose = function() { - this.unregisterHandlers() - this.clearScrollTimeout() - - this.options = null - this.$el.removeData('oc.treeView') - this.$el = null - this.$allItems = null - this.$scrollbar = null - - BaseProto.dispose.call(this) - } - - TreeView.prototype.unregisterHandlers = function() { - this.$scrollbar.off('oc.scrollEnd', this.proxy(this.onScroll)) - this.$el.off('.treeview') - this.$el.off('move.oc.treelist', this.proxy(this.onNodeMove)) - this.$el.off('aftermove.oc.treelist', this.proxy(this.onAfterNodeMove)) - this.$el.off('dispose-control', this.proxy(this.dispose)) - } - - TreeView.prototype.createItemControls = function() { - // Scoped to ol > li to exclude submenu items, using vanilla DOM for performance on large trees - var items = this.$el.get(0).querySelectorAll('ol > li'), - itemCount = items.length - - for (var i = 0; i < itemCount; i++) { - var item = items[i], - container = item.firstElementChild - - if (!container || container.tagName !== 'DIV' || container.querySelector(':scope > span.expand')) { - continue - } - - container.insertAdjacentHTML('afterbegin', 'Expand') - - if (!container.querySelector(':scope > span.drag-handle')) { - var dragTitle = item.hasAttribute('data-no-drag-mode') - ? ' title="Dragging is disabled when the Search is active"' - : '' - - container.insertAdjacentHTML('beforeend', 'Drag') - } - - container.insertAdjacentHTML('beforeend', '') - } - } - - TreeView.prototype.collapseGroup = function($group) { - var $subitems = $('> ol', $group) - - $subitems.css({ - 'overflow': 'hidden' - }) - - $subitems.animate({'height': 0}, { duration: 100, queue: false, complete: function() { - $subitems.css({ - 'overflow': 'visible', - 'display': 'none', - 'height' : 'auto' - }) - $group.attr('data-status', 'collapsed') - $(window).trigger('resize') - } }) - - this.sendGroupStatusRequest($group, 0) - } - - TreeView.prototype.expandGroup = function($group) { - var $subitems = $('> ol', $group) - - $subitems.css({ - 'overflow': 'hidden', - 'display': 'block', - 'height': 0 - }) - - $group.attr('data-status', 'expanded') - $subitems.animate({'height': $subitems[0].scrollHeight}, { duration: 100, queue: false, complete: function() { - $subitems.css({ - 'overflow': 'visible', - 'height': 'auto' - }) - $(window).trigger('resize') - } }) - - this.sendGroupStatusRequest($group, 1); - } - - TreeView.prototype.fixSubItems = function() { - var items = this.$el.get(0).querySelectorAll('ol > li'), - itemCount = items.length - - for (var i = 0; i < itemCount; i++) { - var childList = items[i].querySelector(':scope > ol') - - items[i].classList.toggle('has-subitems', !!(childList && childList.firstElementChild)) - } - } - - TreeView.prototype.toggleGroup = function(group) { - var $group = $(group); - - $group.attr('data-status') == 'expanded' - ? this.collapseGroup($group) - : this.expandGroup($group) - } - - TreeView.prototype.sendGroupStatusRequest = function($group, status) { - if (this.options.groupStatusHandler !== undefined) { - var groupId = $group.data('group-id') - - $group.request(this.options.groupStatusHandler, {data: {group: groupId, status: status}}) - } - } - - TreeView.prototype.sendReorderRequest = function() { - if (this.options.reorderHandler === undefined) - return - - var groups = {} - - function iterator($container, node) { - $('> li', $container).each(function(){ - var subnodes = {} - iterator($('> ol', this), subnodes) - - node[$(this).data('groupId')] = subnodes - }) - } - - iterator($('> ol', this.$el), groups) - - this.$el.request(this.options.reorderHandler, {data: {structure: JSON.stringify(groups)}}) - } - - TreeView.prototype.initSortable = function() { - var $noDragItems = $('[data-no-drag-mode]', this.$el) - - if ($noDragItems.length > 0) - return - - if (this.$el.data('oc.treelist')) - this.$el.treeListWidget('unbind') - - this.$el.treeListWidget({ - tweakCursorAdjustment: this.proxy(this.tweakCursorAdjustment), - isValidTarget: this.proxy(this.isValidTarget), - useAnimation: false, - usePlaceholderClone: true, - handle: 'span.drag-handle', - onDrag: this.proxy(this.onDrag), - tolerance: -20 // Give 20px of carry between containers - }) - - this.$el.on('move.oc.treelist', this.proxy(this.onNodeMove)) - this.$el.on('aftermove.oc.treelist', this.proxy(this.onAfterNodeMove)) - } - - TreeView.prototype.markActive = function(dataId) { - $('ol > li', this.$el).removeClass('active') - - if (dataId) - $('li[data-id="'+dataId+'"]', this.$el).addClass('active') - - this.$el.data('oc.active-item', dataId) - } - - TreeView.prototype.update = function() { - this.$allItems = $('ol > li', this.$el) - this.createItemControls() - this.fixSubItems() - this.initSortable() - - var dataId = this.$el.data('oc.active-item') - if (dataId !== undefined) { - this.markActive(dataId) - } - } - - TreeView.prototype.handleMovedNode = function() { - this.$el.trigger('change') - this.$allItems.removeClass('drop-target') - this.fixSubItems() - this.sendReorderRequest() - } - - TreeView.prototype.tweakCursorAdjustment = function(adjustment) { - if (!adjustment) { - return adjustment - } - - if (this.$scrollbar.length > 0) { - adjustment.top -= this.$scrollbar.scrollTop() - } - - return adjustment - } - - TreeView.prototype.isValidTarget = function($item, container) { - return $(container.el).closest('li').attr('data-status') != 'collapsed' - } - - TreeView.DEFAULTS = { - - } - - // TREEVIEW EVENT HANDLERS - // ============================ - - TreeView.prototype.onOpenSubmenu = function(ev) { - var e = $.Event('submenu.oc.treeview', {relatedTarget: ev.currentTarget, clickEvent: ev}) - this.$el.trigger(e, this) - - return false - } - - TreeView.prototype.onOpen = function(ev) { - var e = $.Event('open.oc.treeview', {relatedTarget: $(ev.currentTarget).closest('li').get(0), clickEvent: ev}) - this.$el.trigger(e, ev.currentTarget) - - return false - } - - TreeView.prototype.onNodeMove = function() { - setTimeout(this.proxy(this.handleMovedNode), 50) - } - - TreeView.prototype.onAfterNodeMove = function(ev, data) { - this.$allItems.removeClass('drop-target') - data.container.el.closest('li').addClass('drop-target') - } - - TreeView.prototype.onItemExpandClick = function(ev) { - this.toggleGroup($(ev.currentTarget).closest('li')) - return false - } - - // TREEVIEW SCROLL ON DRAG - // ============================ - - TreeView.prototype.onScroll = function () { - if (!$('body').hasClass('dragging')) { - return - } - - var changed = this.lastScrollPos - this.$scrollbar.scrollTop() - - this.$el.children('ol').each(function() { - var sortable = $(this).data('oc.sortable') - sortable.refresh() - sortable.cursorAdjustment.top += changed // Keep cursor adjustment in sync with scroll - }); - - this.dragCallback() - - this.lastScrollPos = this.$scrollbar.scrollTop() - } - - TreeView.prototype.onDrag = function ($item, position, _super, event) { - this.lastScrollPos = this.$scrollbar.scrollTop() - - this.dragCallback = function() { - _super($item, position, null, event) - }; - - this.clearScrollTimeout() - this.dragCallback() - - if (!this.$scrollbar || this.$scrollbar.length === 0) - return - - if (position.top < 0) { - this.scrollOffset = -10 + Math.floor(position.top / 5) - } - else if (position.top > this.$scrollbar.height()) { - this.scrollOffset = 10 + Math.ceil((position.top - this.$scrollbar.height()) / 5) - } - else { - return - } - - this.dragScroll() - } - - TreeView.prototype.scrollMax = function() { - return this.$el.height() - this.$scrollbar.height() - } - - TreeView.prototype.dragScroll = function() { - var startScrollTop = this.$scrollbar.scrollTop() - var changed - - this.scrollTimeout = null - - this.$scrollbar.scrollTop(Math.min(startScrollTop + this.scrollOffset, this.scrollMax())) - - changed = this.$scrollbar.scrollTop() - startScrollTop - if (changed === 0) { - return - } - - this.$el.children('ol').each(function() { - var sortable = $(this).data('oc.sortable') - sortable.refresh() - sortable.cursorAdjustment.top -= changed // Keep cursor adjustment in sync with scroll - }); - - this.dragCallback() - - this.$scrollbar.data('oc.scrollbar').setThumbPosition() // Update scrollbar position - - this.scrollTimeout = window.setTimeout(this.proxy(this.dragScroll), 100) - } - - TreeView.prototype.clearScrollTimeout = function() { - if (this.scrollTimeout) { - window.clearTimeout(this.scrollTimeout) - this.scrollTimeout = null - } - } - - // TREEVIEW PLUGIN DEFINITION - // ============================ - - var old = $.fn.treeView - - $.fn.treeView = function (option) { - var args = arguments - - return this.each(function () { - var $this = $(this) - var data = $this.data('oc.treeView') - var options = $.extend({}, TreeView.DEFAULTS, $this.data(), typeof option == 'object' && option) - if (!data) { - $this.data('oc.treeView', (data = new TreeView(this, options))) - } - else if (typeof option !== 'string') { - // Inner contents may have been replaced by an AJAX partial update, - // refresh item controls so expand/drag handles reattach. - data.update() - } - - if (typeof option == 'string' && data) { - var methodArgs = []; - for (var i=1; i div.tab-content > div.tab-pane[data-modified]', this.$masterTabs).each(function(){ - var inputType = $('> form > input[name=objectType]', this).val() - counters[inputType].count++ - }) - - $.each(counters, function(type, data){ - $.oc.sideNav.setCounter('pages/' + data.menu, data.count); - }) - } - - /* - * Triggered when a tab is displayed. Updated the current selection in the sidebar and sets focus on an editor. - */ - PagesPage.prototype.onTabShown = function(e) { - var $tabControl = $(e.target).closest('[data-control=tab]') - - if ($tabControl.attr('id') == 'pages-master-tabs') { - var dataId = $(e.target).closest('li').attr('data-tab-id'), - title = $(e.target).attr('title') - - if (title) - this.setPageTitle(title) - - this.$pageTree.treeView('markActive', dataId) - $('[data-control=filelist]', this.$sidePanel).fileList('markActive', dataId) - $(window).trigger('resize') - } else if ($tabControl.hasClass('secondary')) { - // TODO: Focus the code or rich editor here - } - - this.storeOpenTabs(); - } - - /* - * Triggered when all master tabs are closed. - */ - PagesPage.prototype.onAllTabsClosed = function() { - this.$pageTree.treeView('markActive', null) - $('[data-control=filelist]', this.$sidePanel).fileList('markActive', null) - this.setPageTitle('') - } - - /* - * Triggered when a master tab is closed. - */ - PagesPage.prototype.onTabClosed = function() { - this.updateModifiedCounter(); - this.storeOpenTabs(); - } - - /* - * Handles AJAX errors in the master tab forms. Processes the mtime mismatch condition (concurrency). - */ - PagesPage.prototype.onAjaxError = function(event, context, message, data, jqXHR) { - if (context.handler != 'onSave') - return - - if (jqXHR.responseText == 'mtime-mismatch') { - event.preventDefault() - this.handleMtimeMismatch(event.target) - } - } - - /* - * Handles successful AJAX request in the master tab forms. Updates the UI elements and resets the mtime value. - */ - PagesPage.prototype.onAjaxSuccess = function(event, context, data) { - var $form = $(event.currentTarget), - $tabPane = $form.closest('.tab-pane') - - // Update the visibilities of the commit & reset buttons - $('[data-control=commit-button]', $form).toggleClass('oc-hide hide', !data.canCommit) - $('[data-control=reset-button]', $form).toggleClass('oc-hide hide', !data.canReset) - - if (data.objectPath !== undefined) { - $('input[name=objectPath]', $form).val(data.objectPath) - $('input[name=objectMtime]', $form).val(data.objectMtime) - $('[data-control=delete-button]', $form).removeClass('oc-hide hide') - $('[data-control=preview-button]', $form).removeClass('oc-hide hide') - - if (data.pageUrl !== undefined) - $('[data-control=preview-button]', $form).attr('href', data.pageUrl) - } - - if (data.tabTitle !== undefined) { - this.$masterTabs.ocTab('updateTitle', $tabPane, data.tabTitle) - this.setPageTitle(data.tabTitle) - } - - var tabId = $('input[name=objectType]', $form).val() + '-' - + $('input[name=theme]', $form).val() + '-' - + $('input[name=objectPath]', $form).val(); - - this.$masterTabs.ocTab('updateIdentifier', $tabPane, tabId) - this.$pageTree.treeView('markActive', tabId) - $('[data-control=filelist]', this.$sidePanel).fileList('markActive', tabId) - - // Disable fancy layout on nested forms in repeater items - $('.field-repeater-item .form-tabless-fields', $tabPane).addClass('not-fancy'); - - var objectType = $('input[name=objectType]', $form).val() - if (objectType.length > 0 && - (context.handler == 'onSave' || context.handler == 'onCommit' || context.handler == 'onReset') - ) - - this.updateObjectList(objectType); - - if (context.handler == 'onSave' && (!data['X_OCTOBER_ERROR_FIELDS'] && !data['X_OCTOBER_ERROR_MESSAGE'])) - $form.trigger('unchange.oc.changeMonitor') - - // Reload the form if the server has requested it - if (data.forceReload) { - this.reloadForm($form) - } - } - - PagesPage.prototype.onBeforeSaveContent = function(e, data) { - var form = e.currentTarget, - $tabPane = $(form).closest('.tab-pane') - - this.updateContentEditorMode($tabPane, false) - } - - PagesPage.prototype.onDeletePageSingle = function(el) { - var $el = $(el); - - $el.request('onDelete', { - success: function(data) { - $.oc.pagesPage.closeTabs(data, 'page'); - $.oc.pagesPage.updateObjectList('page'); - $(this).trigger('close.oc.tab', [{force: true}]); - } - }); - } - - /* - * Updates the browser title when an object is saved. - */ - PagesPage.prototype.setPageTitle = function(title) { - $.oc.layout.setPageTitle(title.length ? (title + ' | ') : title) - } - - /* - * Updates the sidebar object list. - */ - PagesPage.prototype.updateObjectList = function(objectType) { - var $form = $('form[data-object-type='+objectType+']', this.$sidePanel), - objectList = objectType + 'List', - self = this - - $.oc.stripeLoadIndicator.show() - $form.request(objectList + '::onUpdate', { - complete: function(data) { - $('button[data-control~=delete-object], button[data-control~=delete-template]', $form).trigger('oc.triggerOn.update') - } - }).always(function(){ - $.oc.stripeLoadIndicator.hide() - }); - } - - /* - * Closes deleted page tabs in the editor area. - */ - PagesPage.prototype.closeTabs = function(data, type) { - var self = this - - $.each(data.deletedObjects, function(){ - var tabId = type + '-' + data.theme + '-' + this, - tab = self.masterTabsObj.findByIdentifier(tabId) - - $(tab).trigger('close.oc.tab', [{force: true}]) - }) - } - - /* - * Triggered when an item is clicked in the sidebar. Opens the item in the editor. - * If the item is already opened, activate its tab in the editor. - */ - PagesPage.prototype.onSidebarItemClick = function(e) { - var self = this, - $item = $(e.relatedTarget), - $form = $item.closest('form'), - theme = $('input[name=theme]', $form).val(), - data = { - type: $form.data('object-type'), - theme: theme, - path: $item.data('item-path') - }, - tabId = data.type + '-' + data.theme + '-' + data.path - - // Find if the tab is already opened - if (this.masterTabsObj.goTo(tabId)) { - return false; - } - - // Open a new tab - $.oc.stripeLoadIndicator.show() - $form - .request('onOpen', { - data: data - }).done(function(data) { - self.$masterTabs.ocTab('addTab', data.tabTitle, data.tab, tabId, $form.data('type-icon')); - }).always(function() { - $.oc.stripeLoadIndicator.hide(); - }); - - return false - } - - /* - * Triggered when the Add button is clicked on the sidebar - */ - PagesPage.prototype.onCreateObject = function(e) { - var self = this, - $button = $(e.target), - $form = $button.closest('form'), - parent = $button.data('parent') !== undefined ? $button.data('parent') : null, - type = $form.data('object-type') ? $form.data('object-type') : $form.data('template-type'), - tabId = type + Math.random() - - $.oc.stripeLoadIndicator.show() - $form.request('onCreateObject', { - data: { - type: type, - parent: parent - } - }).done(function(data){ - self.$masterTabs.ocTab('addTab', data.tabTitle, data.tab, tabId, $form.data('type-icon') + ' new-template') - $('#layout-side-panel').trigger('close.oc.sidePanel') - self.setPageTitle(data.tabTitle) - }).always(function(){ - $.oc.stripeLoadIndicator.hide() - }) - - e.stopPropagation() - - return false; - } - - /* - * Triggered when an item is clicked in the sidebar submenu - */ - PagesPage.prototype.onSidebarSubmenuItemClick = function(e) { - if ($(e.clickEvent.target).data('control') == 'create-object') { - this.onCreateObject(e.clickEvent); - } - - return false; - } - - /* - * Triggered when the Delete button is clicked on the sidebar - */ - PagesPage.prototype.onDeleteObject = function(e) { - var $el = $(e.target), - $form = $el.closest('form'), - objectType = $form.data('object-type'), - self = this - - if (!confirm($el.data('confirmation'))) - return - - $form.request('onDeleteObjects', { - data: { - type: objectType - }, - success: function(data) { - $.each(data.deleted, function(index, path){ - var tabId = objectType + '-' + data.theme + '-' + path, - tab = self.masterTabsObj.findByIdentifier(tabId) - - self.$masterTabs.ocTab('closeTab', tab, true) - }) - - if (data.error !== undefined && $.type(data.error) === 'string' && data.error.length) - $.oc.flashMsg({text: data.error, 'class': 'error'}) - }, - complete: function() { - self.updateObjectList(objectType) - } - }) - - return false - } - - /* - * Triggered when a static page layout changes - */ - PagesPage.prototype.onLayoutChanged = function(e) { - var - self = this, - $el = $(e.target), - $form = $el.closest('form'), - $pane = $form.closest('.tab-pane'), - data = { - type: $('[name=objectType]', $form).val(), - theme: $('[name=theme]', $form).val(), - path: $('[name=objectPath]', $form).val() - }, - tab = $pane.data('tab') - - // $form.trigger('unchange.oc.changeMonitor') - $form.changeMonitor('dispose') - - $.oc.stripeLoadIndicator.show() - - $form - .request('onUpdatePageLayout', { - data: data - }) - .done(function(data){ - self.$masterTabs.ocTab('updateTab', tab, data.tabTitle, data.tab) - }) - .always(function(){ - $.oc.stripeLoadIndicator.hide() - $('form:first', $pane).changeMonitor().trigger('change') - }) - } - - /* - * Triggered when a new tab is added to the Editor - */ - PagesPage.prototype.onInitTab = function(e, data) { - if ($(e.target).attr('id') != 'pages-master-tabs') - return - - var $collapseIcon = $(''), - $panel = $('.form-tabless-fields', data.pane), - $secondaryPanel = $('.control-tabs.secondary-tabs', data.pane), - $primaryPanel = $('.control-tabs.primary-tabs', data.pane), - hasSecondaryTabs = $secondaryPanel.length > 0 - - $secondaryPanel.addClass('secondary-content-tabs') - - // Disable fancy layout on nested forms - $('.form-tabless-fields', $secondaryPanel).addClass('not-fancy'); - - $panel.append($collapseIcon) - - if (!hasSecondaryTabs) { - $primaryPanel.parent().removeClass('min-size'); - } - - $secondaryPanel.find('> .tab-content > .tab-pane').not(':has(>.form-group[data-field-name=markup],>div>div.stretch)').addClass('padded-pane'); - $secondaryPanel.find('> .layout-row > .nav-tabs > li:gt(0), > .form-tab-nav > .nav-tabs > li:gt(0)').addClass('tab-content-bg'); - - $collapseIcon.click(function(){ - $panel.toggleClass('collapsed') - - if (typeof(localStorage) !== 'undefined') - localStorage.ocPagesTablessCollapsed = $panel.hasClass('collapsed') ? 1 : 0 - - window.setTimeout(function(){ - $(window).trigger('oc.updateUi') - }, 500) - - return false - }) - - var $primaryCollapseIcon = $('') - - if ($primaryPanel.length > 0) { - $secondaryPanel.append($primaryCollapseIcon) - - $primaryCollapseIcon.click(function(){ - $primaryPanel.toggleClass('collapsed') - $secondaryPanel.toggleClass('primary-collapsed') - $(window).trigger('oc.updateUi') - if (typeof(localStorage) !== 'undefined') - localStorage.ocPagesPrimaryCollapsed = $primaryPanel.hasClass('collapsed') ? 1 : 0 - return false - }) - } else { - $secondaryPanel.addClass('primary-collapsed') - } - - if (typeof(localStorage) !== 'undefined') { - if (!$('a', data.tab).hasClass('new-template') && localStorage.ocPagesTablessCollapsed == 1) - $panel.addClass('collapsed') - - if (localStorage.ocPagesPrimaryCollapsed == 1 && hasSecondaryTabs) { - $primaryPanel.addClass('collapsed') - $secondaryPanel.addClass('primary-collapsed') - } - } - - var $form = $('form', data.pane), - self = this, - $panel = $('.form-tabless-fields', data.pane) - - this.updateContentEditorMode(data.pane, true) - - $form.on('changed.oc.changeMonitor', function() { - $panel.trigger('modified.oc.tab') - $panel.find('[data-control=commit-button]').addClass('oc-hide hide'); - $panel.find('[data-control=reset-button]').addClass('oc-hide hide'); - self.updateModifiedCounter() - }) - - $form.on('unchanged.oc.changeMonitor', function() { - $panel.trigger('unmodified.oc.tab') - self.updateModifiedCounter() - }) - } - - /* - * Triggered before a menu is saved - */ - PagesPage.prototype.onSaveMenu = function(e, data) { - var form = e.currentTarget, - items = [], - $items = $('div[data-control=treeview] > ol > li', form) - - var iterator = function(items) { - var result = [] - - $.each(items, function() { - var item = $(this).data('menu-item') - - var $subitems = $('> ol >li', this) - if ($subitems.length) - item['items'] = iterator($subitems) - - result.push(item) - }) - - return result - } - - data.options.data['itemData'] = JSON.stringify(iterator($items)) - } - - /* - * Updates the content editor to correspond to the content file extension - */ - PagesPage.prototype.updateContentEditorMode = function(pane, initialization) { - if ($('[data-toolbar-type]', pane).data('toolbar-type') !== 'content') - return - - var extension = this.getContentExtension(pane), - mode = 'html', - editor = $('[data-control=codeeditor]', pane) - - if (extension == 'html') - extension = 'htm' - - if (initialization) - $(pane).data('prev-extension', extension) - - if (extension == 'htm') { - $('[data-field-name=markup]', pane).hide() - $('[data-field-name=markup_html]', pane).show() - - if (!initialization && $(pane).data('prev-extension') != 'htm') { - var val = editor.codeEditor('getContent') - $('div[data-control=richeditor]', pane).richEditor('setContent', val) - } - } - else { - $('[data-field-name=markup]', pane).show() - $('[data-field-name=markup_html]', pane).hide() - - if (!initialization && $(pane).data('prev-extension') == 'htm') { - var val = $('div[data-control=richeditor]', pane).richEditor('getContent') - editor.codeEditor('setContent', val) - } - - var modes = $.oc.codeEditorExtensionModes - - if (modes[extension] !== undefined) - mode = modes[extension]; - - var setEditorMode = function() { - window.setTimeout(function(){ - editor.codeEditor('getEditorObject') - .getSession() - .setMode({ path: 'ace/mode/'+mode }) - }, 200) - } - - if (initialization) - editor.on('oc.codeEditorReady', setEditorMode) - else - setEditorMode() - } - - if (!initialization) - $(pane).data('prev-extension', extension) - } - - /* - * Returns the content file extension - */ - PagesPage.prototype.getContentExtension = function(form) { - var $input = $('input[name=fileName]', form), - fileName = $input.length ? $input.val() : '', - parts = fileName.split('.') - - if (parts.length >= 2) - return parts.pop().toLowerCase() - - return 'htm'; - } - - /* - * Store open tabs in a cookie - */ - PagesPage.prototype.storeOpenTabs = function () { - if (!Cookies) { - return; - } - - var openTabs = []; - document.querySelectorAll('#pages-master-tabs .tab-pane').forEach((pane) => { - var objectPath = pane.querySelector('[name=objectPath]'), - objectType = pane.querySelector('[name=objectType]'); - - if (!objectPath || !objectType) { - return; - } - - openTabs.push({ - path: objectPath.value, - type: objectType.value, - }); - }); - - Cookies.set('oc-rainlab-pages-open-tabs', JSON.stringify(openTabs), { expires: 365, path: '/' }); - } - - PagesPage.prototype.loadStoredOpenTabs = function () { - var cookieValue = Cookies.get('oc-rainlab-pages-open-tabs'); - if (!cookieValue) { - return; - } - - var openTabs = JSON.parse(cookieValue); - if (!Array.isArray(openTabs) || !openTabs.length) { - return; - } - - var self = this, - $form = $('#pages-side-panel form'); - - $.oc.stripeLoadIndicator.show(); - $form - .request('onOpenMultiple', { - data: { - openTabs: openTabs - } - }).done(function(data) { - if (!data.multiObjects || !Array.isArray(data.multiObjects)) { - return; - } - - $.each(data.multiObjects, function(index, item) { - var tabId = item.type + '-' + item.theme + '-' + item.path, - $sideForm = $('[data-object-type='+item.type+']'); - - self.$masterTabs.ocTab('addTab', item.tabTitle, item.tab, tabId, $sideForm.data('type-icon')); - }); - - self.updateModifiedCounter(); - }).always(function(){ - $.oc.stripeLoadIndicator.hide() - }); - } - - $(document).ready(function(){ - $.oc.pagesPage = new PagesPage(); - }); - -}(window.jQuery); diff --git a/assets/js/pages.editor.extension.documentcontroller.content.js b/assets/js/pages.editor.extension.documentcontroller.content.js new file mode 100644 index 00000000..b3793a27 --- /dev/null +++ b/assets/js/pages.editor.extension.documentcontroller.content.js @@ -0,0 +1,20 @@ +import { DocumentControllerBase } from '../../../../../modules/editor/assets/js/editor.extension.documentcontroller.base.js'; + +export class DocumentControllerContent extends DocumentControllerBase { + get documentType() { + return 'content'; + } + + get vueEditorComponentName() { + return 'pages-editor-component-content-editor'; + } + + beforeDocumentOpen(commandObj, nodeData) { + // The tree root ("Content") and folder nodes are not editable; content blocks are. + if (nodeData && nodeData.userData && (nodeData.userData.topLevel || nodeData.userData.isFolder)) { + return false; + } + + return true; + } +} diff --git a/assets/js/pages.editor.extension.documentcontroller.menu.js b/assets/js/pages.editor.extension.documentcontroller.menu.js new file mode 100644 index 00000000..5a6ce353 --- /dev/null +++ b/assets/js/pages.editor.extension.documentcontroller.menu.js @@ -0,0 +1,20 @@ +import { DocumentControllerBase } from '../../../../../modules/editor/assets/js/editor.extension.documentcontroller.base.js'; + +export class DocumentControllerMenu extends DocumentControllerBase { + get documentType() { + return 'menu'; + } + + get vueEditorComponentName() { + return 'pages-editor-component-menu-editor'; + } + + beforeDocumentOpen(commandObj, nodeData) { + // The tree root ("Menus") is not editable; individual menus are. + if (nodeData && nodeData.userData && nodeData.userData.topLevel) { + return false; + } + + return true; + } +} diff --git a/assets/js/pages.editor.extension.documentcontroller.staticpage.js b/assets/js/pages.editor.extension.documentcontroller.staticpage.js new file mode 100644 index 00000000..6bd5ab65 --- /dev/null +++ b/assets/js/pages.editor.extension.documentcontroller.staticpage.js @@ -0,0 +1,155 @@ +import { DocumentControllerBase } from '../../../../../modules/editor/assets/js/editor.extension.documentcontroller.base.js'; +import { EditorCommand } from '../../../../../modules/editor/assets/js/editor.command.js'; +import { DocumentUri } from '../../../../../modules/editor/assets/js/editor.documenturi.js'; + +export class DocumentControllerStaticPage extends DocumentControllerBase { + get documentType() { + return 'static-page'; + } + + get vueEditorComponentName() { + return 'pages-editor-component-staticpage-editor'; + } + + initListeners() { + // Persist page order/nesting when a page is dragged in the navigator. + this.on('pages:navigator-node-moved', this.onPageNodeMoved); + this.on('pages:navigator-context-menu-display', this.getNavigatorContextMenuItems); + } + + getNavigatorContextMenuItems(commandObj, payload) { + const uri = DocumentUri.parse(payload.nodeData.uniqueKey); + if (!uri || uri.documentType !== this.documentType) { + return; + } + + const userData = payload.nodeData.userData || {}; + if (userData.topLevel || !userData.path) { + return; + } + + payload.menuItems.push({ + type: 'text', + icon: 'icon-create', + command: new EditorCommand('pages:create-document@' + this.documentType, { + parentFileName: userData.path, + parentUrl: userData.url + }), + label: this.trans('Add subpage') || 'Add subpage' + }); + } + + onBeforeDocumentCreated(commandObj, payload, documentData) { + const userData = commandObj.userData || {}; + if (!userData.parentFileName) { + return; + } + + // New subpages nest under their parent and preset the URL with the parent's. + // The editor component keeps appending a slug of the title to the parent URL + // (via metadata.parentUrl) until the URL is edited by hand. + documentData.metadata.parentFileName = userData.parentFileName; + + const parentUrl = (userData.parentUrl || '').replace(/\/+$/, ''); + if (parentUrl.length) { + documentData.metadata.parentUrl = parentUrl; + documentData.document.url = parentUrl + '/'; + if (documentData.document.settings) { + documentData.document.settings.url = parentUrl + '/'; + } + } + } + + beforeDocumentOpen(commandObj, nodeData) { + // The tree root ("Static Pages") is not an editable document; page nodes are. + if (nodeData && nodeData.userData && nodeData.userData.topLevel) { + return false; + } + + return true; + } + + // Persist a page drag. The TreeView applies the move to its own node arrays in + // completeDrop(), which only runs if we DON'T preventDefault - so let it reorder the + // tree first, then (on the next tick) serialize the updated tree and post it. + onPageNodeMoved(cmd) { + // Do not preventDefault: the tree must run completeDrop to reflect the new order. + setTimeout(() => this.persistPageStructure(), 0); + } + + async persistPageStructure() { + const structure = this.buildPageStructure(); + + // Guard: never post an empty structure (would wipe the yaml server-side). + if (!structure || !Object.keys(structure).length) { + return; + } + + $.oc.editor.application.setNavigatorReadonly(true); + try { + await $.oc.editor.application.ajaxRequest('onCommand', { + extension: this.editorNamespace, + command: 'onPageStructureUpdate', + // JSON-encode: form encoding drops the empty-object leaves that represent + // childless pages, which would otherwise post an empty structure. + documentData: { structure: JSON.stringify(structure) } + }); + await this.editorStore.refreshExtensionNavigatorNodes(this.editorNamespace, this.documentType); + } + catch (error) { + await this.editorStore.refreshExtensionNavigatorNodes(this.editorNamespace, this.documentType); + $.oc.editor.page.showAjaxErrorAlert(error, this.trans('editor::lang.common.error')); + } + finally { + $.oc.editor.application.setNavigatorReadonly(false); + } + } + + // Walk the Static Pages navigator node into a nested {path: {child: {...}}} map, + // matching the structure PageList::updateStructure() expects. + buildPageStructure() { + const sections = this.parentExtension.state.navigatorSections || []; + let pagesRoot = null; + + const findRoot = (nodes) => { + (nodes || []).forEach((node) => { + if (node.userData && node.userData.topLevel && node.uniqueKey + && node.uniqueKey.indexOf('static-page') !== -1) { + pagesRoot = node; + } + if (!pagesRoot && node.nodes) { + findRoot(node.nodes); + } + }); + }; + + sections.forEach((section) => findRoot(section.nodes)); + + const serialize = (nodes) => { + const result = {}; + (nodes || []).forEach((node) => { + const path = node.userData && node.userData.path; + if (!path) { + return; + } + result[path] = serialize(node.nodes); + }); + return result; + }; + + return pagesRoot ? serialize(pagesRoot.nodes) : {}; + } + + preprocessSettingsFields(settingsFields) { + const layouts = this.parentExtension.customData.layouts || {}; + + settingsFields.some((field) => { + if (field.property === 'layout') { + field.options = layouts; + return true; + } + }); + + return settingsFields; + } +} diff --git a/assets/js/pages.editor.extension.js b/assets/js/pages.editor.extension.js new file mode 100644 index 00000000..e38febd1 --- /dev/null +++ b/assets/js/pages.editor.extension.js @@ -0,0 +1,32 @@ +import { ExtensionBase } from '../../../../../modules/editor/assets/js/editor.extension.base.js'; +import { DocumentControllerStaticPage } from './pages.editor.extension.documentcontroller.staticpage.js'; +import { DocumentControllerMenu } from './pages.editor.extension.documentcontroller.menu.js'; +import { DocumentControllerContent } from './pages.editor.extension.documentcontroller.content.js'; + +class PagesEditorExtension extends ExtensionBase { + constructor(namespace) { + super(namespace); + } + + listDocumentControllerClasses() { + return [ + DocumentControllerStaticPage, + DocumentControllerMenu, + DocumentControllerContent + ]; + } + + onCommand(commandString, payload) { + super.onCommand(commandString, payload); + + if (commandString === 'pages:refresh-navigator') { + this.editorStore.refreshExtensionNavigatorNodes(this.editorNamespace).then(() => {}); + } + } +} + +// Register with the editor extension registry +oc.editorExtensions = oc.editorExtensions || {}; +oc.editorExtensions['pages'] = PagesEditorExtension; + +export { PagesEditorExtension }; diff --git a/assets/less/pages.less b/assets/less/pages.less deleted file mode 100644 index 82d75074..00000000 --- a/assets/less/pages.less +++ /dev/null @@ -1,209 +0,0 @@ -@import "../../../../../modules/backend/assets/less/core/boot.less"; - -.control-filelist.menu-list { - li { - > a { - position: relative; - - &:before { - position: absolute; - width: 18px; - height: 18px; - left: 17px; - top: 18px; - - content: ' '; - - background-image: url(../images/menu-icons.png); - background-position: 0 0; - background-repeat: no-repeat; - background-size: 36px auto; - } - - &:hover { - &:before { - background-position: 0 -60px; - } - } - } - - &.active > a:before { - background-position: 0 -60px; - } - } -} - -.control-filelist.content { - li > a { - position: relative; - - &:before { - position: absolute; - width: 18px; - height: 22px; - left: 18px; - top: 10px; - - content: ' '; - - background-image: url(../images/content-icons.png); - background-position: 0 0; - background-repeat: no-repeat; - background-size: 34px auto; - } - - &:hover { - &:before { - background-position: 0 -27px; - } - } - } - - li.active > a:before { - background-position: 0 -27px; - } - - li.group ul li > a:before { - left: 34px; - } -} - -.page-snippet-icon() { - position: absolute; - width: 17px; - height: 19px; - left: 18px; - top: 13px; - - content: ' '; - - background-image: url(../images/snippet-icons.png); - background-position: 0 0; - background-repeat: no-repeat; - background-size: 34px auto; -} - -.control-filelist.snippet-list { - li > a { - position: relative; - color: #808c8d; - - &:before { - .page-snippet-icon(); - - left: 18px; - top: 12px; - } - - &:hover { - &:before { - background-position: 0 -21px; - } - } - } - - li.group ul li > a:before { - left: 34px; - } -} - -@media only screen and (-moz-min-device-pixel-ratio: 1.5), only screen and (-o-min-device-pixel-ratio: 3/2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-devicepixel-ratio: 1.5), only screen and (min-resolution: 1.5dppx) { - .control-filelist.menu-list { - li { - > a { - &:before { - background-position: 0px -11px; - background-size: 18px auto; - } - - &:hover { - &:before { - background-position: 0px -40px; - } - } - } - - &.active > a:before { - background-position: 0px -40px; - } - } - } - - .control-filelist.content { - li { - a { - &:before { - background-position: 0px -27px; - background-size: 17px auto; - } - - &:hover { - &:before { - background-position: 0px -52px; - } - } - } - - &.active a:before { - background-position: 0px -52px; - } - } - } - - .control-filelist.snippet-list { - li { - a { - &:before { - background-position: 0px -21px; - background-size: 17px auto; - } - - &:hover { - &:before { - background-position: 0px -41px; - } - } - } - } - } -} - -.fancy-layout { - .pagesTextEditor { - border-left: 1px solid #cfd7e1 !important; - } -} - -.control-richeditor { - [data-snippet] { - &:before { - content: attr(data-name); - } - - &:after { - .page-snippet-icon(); - - left: 11px; - top: 12px; - } - - &.loading:after { - background-image:url(../images/loader-transparent.svg); - background-size: 15px 15px; - background-position: 50% 50%; - position: absolute; - width: 15px; - height: 15px; - top: 13px; - content: ' '; - .animation(spin 1s linear infinite); - } - } -} - -@media only screen and (-moz-min-device-pixel-ratio: 1.5), only screen and (-o-min-device-pixel-ratio: 3/2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-devicepixel-ratio: 1.5), only screen and (min-resolution: 1.5dppx) { - .control-richeditor [data-snippet]:after { - background-position: 0px -21px; - background-size: 17px auto; - } -} diff --git a/assets/less/treeview.less b/assets/less/treeview.less deleted file mode 100644 index c8bbb6ae..00000000 --- a/assets/less/treeview.less +++ /dev/null @@ -1,634 +0,0 @@ -@import "../../../../../modules/backend/assets/less/core/boot.less"; - -@color-treeview-item-bg: var(--oc-primary-bg, #ffffff); -@color-treeview-item-title: var(--oc-primary-color, #2b3e50); -@color-treeview-item-comment: #95a5a6; -@color-treeview-control: #bdc3c7; -@color-treeview-hover-bg: var(--bs-primary, #6a6cf7); -@color-treeview-hover-text: #fff; -@color-treeview-active-bg: var(--oc-selection, #5254f6); -@color-treeview-active-text: #ffffff; -@color-treeview-item-active-comment: #ffffff; -@color-treeview-light-submenu-bg: #8284f8; -@color-treeview-light-submenu-border: var(--bs-primary, #6a6cf7); -@color-treeview-cb-border: #cccccc; -@color-filelist-norecords-text: #666666; - -@font-size-base: 14px; - -.control-treeview { - margin-bottom: 40px; - - .no-data() { - padding: 18px 0; - margin: 0; - color: @color-filelist-norecords-text; - font-size: @font-size-base; - text-align: center; - font-weight: 400; - } - - ol { - margin: 0; - padding: 0; - list-style: none; - background: @color-treeview-item-bg; - - > li { - .transition(width 1s); - - > div { - font-size: @font-size-base; - font-weight: normal; - background: @color-treeview-item-bg; - border-bottom: 1px solid @tertiary-bg; - position: relative; - - > a { - color: @color-treeview-item-title; - padding: 11px 45px 10px 61px; - display: block; - line-height: 150%; - text-decoration: none; - .box-sizing(border-box); - } - - &:before { - content: ' '; - background-image: url(../images/treeview-icons.png); - background-position: 0px -28px; - background-repeat: no-repeat; - background-size: 42px auto; - - position: absolute; - width: 21px; - height: 22px; - left: 28px; - top: 15px; - } - - span.comment { - display: block; - font-weight: 400; - color: @color-treeview-item-comment; - font-size: @font-size-base - 1; - margin-top: 2px; - overflow: hidden; - text-overflow: ellipsis; - } - - > span.expand { - .hide-text(); - display: none; - position: absolute; - width: 20px; - height: 20px; - top: 19px; - left: 2px; - cursor: pointer; - color: @color-treeview-control; - .transition(transform 0.1s ease); - - &:before { - .icon(@icon-caret-right); - font-family: FontAwesome, 'octo-icon'; - line-height: 100%; - font-size: @font-size-base + 1; - - position: relative; - left: 8px; - top: 2px; - } - } - - > span.drag-handle { - .hide-text(); - .transition(opacity 0.4s); - - position: absolute; - right: 9px; - bottom: 0; - width: 18px; - height: 19px; - cursor: move; - color: @color-treeview-control; - opacity: 0; - - &:before { - .icon(@icon-bars); - font-family: FontAwesome, 'octo-icon'; - font-size: 18px; - line-height: inherit; - } - } - - span.borders { - font-size: 0; - } - - > ul.submenu { - position: absolute; - left: 20px; - bottom: -26.9px; - padding: 0; - list-style: none; - z-index: 200; - height: 27px; - display: none; - margin-left: 15px; - background-color: #7476f8; - .border-bottom-radius(8px); - .box-shadow(~"inset 0 3px 3px -3px rgba(0, 0, 0, 0.2)"); - - [data-control="create-object"] { - padding-left: 15px; - padding-right: 15px; - } - - li { - font-size: @font-size-base - 2; - - a { - display: block; - padding: 4px 3px 0 3px; - color: #fff; - text-decoration: none; - outline: none; - - i { - margin-right: 5px; - } - } - } - } - - &:hover { - > ul.submenu { - display: block; - } - } - - &:active { - > ul.submenu { - background-color: @color-treeview-active-bg; - } - } - - .checkbox { - position: absolute; - top: -2px; - right: 0; - - label { - margin-right: 0; - - &:before { - border-color: @color-treeview-cb-border; - } - } - } - - &.popover-highlight { - background-color: @color-treeview-hover-bg !important; - - &:before { - background-position: 0px -80px; - } - - > a { - color: @color-treeview-hover-text !important; - cursor: default; - } - - span { - color: @color-treeview-hover-text !important; - } - - > ul.submenu, > span.drag-handle { - display: none!important; - } - } - } - - &.dragged div, > div:hover { - background-color: @color-treeview-hover-bg !important; - - > a { - color: @color-treeview-hover-text !important; - } - - &:before { - background-position: 0px -80px; - } - - &:after { - top: 0 !important; - bottom: 0 !important; - } - - span { - color: @color-treeview-hover-text !important; - - &.drag-handle { - cursor: move; - opacity: 1; - } - - &.borders { - display: none; - } - } - } - - > div:active { - background-color: @color-treeview-active-bg !important; - - > a { - color: @color-treeview-active-text !important; - } - } - - &[data-no-drag-mode] div:hover { - span.drag-handle { - cursor: default !important; - opacity: .3 !important; - } - } - - &.dragged { - li.has-subitems, &.has-subitems { - > div:before { - background-position: 0px -52px; - } - } - - div > ul.submenu { - display: none!important; - } - } - - > ol { - padding-left: 20px; - padding-right: 20px; - } - - &[data-status=collapsed] > ol { - display: none; - } - - &.has-subitems { - > div { - &:before { - background-position: 0 0; - width: 23px; - height: 26px; - left: 26px; - } - - &:hover, &.popover-highlight { - &:before { background-position: 0px -52px; } - } - - span.expand { - display: block; - } - } - } - - &.placeholder { - position: relative; - opacity: .5; - } - - &.dragged { - position: absolute; - z-index: 2000; - opacity: .25; - - > div { - .border-radius(3px); - } - } - - &.drop-target { - > div { - background-color: #2581b8!important; - - > a { - color: @color-treeview-hover-text; - > span.comment { - color: @color-treeview-hover-text; - } - } - - &:before { - background-position: 0px -80px; - } - } - - &.has-subitems > div:before { - background-position: 0px -52px; - } - } - - &[data-status=expanded] > div > span.expand { - .transform( ~'rotate(90deg) translate(0, 0)' ); - } - - &.drag-ghost { - background-color: transparent; - box-sizing: content-box; - } - - &.active { - > div { - background: @color-treeview-active-bg; - - > a { - color: @color-treeview-item-active-comment; - - > span.comment, > span.expand { - color: @color-treeview-item-active-comment; - } - } - - > span.expand { - color: @color-treeview-item-active-comment; - } - - > span.borders { - &:before, &:after { - content: ' '; - position: absolute; - width: 100%; - height: 1px; - display: block; - left: 0; - background-color: @color-treeview-active-bg; - } - - &:before {top: -1px;} - &:after {bottom: -1px;} - } - - &:before { - background-position: 0px -80px; - } - } - - &.has-subitems > div:before { - background-position: 0px -52px; - } - } - - &.no-data { - .no-data(); - } - } - - @max-level: 10; - - .tree-view-paddings (@level) when (@level > 0) { - > li { - > ol { - > li > div { - margin-left: -20-(@max-level - @level)*20px; - margin-right: -20-(@max-level - @level)*20px; - padding-left: 61+(@max-level - @level + 1)*10px; - - > a { - margin-left: -61-(@max-level - @level + 1)*10px; - padding-left: 61+(@max-level - @level + 1)*10px; - } - - &:before { - margin-left: (@max-level - @level + 1)*10px; - } - - > span.expand { - left: 2+(@max-level - @level + 1)*10px; - } - } - - .tree-view-paddings(@level - 1); - } - } - } - - .tree-view-paddings (@max-level); - } - - p.no-data { - .no-data(); - } - - a.menu-control { - display: block; - margin: 20px; - padding: 13px 15px; - border: dotted 2px #ebebeb; - color: #bdc3c7; - font-size: @font-size-base - 2; - font-weight: 600; - text-transform: uppercase; - border-radius: 5px; - vertical-align: middle; - - &:hover, &:focus { - text-decoration: none; - background-color: @color-treeview-hover-bg; - color: @color-treeview-hover-text; - border: none; - padding: 15px 17px; - } - - &:active { - background: @color-treeview-active-bg; - color: @color-treeview-active-text; - } - - i { - margin-right: 10px; - font-size: 14px; - } - } - - /* - * Light version of the treeview - transparent background, no bottom borders, - * smaller paddings, inline submenu - */ - &.treeview-light { - margin-bottom: 0; - margin-top: 20px; - - ol { - background-color: transparent; - > li { - > div { - background-color: transparent; - border-bottom: none; - - &:before { - top: 15px; - } - - > a { - padding-top: 10px; - padding-bottom: 10px; - } - - span.expand { - top: 19px; - } - - > span.drag-handle { - top: 0; - right: 0; - bottom: auto; - height: 100%; - width: 60px; - background: @color-treeview-light-submenu-bg; - .transition(none)!important; - - &:before { - position: absolute; - left: 50%; - top: 50%; - margin-left: -6px; - } - } - - > ul.submenu { - right: 60px; - left: auto; - bottom: auto; - top: 0; - height: 100%; - margin: 0; - background: transparent; - white-space: nowrap; - font-size: 0; - - &:before, &:after { - display: none; - } - - li { - height: 100%; - display: inline-block; - background: @color-treeview-light-submenu-bg; - border-right: 1px solid @color-treeview-light-submenu-border; - - p { - display: table; - height: 100%; - padding: 0; - margin: 0; - - a { - display: table-cell; - vertical-align: middle; - height: 100%; - padding: 0 20px; - font-size: @font-size-base - 1; - .box-sizing(border-box); - - i.control-icon { - font-size: 22px; - margin-right: 0; - } - } - } - } - } - } - } - } - } -} - -// -// Sorting guides -// - -body.dragging .control-treeview { - ol.dragging, ol.dragging ol { - background: #ccc; - padding-right: 20px; - .transition(padding 1s); - - > li { - > div { - margin-right: 0; - .transition(margin 1s); - - .custom-checkbox { - transition: opacity .5s; - opacity: 0; - } - } - } - } - - &.treeview-light { - ol.dragging, ol.dragging ol { - > li > div { - background-color: #f9f9f9; - } - } - } -} - -// -// Retina -// - -@media only screen and (min--moz-device-pixel-ratio: 1.5), only screen and (-o-min-device-pixel-ratio: 3/2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-devicepixel-ratio: 1.5), only screen and (min-resolution: 1.5dppx) { - .control-treeview { - ol { - > li { - > div{ - &:before { - background-position: 0px -79px; - background-size: 21px auto; - } - } - - &.has-subitems { - > div { - &:before {background-position: 0px -52px;} - &:hover, &.popover-highlight { - &:before {background-position: 0px -102px;} - } - } - - &.active > div { - &:before {background-position: 0px -102px;} - } - } - - &.dragged > div, &.dragged li > div, > div:hover, &.active > div, > div.popover-highlight { - &:before {background-position: 0px -129px;} - } - - &.dragged { - li.has-subitems, &.has-subitems { - > div:before { - background-position: 0px -102px; - } - } - } - - &.drop-target { - > div:before { - background-position: 0px -129px; - } - - &.has-subitems > div:before { - background-position: 0px -102px; - } - } - } - } - } -} diff --git a/classes/Content.php b/classes/Content.php index 776999a2..a8b9dc4c 100644 --- a/classes/Content.php +++ b/classes/Content.php @@ -3,28 +3,12 @@ use Cms\Classes\Content as ContentBase; /** - * Represents a content template. - * - * @package rainlab\pages - * @author Alexey Bobkov, Samuel Georges + * Content represents a content template. */ class Content extends ContentBase { - public $implement = ['@RainLab.Translate.Behaviors.TranslatableCmsObject']; - - /** - * @var array Attributes that support translation, if available. - */ - public $translatable = [ - 'markup' - ]; - - public $translatableModel = 'RainLab\Translate\Classes\MLContent'; - /** - * Converts the content object file name in to something nicer - * for humans to read. - * @return string + * getNiceTitleAttribute converts the file name into something nicer for humans to read */ public function getNiceTitleAttribute() { diff --git a/classes/Controller.php b/classes/Controller.php index e77b3a6f..172cad66 100644 --- a/classes/Controller.php +++ b/classes/Controller.php @@ -3,25 +3,24 @@ use Lang; use Cms\Classes\Page as CmsPage; use Cms\Classes\Theme; -use Cms\Classes\Layout; use Cms\Classes\CmsException; use October\Rain\Parse\Syntax\Parser as SyntaxParser; use Exception; /** - * Represents a static page controller. - * - * @package rainlab\pages - * @author Alexey Bobkov, Samuel Georges + * Controller represents a static page controller. */ class Controller { use \October\Rain\Support\Traits\Singleton; + /** + * @var \Cms\Classes\Theme theme active theme instance + */ protected $theme; /** - * Initialize this singleton. + * init this singleton */ protected function init() { @@ -32,7 +31,7 @@ protected function init() } /** - * Creates a CMS page from a static page and configures it. + * initCmsPage creates a CMS page from a static page and configures it * @param string $url Specifies the static page URL. * @return \Cms\Classes\Page Returns the CMS page object or NULL of the requested page was not found. */ @@ -45,6 +44,9 @@ public function initCmsPage($url) return null; } + // Overlay translated content for the active site's locale + $page->applySiteContext(); + $viewBag = $page->viewBag; $cmsPage = CmsPage::inTheme($this->theme); @@ -66,6 +68,9 @@ public function initCmsPage($url) return $cmsPage; } + /** + * injectPageTwig renders the static page template into the CMS page + */ public function injectPageTwig($page, $loader, $twig) { if (!isset($page->apiBag['staticPage'])) { @@ -81,6 +86,9 @@ public function injectPageTwig($page, $loader, $twig) CmsException::unmask(); } + /** + * getPageContents returns the processed markup for the static page + */ public function getPageContents($page) { if (!isset($page->apiBag['staticPage'])) { @@ -90,6 +98,9 @@ public function getPageContents($page) return $page->apiBag['staticPage']->getProcessedMarkup(); } + /** + * getPlaceholderContents returns the processed markup for a named placeholder + */ public function getPlaceholderContents($page, $placeholderName, $placeholderContents) { if (!isset($page->apiBag['staticPage'])) { @@ -99,6 +110,9 @@ public function getPlaceholderContents($page, $placeholderName, $placeholderCont return $page->apiBag['staticPage']->getProcessedPlaceholderMarkup($placeholderName, $placeholderContents); } + /** + * parseSyntaxFields converts syntax fields in the content to Twig markup + */ public function parseSyntaxFields($content) { try { diff --git a/classes/EditorExtension.php b/classes/EditorExtension.php new file mode 100644 index 00000000..81f385c5 --- /dev/null +++ b/classes/EditorExtension.php @@ -0,0 +1,545 @@ + ['rainlab.pages.manage_pages'], + self::DOCUMENT_TYPE_MENU => ['rainlab.pages.manage_menus'], + self::DOCUMENT_TYPE_CONTENT => ['rainlab.pages.manage_content'] + ]; + + /** + * @var string CONTEXT is the editor context this extension is hosted in - its own + * dedicated Pages backend page, not the global /admin/editor IDE. + */ + const CONTEXT = 'pages'; + + /** + * getNamespace returns the unique extension namespace. + */ + public function getNamespace(): string + { + return 'pages'; + } + + /** + * getEditorContext scopes this extension to its own Pages page only, keeping it out + * of the global Editor IDE (and keeping the global extensions out of the Pages page). + */ + public function getEditorContext(): string + { + return self::CONTEXT; + } + + /** + * getExtensionSortOrder affects the extension position in the Editor Navigator. + */ + public function getExtensionSortOrder() + { + return 30; + } + + /** + * hasAccessToDocType returns true if the user can manage a document type. + */ + public static function hasAccessToDocType($user, $documentType): bool + { + if (!array_key_exists($documentType, self::DOCUMENT_TYPE_PERMISSIONS)) { + throw new SystemException(sprintf('Unknown document type: %s', $documentType)); + } + + return $user && $user->hasAnyAccess(self::DOCUMENT_TYPE_PERMISSIONS[$documentType]); + } + + /** + * assertDocumentTypePermissions guards a command against unauthorized access. + */ + protected function assertDocumentTypePermissions($documentType) + { + if (!self::hasAccessToDocType(BackendAuth::getUser(), $documentType)) { + throw new ApplicationException(__("You don't have permissions to manage :type documents.", ['type' => $documentType])); + } + } + + /** + * resolveRequestDocumentType normalizes a posted document type value. + */ + protected function resolveRequestDocumentType($value): string + { + return in_array($value, [self::DOCUMENT_TYPE_MENU, self::DOCUMENT_TYPE_CONTENT], true) + ? $value + : self::DOCUMENT_TYPE_PAGE; + } + + /** + * command_onOpenDocument dispatches to the handler for the requested document type. + */ + protected function command_onOpenDocument($controller) + { + $type = $this->resolveRequestDocumentType(array_get((array) post('documentData'), 'type')); + $this->assertDocumentTypePermissions($type); + + switch ($type) { + case self::DOCUMENT_TYPE_MENU: + return $this->openMenuDocument($controller); + case self::DOCUMENT_TYPE_CONTENT: + return $this->openContentDocument($controller); + default: + return $this->openPageDocument($controller); + } + } + + /** + * command_onSaveDocument dispatches to the handler for the requested document type. + */ + protected function command_onSaveDocument($controller) + { + $type = $this->resolveRequestDocumentType(array_get((array) post('documentMetadata'), 'type')); + $this->assertDocumentTypePermissions($type); + + switch ($type) { + case self::DOCUMENT_TYPE_MENU: + return $this->saveMenuDocument($controller); + case self::DOCUMENT_TYPE_CONTENT: + return $this->saveContentDocument($controller); + default: + return $this->savePageDocument($controller); + } + } + + /** + * command_onPageStructureUpdate persists a reordered/re-nested static page tree. + */ + protected function command_onPageStructureUpdate($controller) + { + $this->assertDocumentTypePermissions(self::DOCUMENT_TYPE_PAGE); + + return $this->updatePageStructure($controller); + } + + /** + * command_onDeleteDocument dispatches to the handler for the requested document type. + */ + protected function command_onDeleteDocument($controller) + { + $type = $this->resolveRequestDocumentType(array_get((array) post('documentMetadata'), 'type')); + $this->assertDocumentTypePermissions($type); + + switch ($type) { + case self::DOCUMENT_TYPE_MENU: + return $this->deleteMenuDocument($controller); + case self::DOCUMENT_TYPE_CONTENT: + return $this->deleteContentDocument($controller); + default: + return $this->deletePageDocument($controller); + } + } + + /** + * listJsFiles returns the client-side extension bundle. + */ + public function listJsFiles() + { + return [ + '/plugins/rainlab/pages/assets/js/pages.editor.extension.js' + ]; + } + + /** + * listVueComponents returns the Vue components required by the extension. + */ + public function listVueComponents() + { + return [ + \RainLab\Pages\VueComponents\StaticPageEditor::class, + \RainLab\Pages\VueComponents\MenuEditor::class, + \RainLab\Pages\VueComponents\ContentEditor::class + ]; + } + + /** + * getSettingsForms returns the Inspector settings forms per document type. + * Menus have no settings form - the name and code are edited in the + * document header, which hides the Settings toolbar button. + */ + public function getSettingsForms() + { + return [ + self::DOCUMENT_TYPE_PAGE => $this->loadLocalizedSettingsFields(\RainLab\Pages\Classes\StaticPage\Fields::class) + ]; + } + + /** + * loadLocalizedSettingsFields loads and localizes an Inspector settings fields class. + */ + protected function loadLocalizedSettingsFields(string $fieldsClass) + { + $fields = $this->loadSettingsFields($fieldsClass); + + array_walk_recursive($fields, function (&$value, $key) { + if (is_string($value)) { + $value = trans($value); + } + }); + + return $fields; + } + + /** + * getNewDocumentsData returns the new document descriptions per document type. + */ + public function getNewDocumentsData() + { + $description = new NewDocumentDescription( + __("New page"), + [ + 'mtime' => null, + 'path' => null, + 'type' => self::DOCUMENT_TYPE_PAGE, + 'isNewDocument' => true + ] + ); + + $description->setIcon(self::ICON_COLOR_PAGE, 'backend-icon-background entity-small cms-page'); + $description->setInitialDocumentData([ + 'fileName' => '', + 'markup' => '', + 'settings' => ['title' => '', 'url' => '/'] + ]); + + $menuDescription = new NewDocumentDescription( + __("New menu"), + [ + 'mtime' => null, + 'path' => null, + 'type' => self::DOCUMENT_TYPE_MENU, + 'isNewDocument' => true + ] + ); + + $menuDescription->setIcon(self::ICON_COLOR_MENU, 'backend-icon-background entity-small text'); + // No top-level `code` here: the document header only auto-generates the + // code from the name while the code is untouched (undefined). + $menuDescription->setInitialDocumentData([ + 'name' => __("New menu"), + 'items' => [], + 'settings' => ['name' => __("New menu")] + ]); + + $contentDescription = new NewDocumentDescription( + __("New content block"), + [ + 'mtime' => null, + 'path' => null, + 'type' => self::DOCUMENT_TYPE_CONTENT, + 'isNewDocument' => true + ] + ); + + $contentDescription->setIcon(self::ICON_COLOR_CONTENT, 'backend-icon-background entity-small cms-content'); + $contentDescription->setInitialDocumentData([ + 'fileName' => '', + 'markup' => '', + 'language' => 'html' + ]); + + return [ + self::DOCUMENT_TYPE_PAGE => $description, + self::DOCUMENT_TYPE_MENU => $menuDescription, + self::DOCUMENT_TYPE_CONTENT => $contentDescription + ]; + } + + /** + * getCustomData exposes layout options to the client for the layout dropdown. + */ + public function getCustomData(): array + { + return [ + 'layouts' => $this->listLayoutOptions() + ]; + } + + /** + * listLayoutOptions returns theme layouts that support static pages. + */ + protected function listLayoutOptions(): array + { + $page = StaticPage::inTheme(Theme::getEditTheme()); + + return $page->getLayoutOptions(); + } + + /** + * getClientSideLangStrings returns language strings required by the client controller. + */ + public function getClientSideLangStrings() + { + return [ + 'backend::lang.form.save', + 'backend::lang.form.delete', + 'editor::lang.common.settings', + 'editor::lang.common.toggle_document_header', + // Plain-English strings the Vue editor components resolve via trans(). + 'Add item', + 'Add subpage', + 'Content', + 'Custom Fields', + 'Menu', + 'New menu item', + 'Preview', + 'Static page', + ]; + } + + /** + * listNavigatorSections initializes the extension's sidebar Navigator sections. + */ + public function listNavigatorSections(SectionList $sectionList, $documentType = null) + { + $user = BackendAuth::getUser(); + $theme = Theme::getEditTheme(); + + $section = $sectionList->addSection(__("Pages"), 'pages'); + $section->setHasApiMenuItems(true); + $section->setUserDataElement('uniqueKey', 'pages:root'); + + $this->addSectionMenuItems($section); + + if ( + self::hasAccessToDocType($user, self::DOCUMENT_TYPE_PAGE) && + (!$documentType || $documentType === self::DOCUMENT_TYPE_PAGE) + ) { + $this->addPagesNavigatorNodes($section, $theme); + } + + if ( + self::hasAccessToDocType($user, self::DOCUMENT_TYPE_MENU) && + (!$documentType || $documentType === self::DOCUMENT_TYPE_MENU) + ) { + $this->addMenusNavigatorNodes($section, $theme); + } + + if ( + self::hasAccessToDocType($user, self::DOCUMENT_TYPE_CONTENT) && + (!$documentType || $documentType === self::DOCUMENT_TYPE_CONTENT) + ) { + $this->addContentNavigatorNodes($section, $theme); + } + } + + /** + * addContentNavigatorNodes builds the list of content blocks, excluding static page content. + */ + protected function addContentNavigatorNodes($section, $theme) + { + $rootNode = $section->addNode(__("Content"), self::DOCUMENT_TYPE_CONTENT); + $rootNode + ->setDisplayMode(NodeDefinition::DISPLAY_MODE_TREE) + ->setChildKeyPrefix(self::DOCUMENT_TYPE_CONTENT.':') + ->setUserData(['topLevel' => true]); + + $this->addNodeCreateMenu($rootNode, self::DOCUMENT_TYPE_CONTENT, __("New Content Block")); + + // Folder nodes are cached by path so files sharing a directory nest under one folder. + $folderNodes = []; + + foreach (Content::listInTheme($theme, true) as $content) { + $fileName = ltrim($content->fileName, '/'); + + // Static page content is managed by the page document type, not as content + // blocks. This includes translated mirror directories (static-pages-{locale}). + if (preg_match('#^static-pages(-[^/]+)?/#', $fileName)) { + continue; + } + + $parentNode = $this->resolveContentFolderNode($rootNode, $fileName, $folderNodes); + + $node = $parentNode->addNode(basename($fileName), $fileName); + $node->setIcon(self::ICON_COLOR_CONTENT, 'backend-icon-background entity-small cms-content'); + } + } + + /** + * resolveContentFolderNode returns (creating as needed) the folder node a content file nests under. + */ + protected function resolveContentFolderNode($rootNode, string $fileName, array &$folderNodes) + { + $dir = trim(dirname($fileName), './'); + if ($dir === '') { + return $rootNode; + } + + $parentNode = $rootNode; + $accumulated = ''; + + foreach (explode('/', $dir) as $segment) { + $accumulated = $accumulated === '' ? $segment : $accumulated.'/'.$segment; + + if (!isset($folderNodes[$accumulated])) { + $folder = $parentNode->addNode($segment, 'folder:'.$accumulated); + $folder + ->setDisplayMode(NodeDefinition::DISPLAY_MODE_TREE) + ->setUserData(['isFolder' => true]); + $folder->setFolderIcon(); + $folderNodes[$accumulated] = $folder; + } + + $parentNode = $folderNodes[$accumulated]; + } + + return $parentNode; + } + + /** + * addMenusNavigatorNodes builds the flat list of menus. + */ + protected function addMenusNavigatorNodes($section, $theme) + { + $rootNode = $section->addNode(__("Menus"), self::DOCUMENT_TYPE_MENU); + $rootNode + ->setChildKeyPrefix(self::DOCUMENT_TYPE_MENU.':') + ->setUserData(['topLevel' => true]); + + $this->addNodeCreateMenu($rootNode, self::DOCUMENT_TYPE_MENU, __("New Menu")); + + foreach (Menu::listInTheme($theme, true) as $menu) { + $node = $rootNode->addNode($menu->name ?: $menu->getBaseFileName(), $menu->getBaseFileName()); + $node->setIcon(self::ICON_COLOR_MENU, 'backend-icon-background entity-small text'); + } + } + + /** + * addPagesNavigatorNodes builds the hierarchical static page tree. + */ + protected function addPagesNavigatorNodes($section, $theme) + { + $rootNode = $section->addNode(__("Static Pages"), self::DOCUMENT_TYPE_PAGE); + $rootNode + ->setDisplayMode(NodeDefinition::DISPLAY_MODE_TREE) + ->setChildKeyPrefix(self::DOCUMENT_TYPE_PAGE.':') + // Drag to reorder (sort) and re-nest (move), persisted via command_onPageStructureUpdate. + ->setDragAndDropMode([NodeDefinition::DND_SORT, NodeDefinition::DND_MOVE]) + ->setUserData(['topLevel' => true]); + + $this->addNodeCreateMenu($rootNode, self::DOCUMENT_TYPE_PAGE, __("New Page")); + + $pageList = new PageList($theme); + $this->addPageTreeNodes($pageList->getPageTree(true), $rootNode); + } + + /** + * addPageTreeNodes recursively adds page nodes and their subpages. + */ + protected function addPageTreeNodes($pages, $parentNode) + { + foreach ($pages as $pageInfo) { + $page = $pageInfo->page; + $baseName = $page->getBaseFileName(); + $title = $page->getViewBag()->property('title') ?: $baseName; + + $node = $parentNode->addNode($title, $baseName); + $node->setIcon(self::ICON_COLOR_PAGE, 'backend-icon-background entity-small cms-page'); + // path drives the drag-move handler; url presets new subpage URLs. + $node->setUserData([ + 'path' => $baseName, + 'url' => (string) $page->getViewBag()->property('url') + ]); + $node->setHasApiMenuItems(true); + + if ($pageInfo->subpages) { + $this->addPageTreeNodes($pageInfo->subpages, $node); + } + } + } + + /** + * addNodeCreateMenu adds a "create document" action to a top-level navigator node, so + * each type (Static Pages / Menus / Content) offers creating a new object in context, + * mirroring the main Editor IDE. + */ + protected function addNodeCreateMenu($node, string $documentType, string $label) + { + $node->addRootMenuItem( + ItemDefinition::TYPE_TEXT, + $label, + 'pages:create-document@'.$documentType + )->setIcon('icon-create'); + } + + /** + * addSectionMenuItems adds the refresh and create menu items to a section. + */ + protected function addSectionMenuItems($section) + { + $user = BackendAuth::getUser(); + + $section->addMenuItem(ItemDefinition::TYPE_TEXT, __("Refresh"), 'pages:refresh-navigator') + ->setIcon('icon-refresh'); + + $createMenuItem = new ItemDefinition(ItemDefinition::TYPE_TEXT, __("Add"), 'pages:create'); + $createMenuItem->setIcon('icon-create'); + + if (self::hasAccessToDocType($user, self::DOCUMENT_TYPE_PAGE)) { + $createMenuItem->addItemObject( + $section->addCreateMenuItem( + ItemDefinition::TYPE_TEXT, + __("Page"), + 'pages:create-document@'.self::DOCUMENT_TYPE_PAGE + ) + ); + } + + if (self::hasAccessToDocType($user, self::DOCUMENT_TYPE_MENU)) { + $createMenuItem->addItemObject( + $section->addCreateMenuItem( + ItemDefinition::TYPE_TEXT, + __("Menu"), + 'pages:create-document@'.self::DOCUMENT_TYPE_MENU + ) + ); + } + + if (self::hasAccessToDocType($user, self::DOCUMENT_TYPE_CONTENT)) { + $createMenuItem->addItemObject( + $section->addCreateMenuItem( + ItemDefinition::TYPE_TEXT, + __("Content block"), + 'pages:create-document@'.self::DOCUMENT_TYPE_CONTENT + ) + ); + } + + if ($createMenuItem->hasItems()) { + $section->addMenuItemObject($createMenuItem); + } + } +} diff --git a/classes/ExtendCmsModule.php b/classes/ExtendCmsModule.php new file mode 100644 index 00000000..a2195372 --- /dev/null +++ b/classes/ExtendCmsModule.php @@ -0,0 +1,177 @@ +listen('cms.router.beforeRoute', [static::class, 'initCmsPage']); + $events->listen('cms.page.beforeRenderPage', [static::class, 'beforeRenderPage']); + $events->listen('cms.block.render', [static::class, 'renderBlockContents']); + $events->listen('cms.template.processTwigContent', [static::class, 'processTwigContent']); + $events->listen('cms.template.save', [static::class, 'templateAfterSave']); + $events->listen('cms.sitePicker.overridePattern', [static::class, 'overrideSitePickerPattern']); + + // Page lookup + $events->listen('cms.pageLookup.listTypes', [static::class, 'listPageLookupTypes']); + $events->listen('cms.pageLookup.getTypeInfo', [static::class, 'getPageLookupTypeInfo']); + $events->listen('cms.pageLookup.resolveItem', [static::class, 'resolvePageLookupItem']); + + // Rich editor page links + $events->listen('backend.richeditor.listTypes', [static::class, 'listRichEditorTypes']); + $events->listen('backend.richeditor.getTypeInfo', [static::class, 'getRichEditorTypeInfo']); + + // Theme sync + $events->listen('system.console.theme.sync.getAvailableModelClasses', [static::class, 'getThemeSyncModelClasses']); + } + + /** + * initCmsPage routes the URL to a static page when no CMS page matches. + */ + public function initCmsPage($url) + { + return Controller::instance()->initCmsPage($url); + } + + /** + * beforeRenderPage renders the static page contents in place of the CMS page. + */ + public function beforeRenderPage($controller, $page) + { + // Before twig renders + $twig = $controller->getTwig(); + $loader = $controller->getLoader(); + Controller::instance()->injectPageTwig($page, $loader, $twig); + + // Get rendered content + $contents = Controller::instance()->getPageContents($page); + if ($contents && strlen($contents)) { + return $contents; + } + } + + /** + * renderBlockContents renders placeholder contents defined by a static page. + */ + public function renderBlockContents($blockName, $blockContents) + { + $page = CmsController::getController()->getPage(); + + if (!isset($page->apiBag['staticPage'])) { + return; + } + + $contents = Controller::instance()->getPlaceholderContents($page, $blockName, $blockContents); + if ($contents && strlen($contents)) { + return $contents; + } + } + + /** + * processTwigContent parses syntax fields defined in layout templates. + */ + public function processTwigContent($template, $dataHolder) + { + if ($template instanceof \Cms\Classes\Layout) { + $dataHolder->content = Controller::instance()->parseSyntaxFields($dataHolder->content); + } + } + + /** + * templateAfterSave clears the static page caches when any template is saved. + */ + public function templateAfterSave($controller, $template, $type) + { + Plugin::clearCache(); + } + + /** + * overrideSitePickerPattern resolves translated static page URLs when switching + * sites via the site picker. + */ + public function overrideSitePickerPattern($page, $pattern, $currentSite, $proposedSite) + { + if (isset($page->apiBag['staticPage'])) { + $staticPage = $page->apiBag['staticPage']; + + return $staticPage->getTranslatableUrl($proposedSite) + ?: array_get($staticPage->attributes, 'viewBag.url'); + } + } + + /** + * listPageLookupTypes + */ + public function listPageLookupTypes() + { + return [ + 'static-page' => 'Static page', + 'all-static-pages' => ['label' => 'All static pages', 'nesting' => true] + ]; + } + + /** + * getPageLookupTypeInfo + */ + public function getPageLookupTypeInfo($type) + { + if ($type == 'url') { + return []; + } + + if ($type == 'static-page' || $type == 'all-static-pages') { + return Page::getMenuTypeInfo($type); + } + } + + /** + * resolvePageLookupItem + */ + public function resolvePageLookupItem($type, $item, $url, $theme) + { + if ($type == 'static-page' || $type == 'all-static-pages') { + return Page::resolveMenuItem($item, $url, $theme); + } + } + + /** + * listRichEditorTypes + */ + public function listRichEditorTypes() + { + return [ + 'static-page' => 'Static page', + ]; + } + + /** + * getRichEditorTypeInfo + */ + public function getRichEditorTypeInfo($type) + { + if ($type === 'static-page') { + return Page::getRichEditorTypeInfo($type); + } + } + + /** + * getThemeSyncModelClasses registers static page models with the theme:sync command. + */ + public function getThemeSyncModelClasses() + { + return [ + Menu::class, + Page::class, + ]; + } +} diff --git a/classes/Menu.php b/classes/Menu.php index 711d658c..aae4439b 100644 --- a/classes/Menu.php +++ b/classes/Menu.php @@ -1,6 +1,7 @@ 'required|regex:/^[0-9a-z\-\_]+$/i', ]; /** - * @var array The array of custom error messages. + * @var array customMessages for validation errors. */ public $customMessages = [ - 'required' => 'rainlab.pages::lang.menu.code_required', - 'regex' => 'rainlab.pages::lang.menu.invalid_code', + 'required' => 'The Code is required', + 'regex' => 'Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-', ]; /** - * Returns the menu code. + * getCodeAttribute returns the menu code * @return string */ public function getCodeAttribute() @@ -62,7 +60,7 @@ public function getCodeAttribute() } /** - * Sets the menu code. + * setCodeAttribute sets the menu code * @param string $code Specifies the file code. * @return \Cms\Classes\CmsObject Returns the object instance. */ @@ -79,7 +77,7 @@ public function setCodeAttribute($code) } /** - * Returns a default value for items attribute. + * getItemsAttribute returns a default value for the items attribute * Items are objects of the \RainLab\Pages\Classes\MenuItem class. * @return array */ @@ -94,10 +92,8 @@ public function getItemsAttribute() } /** - * Store the itemData in the items attribute - * + * setItemDataAttribute stores the itemData in the items attribute * @param array $data - * @return void */ public function setItemDataAttribute($data) { @@ -106,7 +102,7 @@ public function setItemDataAttribute($data) } /** - * Processes the content attribute to an array of menu data. + * parseContent processes the content attribute to an array of menu data * @return array|null */ protected function parseContent() @@ -121,7 +117,7 @@ protected function parseContent() } /** - * Initializes a cache item. + * initCacheItem initializes a cache item * @param array &$item The cached item array. */ public static function initCacheItem(&$item) @@ -132,9 +128,8 @@ public static function initCacheItem(&$item) } /** - * Returns the menu item references. - * This function is used on the front-end. - * @param Cms\Classes\Page $page The current page object. + * generateReferences returns the menu item references, used on the front-end + * @param \Cms\Classes\Page $page The current page object. * @return array Returns an array of the \RainLab\Pages\Classes\MenuItemReference objects. */ public function generateReferences($page) @@ -168,10 +163,11 @@ public function generateReferences($page) } else { /* - * If the item type is not URL, use the API to request the item type's provider to + * If the item type is not URL, use the shared page lookup system + * (cms.pageLookup.resolveItem) to request the item type's provider to * return the item URL, subitems and determine whether the item is active. */ - $apiResult = Event::fire('pages.menuitem.resolveItem', [$item->type, $item, $currentUrl, $this->theme]); + $apiResult = Event::fire('cms.pageLookup.resolveItem', [$item->type, $item, $currentUrl, $this->theme]); if (is_array($apiResult)) { foreach ($apiResult as $itemInfo) { if (!is_array($itemInfo)) { @@ -262,6 +258,12 @@ public function generateReferences($page) $iterator($items); + /* + * Apply per-item locale overrides stored by the menu editor in the item + * view bag (viewBag.locale.{locale}.{field}) for the active site's locale. + */ + $this->applyLocaleOverrides($items); + /* * @event pages.menu.referencesGenerated * Provides opportunity to dynamically change menu entries right after reference generation. @@ -297,4 +299,54 @@ public function generateReferences($page) return $items; } + + /** + * applyLocaleOverrides replaces item fields with translated values stored in + * the item view bag (viewBag.locale.{locale}.{field}). + */ + protected function applyLocaleOverrides($items) + { + if (!Site::hasMultiSite()) { + return; + } + + $site = Site::getActiveSite(); + $primary = Site::getPrimarySite(); + if (!$site || !$primary) { + return; + } + + $locale = (string) $site->hard_locale; + if (!strlen($locale) || $locale === (string) $primary->hard_locale) { + return; + } + + $currentUrl = Request::path(); + if (!strlen($currentUrl)) { + $currentUrl = '/'; + } + $currentUrl = Str::lower(Url::to($currentUrl)); + + $iterator = function($menuItems) use (&$iterator, $locale, $currentUrl) { + foreach ($menuItems as $item) { + $localeFields = array_get($item->viewBag, "locale.{$locale}", []); + foreach ($localeFields as $fieldName => $fieldValue) { + if ($fieldValue) { + $item->$fieldName = $fieldValue; + + // A translated URL changes which item matches the current page + if ($fieldName === 'url') { + $item->isActive = $item->isActive || $currentUrl == Str::lower(Url::to($fieldValue)); + } + } + } + + if ($item->items) { + $iterator($item->items); + } + } + }; + + $iterator($items); + } } diff --git a/classes/MenuItem.php b/classes/MenuItem.php index 8791380e..e1d5c357 100644 --- a/classes/MenuItem.php +++ b/classes/MenuItem.php @@ -1,36 +1,32 @@ 'Header', ]; - $apiResult = Event::fire('pages.menuitem.listTypes'); - - if (is_array($apiResult)) { - foreach ($apiResult as $typeList) { - if (!is_array($typeList)) { - continue; - } - - foreach ($typeList as $typeCode => $typeName) { - $result[$typeCode] = $typeName; - } - } - } + $result += (new PageLookupItem)->getTypeOptions(); return $result; } + /** + * getCmsPageOptions returns options for the CMS page dropdown + */ public function getCmsPageOptions($keyValue = null) { return []; // CMS Pages are loaded client-side } + /** + * getReferenceOptions returns options for the reference dropdown + */ public function getReferenceOptions($keyValue = null) { return []; // References are loaded client-side } + /** + * getTypeInfo returns type information resolved from page lookup providers + * via the shared page lookup system (cms.pageLookup.getTypeInfo). + */ public static function getTypeInfo($type) { - $result = []; - $apiResult = Event::fire('pages.menuitem.getTypeInfo', [$type]); - - if (is_array($apiResult)) { - foreach ($apiResult as $typeInfo) { - if (!is_array($typeInfo)) { - continue; - } - - foreach ($typeInfo as $name => $value) { - if ($name == 'cmsPages') { - $cmsPages = []; - - foreach ($value as $page) { - $baseName = $page->getBaseFileName(); - $pos = strrpos($baseName, '/'); - - $dir = $pos !== false ? substr($baseName, 0, $pos).' / ' : null; - $cmsPages[$baseName] = strlen($page->title) - ? $dir.$page->title - : $baseName; - } - - $value = $cmsPages; - } - - $result[$name] = $value; - } - } - } - - return $result; + return (new PageLookupItem)->getTypeInfo((string) $type); } /** - * Converts the menu item data to an array + * toArray converts the menu item data to an array * @return array Returns the menu item data as array */ public function toArray() diff --git a/classes/MenuItemReference.php b/classes/MenuItemReference.php index da532cb8..35d62269 100644 --- a/classes/MenuItemReference.php +++ b/classes/MenuItemReference.php @@ -1,53 +1,49 @@ 'required', @@ -69,7 +58,7 @@ class Page extends ContentBase ]; /** - * @var array The array of custom attribute names. + * @var array attributeNames of custom attribute names */ public $attributeNames = [ 'title' => 'title', @@ -77,24 +66,7 @@ class Page extends ContentBase ]; /** - * @var array Attributes that support translation, if available. - */ - public $translatable = [ - 'code', - 'markup', - 'viewBag[title]', - 'viewBag[meta_title]', - 'viewBag[meta_description]', - ]; - - /** - * @var string Translation model used for translation, if available. - */ - public $translatableModel = 'RainLab\Translate\Classes\MLStaticPage'; - - /** - * @var string Contains the page parent file name. - * This property is used by the page editor internally. + * @var string parentFileName used by the page editor internally */ public $parentFileName; @@ -132,8 +104,8 @@ public function __construct(array $attributes = []) parent::__construct($attributes); $this->customMessages = [ - 'url.regex' => 'rainlab.pages::lang.page.invalid_url', - 'url.unique_url' => 'rainlab.pages::lang.page.url_not_unique', + 'url.regex' => __("Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/."), + 'url.unique_url' => __("This URL is already used by another page."), ]; } @@ -142,7 +114,7 @@ public function __construct(array $attributes = []) // /** - * Sets the object attributes. + * fill sets the object attributes * @param array $attributes A list of attributes to set. */ public function fill(array $attributes) @@ -160,7 +132,7 @@ public function fill(array $attributes) } /** - * Returns the attributes used for validation. + * getValidationAttributes returns the attributes used for validation * @return array */ protected function getValidationAttributes() @@ -169,8 +141,7 @@ protected function getValidationAttributes() } /** - * Validates the object properties. - * Throws a ValidationException in case of an error. + * beforeValidate validates the object properties */ public function beforeValidate() { @@ -193,7 +164,7 @@ public function beforeValidate() } /** - * Triggered before a new object is saved. + * beforeCreate is triggered before a new object is saved */ public function beforeCreate() { @@ -201,7 +172,7 @@ public function beforeCreate() } /** - * Triggered after a new object is saved. + * afterCreate is triggered after a new object is saved */ public function afterCreate() { @@ -209,7 +180,7 @@ public function afterCreate() } /** - * Adds this page to the meta index. + * appendToMeta adds this page to the meta index */ protected function appendToMeta() { @@ -245,9 +216,8 @@ protected function generateFilenameFromCode() } /** - * delete the object from the disk. - * Recursively deletes subpages. Returns a list of file names of deleted pages. - * @return array + * delete the object from the disk, recursively deleting subpages + * @return array Returns a list of file names of deleted pages. */ public function delete() { @@ -261,10 +231,12 @@ public function delete() } /* - * Delete the object + * Delete the object, along with any translated mirror files */ $result = array_merge($result, [$this->getBaseFileName()]); + $this->deleteLocaleMirrors(); + parent::delete(); /* @@ -276,7 +248,7 @@ public function delete() } /** - * Removes this page to the meta index. + * removeFromMeta removes this page from the meta index */ protected function removeFromMeta() { @@ -289,7 +261,7 @@ protected function removeFromMeta() // /** - * Helper that makes a URL for a static page in the active theme. + * url makes a URL for a static page in the active theme * * Guide for the page reference: * - chairs -> content/static-pages/chairs.htm @@ -303,13 +275,13 @@ public static function url($name) return null; } - $url = array_get($page->attributes, 'viewBag.url'); + $url = $page->getTranslatableUrl() ?: array_get($page->attributes, 'viewBag.url'); return Cms::url($url); } /** - * Determine the default layout for a new page + * setDefaultLayout determines the default layout for a new page * @param \RainLab\Pages\Classes\Page $parentPage */ public function setDefaultLayout($parentPage) @@ -342,7 +314,7 @@ public function setDefaultLayout($parentPage) // /** - * Returns the parent page that belongs to this one, or null. + * getParent returns the parent page that belongs to this one, or null * @return mixed */ public function getParent() @@ -362,7 +334,7 @@ public function getParent() } /** - * Returns all the child pages that belong to this one. + * getChildren returns all the child pages that belong to this one * @return array */ public function getChildren() @@ -387,8 +359,7 @@ public function getChildren() } /** - * Returns a list of layouts available in the theme. - * This method is used by the form widget. + * getLayoutOptions returns a list of layouts available in the theme * @return array Returns an array of strings. */ public function getLayoutOptions() @@ -406,14 +377,14 @@ public function getLayoutOptions() } if (!$result) { - $result[null] = Lang::get('rainlab.pages::lang.page.layouts_not_found'); + $result[null] = __("Layouts not found"); } return $result; } /** - * Looks up the Layout Cms object for this page. + * getLayoutObject looks up the Layout Cms object for this page * @return Cms\Classes\Layout */ public function getLayoutObject() @@ -470,8 +441,7 @@ public function listLayoutSyntaxFields() // /** - * listLayoutPlaceholders gets information about placeholders defined in the page layout. - * Returns an associative array of the placeholder name and codes. + * listLayoutPlaceholders gets information about placeholders defined in the page layout * @return array */ public function listLayoutPlaceholders() @@ -485,7 +455,7 @@ public function listLayoutPlaceholders() $nodes = array_merge([$bodyNode], $this->flattenTwigNode($bodyNode)); foreach ($nodes as $node) { - if (!$node instanceof \Cms\Twig\PlaceholderNode) { + if (!$node instanceof \Cms\Twig\Node\PlaceholderNode) { continue; } @@ -515,7 +485,7 @@ public function listLayoutPlaceholders() /** * flattenTwigNode recursively flattens a twig node and children * @param $node - * @return array A flat array of twig nodes + * @return array Returns a flat array of twig nodes. */ protected function flattenTwigNode($node) { @@ -533,8 +503,7 @@ protected function flattenTwigNode($node) } /** - * getPlaceholdersAttribute parses the page placeholder {% put %} tags and extracts the - * placeholder values. Returns an associative array of the placeholder names and values. + * getPlaceholdersAttribute parses the page placeholder {% put %} tags and extracts the placeholder values * @return array */ public function getPlaceholdersAttribute() @@ -548,13 +517,13 @@ public function getPlaceholdersAttribute() } $bodyNode = $this->getTwigNodeTree($this->code)->getNode('body')->getNode(0); - if ($bodyNode instanceof \Cms\Twig\PutNode) { + if ($bodyNode instanceof \Cms\Twig\Node\PutNode) { $bodyNode = [$bodyNode]; } $result = []; foreach ($bodyNode as $node) { - if (!$node instanceof \Cms\Twig\PutNode) { + if (!$node instanceof \Cms\Twig\Node\PutNode) { continue; } @@ -573,10 +542,8 @@ public function getPlaceholdersAttribute() } /** - * setPlaceholdersAttribute takes an array of placeholder data (key: code, value: content) - * and renders it as a single string of Twig markup against the "code" attribute. - * @param array $value - * @return void + * setPlaceholdersAttribute takes an array of placeholder data and renders it as Twig markup against the "code" attribute + * @param array $value */ public function setPlaceholdersAttribute($value) { @@ -605,6 +572,111 @@ public function setPlaceholdersAttribute($value) $this->attributes['placeholders'] = $placeholders; } + // + // Localization + // + + /** + * @var string|null appliedSiteLocale is set once locale overrides have been applied, + * also used to differentiate the Twig cache between locales. + */ + protected $appliedSiteLocale = null; + + /** + * applySiteContext overlays translated content for the active site's locale. + * + * Translated page content lives in a locale-suffixed mirror directory + * (content/static-pages-{locale}/) controlled entirely by this plugin - the + * core content/{locale}/ convention only applies to the {% content %} tag. + * Mirror values (view bag, markup, placeholders) override the base page + * where present. Translated URLs come from viewBag.localeUrl in the base + * file via the HasTranslatableBag trait. + */ + public function applySiteContext($site = null) + { + if ($this->appliedSiteLocale !== null || !Site::hasMultiSite()) { + return; + } + + $site = $site ?: Site::getActiveSite(); + $primary = Site::getPrimarySite(); + if (!$site || !$primary) { + return; + } + + $locale = (string) $site->hard_locale; + if (!strlen($locale) || $locale === (string) $primary->hard_locale) { + return; + } + + foreach (Site::getLocaleKeyChain($locale) as $localeKey) { + if ($mirror = PageLocale::findLocale($localeKey, $this)) { + $this->applyLocaleMirror($mirror); + $this->appliedSiteLocale = $localeKey; + return; + } + } + + $this->appliedSiteLocale = $locale; + } + + /** + * applyLocaleMirror overlays a translated mirror's values over this page. + */ + protected function applyLocaleMirror(PageLocale $mirror) + { + // Non-empty view bag values override the base. The URL and layout stay + // structural - the URL is resolved via getTranslatableUrl and the layout + // always comes from the base page. + foreach ((array) $mirror->getViewBag()->getProperties() as $name => $value) { + if (in_array($name, ['url', 'layout']) || $value === null || $value === '') { + continue; + } + + $this->getViewBag()->setProperty($name, $value); + } + + $this->fillViewBagArray(); + + if (strlen(trim((string) $mirror->markup))) { + $this->markup = $mirror->markup; + $this->processedMarkupCache = false; + } + + // Placeholder content is stored as {% put %} blocks in the code section + if (strlen(trim((string) $mirror->code))) { + $this->attributes['code'] = $mirror->code; + unset($this->attributes['placeholders']); + } + } + + /** + * getTwigCacheKey differentiates compiled templates per applied locale, since + * translated markup is rendered under the base page's file path. + */ + public function getTwigCacheKey() + { + $key = parent::getTwigCacheKey(); + + if ($this->appliedSiteLocale !== null) { + $key .= '-'.$this->appliedSiteLocale; + } + + return $key; + } + + /** + * deleteLocaleMirrors removes any translated mirror files for this page. + */ + protected function deleteLocaleMirrors() + { + $pattern = $this->theme->getPath().'/content/static-pages-*/'.$this->fileName; + + foreach (File::glob($pattern) ?: [] as $filePath) { + File::delete($filePath); + } + } + /** * getProcessedMarkup will return the processed markup of a page */ @@ -666,11 +738,21 @@ public function getProcessedPlaceholderMarkup($placeholderName, $placeholderCont // /** - * Returns a cache key for this record. + * getMenuCacheKey returns a cache key for this record */ - protected static function getMenuCacheKey($theme) + protected static function getMenuCacheKey($theme, $locale = null) { $key = crc32($theme->getPath()).'static-page-menu'; + + // Menu trees hold translated URLs and titles, cache them per locale + if ($locale === null && Site::hasMultiSite()) { + $locale = Site::getActiveSite()?->hard_locale; + } + + if ($locale) { + $key .= '-'.$locale; + } + /** * @event pages.page.getMenuCacheKey * Enables modifying the key used to reference cached RainLab.Pages menu trees @@ -687,7 +769,7 @@ protected static function getMenuCacheKey($theme) } /** - * Returns whether the specified URLs are equal. + * urlsAreEqual returns whether the specified URLs are equal */ protected static function urlsAreEqual($url, $other) { @@ -695,18 +777,27 @@ protected static function urlsAreEqual($url, $other) } /** - * Clears the menu item cache + * clearMenuCache clears the menu item cache * @param \Cms\Classes\Theme $theme Specifies the current theme. */ public static function clearMenuCache($theme) { Cache::forget(self::getMenuCacheKey($theme)); + + // Clear every locale's menu tree + if (Site::hasMultiSite()) { + foreach (Site::listSites() as $site) { + if ($site->hard_locale) { + Cache::forget(self::getMenuCacheKey($theme, $site->hard_locale)); + } + } + } } /** - * Handler for the pages.menuitem.getTypeInfo event. - * Returns a menu item type information. The type information is returned as array - * with the following elements: + * getMenuTypeInfo is the handler for the cms.pageLookup.getTypeInfo event + * + * The type information is returned as array with the following elements: * - references - a list of the item type reference options. The options are returned in the * ["key"] => "title" format for options that don't have sub-options, and in the format * ["key"] => ["title"=>"Option title", "items"=>[...]] for options that have sub-options. Optional, @@ -738,9 +829,9 @@ public static function getMenuTypeInfo($type) } /** - * Handler for the pages.menuitem.resolveItem event. - * Returns information about a menu item. The result is an array - * with the following keys: + * resolveMenuItem is the handler for the cms.pageLookup.resolveItem event + * + * The result is an array with the following keys: * - url - the menu item URL. Not required for menu item types that return all available records. * The URL should be returned relative to the website root and include the subdirectory, if any. * Use the Cms::url() helper to generate the URLs. @@ -750,8 +841,7 @@ public static function getMenuTypeInfo($type) * The items array should be added only if the $item's $nesting property value is TRUE. * @param \RainLab\Pages\Classes\MenuItem $item Specifies the menu item. * @param \Cms\Classes\Theme $theme Specifies the current theme. - * @param string $url Specifies the current page URL, normalized, in lower case - * The URL is specified relative to the website root, it includes the subdirectory name, if any. + * @param string $url Specifies the current page URL, normalized, in lower case. * @return mixed Returns an array. Returns null if the item cannot be resolved. */ public static function resolveMenuItem($item, $url, $theme) @@ -809,9 +899,7 @@ public static function resolveMenuItem($item, $url, $theme) } /** - * Handler for the backend.richeditor.getTypeInfo event. - * Returns a menu item type information. The type information is returned as array - * + * getRichEditorTypeInfo is the handler for the backend.richeditor.getTypeInfo event * @param string $type Specifies the page link type * @return array Array of available link targets keyed by URL ['https://example.com/' => 'Homepage] */ @@ -847,24 +935,25 @@ public static function getRichEditorTypeInfo($type) } /** - * Builds and caches a menu item tree. - * This method is used internally for menu items and breadcrumbs. + * buildMenuTree builds and caches a menu item tree * @param \Cms\Classes\Theme $theme Specifies the current theme. * @return array Returns an array containing the page information */ public static function buildMenuTree($theme) { - if (self::$menuTreeCache !== null) { - return self::$menuTreeCache; - } - + // The request-level cache is keyed per theme and locale, matching the + // persistent cache, so iterating sites in one request stays correct. $key = self::getMenuCacheKey($theme); + if (is_array(self::$menuTreeCache) && array_key_exists($key, self::$menuTreeCache)) { + return self::$menuTreeCache[$key]; + } + $cached = Cache::get($key, false); $unserialized = $cached ? @unserialize($cached) : false; if ($unserialized !== false) { - return self::$menuTreeCache = $unserialized; + return self::$menuTreeCache[$key] = $unserialized; } $menuTree = [ @@ -875,9 +964,13 @@ public static function buildMenuTree($theme) $result = []; foreach ($items as $item) { + // Overlay the active site's translated title and URL, if any + $item->page->applySiteContext(); + $viewBag = $item->page->viewBag; $pageCode = $item->page->getBaseFileName(); - $pageUrl = Str::lower(RouterHelper::normalizeUrl(array_get($viewBag, 'url'))); + $pageUrl = $item->page->getTranslatableUrl() ?: array_get($viewBag, 'url'); + $pageUrl = Str::lower(RouterHelper::normalizeUrl($pageUrl)); $itemData = [ 'url' => $pageUrl, @@ -902,17 +995,16 @@ public static function buildMenuTree($theme) $pageList = new PageList($theme); $iterator($pageList->getPageTree(), null, 0); - self::$menuTreeCache = $menuTree; + self::$menuTreeCache[$key] = $menuTree; $comboConfig = Config::get('cms.template_cache_ttl', 10); $expiresAt = now()->addMinutes($comboConfig); Cache::put($key, serialize($menuTree), $expiresAt); - return self::$menuTreeCache; + return self::$menuTreeCache[$key]; } /** - * Returns a list of options for the Reference drop-down menu in the - * menu item configuration form, when the Static Page item type is selected. + * listStaticPageMenuOptions returns a list of options for the Reference drop-down menu in the menu item configuration form * @return array Returns an array */ protected static function listStaticPageMenuOptions() @@ -947,11 +1039,7 @@ protected static function listStaticPageMenuOptions() } /** - * Disables safe mode check for static pages. - * - * This allows developers to use placeholders in layouts even if safe mode is enabled. - * - * @return void + * checkSafeMode disables the safe mode check for static pages, allowing placeholders in layouts even if safe mode is enabled */ protected function checkSafeMode() { diff --git a/classes/PageList.php b/classes/PageList.php index c4f14a14..a88cef76 100644 --- a/classes/PageList.php +++ b/classes/PageList.php @@ -4,19 +4,22 @@ use RainLab\Pages\Classes\Page; /** - * The page list class reads and manages the static page hierarchy. - * - * @package rainlab\pages - * @author Alexey Bobkov, Samuel Georges + * PageList reads and manages the static page hierarchy. */ class PageList { + /** + * @var \Cms\Classes\Theme theme parent theme + */ protected $theme; + /** + * @var mixed configCache + */ protected static $configCache = false; /** - * Creates the page list object. + * __construct creates the page list object * @param \Cms\Classes\Theme $theme Specifies a parent theme. */ public function __construct($theme) @@ -25,8 +28,7 @@ public function __construct($theme) } /** - * Returns a list of static pages in the specified theme. - * This method is used internally by the system. + * listPages returns a list of static pages in the specified theme * @param boolean $skipCache Indicates if objects should be reloaded from the disk bypassing the cache. * @return object Returns an array of static pages. */ @@ -36,10 +38,7 @@ public function listPages($skipCache = false) } /** - * Returns a list of top-level pages with subpages. - * The method uses the theme's meta/static-pages.yaml file to build the hierarchy. The pages are returned - * in the order defined in the YAML file. The result of the method is used for building the back-end UI - * and for generating the menus. + * getPageTree returns a list of top-level pages with subpages * @param boolean $skipCache Indicates if objects should be reloaded from the disk bypassing the cache. * @return array Returns a nested array of objects: object('page': $pageObj, 'subpages'=>[...]) */ @@ -72,7 +71,7 @@ public function getPageTree($skipCache = false) } /** - * Returns the parent name of the specified page. + * getPageParent returns the parent name of the specified page * @param \Cms\Classes\Page $page Specifies a page object. * @param string Returns the parent page name. */ @@ -104,7 +103,7 @@ public function getPageParent($page) } /** - * Returns a part of the page hierarchy starting from the specified page. + * getPageSubTree returns a part of the page hierarchy starting from the specified page * @param \Cms\Classes\Page $page Specifies a page object. * @param array Returns a nested array of page names. */ @@ -137,8 +136,7 @@ public function getPageSubTree($page) } /** - * Appends page to the page hierarchy. - * The page can be added to the end of the hierarchy or as a subpage to any existing page. + * appendPage appends a page to the page hierarchy */ public function appendPage($page) { @@ -171,7 +169,7 @@ public function appendPage($page) } /** - * Removes a part of the page hierarchy starting from the specified page. + * removeSubtree removes a part of the page hierarchy starting from the specified page * @param \Cms\Classes\Page $page Specifies a page object. */ public function removeSubtree($page) @@ -198,7 +196,7 @@ public function removeSubtree($page) } /** - * Returns the parsed meta/static-pages.yaml file contents. + * getPagesConfig returns the parsed meta/static-pages.yaml file contents * @return mixed */ protected function getPagesConfig() @@ -224,7 +222,7 @@ protected function getPagesConfig() } /** - * Updates the page hierarchy structure in the theme's meta/static-pages.yaml file. + * updateStructure updates the page hierarchy structure in the theme's meta/static-pages.yaml file * @param array $structure A nested associative array representing the page structure */ public function updateStructure($structure) diff --git a/classes/PageLocale.php b/classes/PageLocale.php new file mode 100644 index 00000000..caf639e8 --- /dev/null +++ b/classes/PageLocale.php @@ -0,0 +1,95 @@ +theme, $page->fileName); + }); + } + + /** + * getObjectTypeDirName + */ + public function getObjectTypeDirName() + { + return 'content/static-pages-'.static::$contextLocale; + } + + /** + * beforeCreate keeps the file name assigned by the caller - mirrors always + * share their base page's file name. + */ + public function beforeCreate() + { + } + + /** + * afterCreate does not touch the meta index. + */ + public function afterCreate() + { + } + + /** + * beforeValidate has no unique URL constraints for mirrors. + */ + public function beforeValidate() + { + } + + /** + * appendToMeta is disabled for mirrors. + */ + protected function appendToMeta() + { + } + + /** + * removeFromMeta is disabled for mirrors. + */ + protected function removeFromMeta() + { + } +} diff --git a/classes/Router.php b/classes/Router.php index d7b238a5..ccc8c0de 100644 --- a/classes/Router.php +++ b/classes/Router.php @@ -1,6 +1,6 @@ getCacheKey('static-page-url-map'); + + if (isset(self::$cache[$cacheKey]) && array_key_exists($url, self::$cache[$cacheKey])) { + return self::$cache[$cacheKey][$url]; } - $urlMap = $this->getUrlMap(); + $urlMap = $this->getUrlMap($cacheKey); $urlMap = array_key_exists('urls', $urlMap) ? $urlMap['urls'] : []; if (!array_key_exists($url, $urlMap)) { @@ -70,38 +71,33 @@ public function findByUrl($url) */ $this->clearCache(); - return self::$cache[$url] = Page::loadCached($this->theme, $fileName); + return self::$cache[$cacheKey][$url] = Page::loadCached($this->theme, $fileName); } - return self::$cache[$url] = $page; + return self::$cache[$cacheKey][$url] = $page; } /** - * Autoloads the URL map only allowing a single execution. + * getUrlMap autoloads the URL map only allowing a single execution * @return array Returns the URL map. */ - protected function getUrlMap() + protected function getUrlMap($cacheKey) { - if (!count(self::$urlMap)) { - $this->loadUrlMap(); + if (empty(self::$urlMap[$cacheKey])) { + $this->loadUrlMap($cacheKey); } - return self::$urlMap; + return self::$urlMap[$cacheKey]; } /** - * Loads the URL map - a list of page file names and corresponding URL patterns. - * The URL map can is cached. The clearUrlMap() method resets the cache. By default - * the map is updated every time when a page is saved in the back-end, or - * when the interval defined with the cms.urlCacheTtl expires. + * loadUrlMap loads the URL map - a list of page file names and corresponding URL patterns * @return boolean Returns true if the URL map was loaded from the cache. Otherwise returns false. */ - protected function loadUrlMap() + protected function loadUrlMap($cacheKey) { - $key = $this->getCacheKey('static-page-url-map'); - $cacheable = Config::get('cms.enable_route_cache', false); - $cached = $cacheable ? Cache::get($key, false) : false; + $cached = $cacheable ? Cache::get($cacheKey, false) : false; if (!$cached || ($unserialized = @unserialize($cached)) === false) { /* @@ -120,7 +116,8 @@ protected function loadUrlMap() continue; } - $url = $page->getViewBag()->property('url'); + // Prefer the translated URL for the active site, if any + $url = $page->getTranslatableUrl() ?: $page->getViewBag()->property('url'); if (!$url) { continue; } @@ -133,30 +130,40 @@ protected function loadUrlMap() $map['titles'][$file] = $page->getViewBag()->property('title'); } - self::$urlMap = $map; + self::$urlMap[$cacheKey] = $map; if ($cacheable) { $comboConfig = Config::get('cms.url_cache_ttl', 10); $expiresAt = now()->addMinutes($comboConfig); - Cache::put($key, serialize($map), $expiresAt); + Cache::put($cacheKey, serialize($map), $expiresAt); } return false; } - self::$urlMap = $unserialized; + self::$urlMap[$cacheKey] = $unserialized; return true; } /** - * Returns the caching URL key depending on the theme. + * getCacheKey returns the caching URL key depending on the theme * @param string $keyName Specifies the base key name. * @return string Returns the theme-specific key name. */ - protected function getCacheKey($keyName) + protected function getCacheKey($keyName, $locale = null) { $key = crc32($this->theme->getPath()).$keyName; + + // URL maps hold translated URLs, cache them per locale + if ($locale === null && Site::hasMultiSite()) { + $locale = Site::getActiveSite()?->hard_locale; + } + + if ($locale) { + $key .= '-'.$locale; + } + /** * @event pages.router.getCacheKey * Enables modifying the key used to reference cached RainLab.Pages routes @@ -173,12 +180,21 @@ protected function getCacheKey($keyName) } /** - * Clears the router cache. + * clearCache clears the router cache */ public function clearCache() { self::$cache = []; self::$urlMap = []; Cache::forget($this->getCacheKey('static-page-url-map')); + + // Clear every locale's map + if (Site::hasMultiSite()) { + foreach (Site::listSites() as $site) { + if ($site->hard_locale) { + Cache::forget($this->getCacheKey('static-page-url-map', $site->hard_locale)); + } + } + } } } diff --git a/classes/content/fields.yaml b/classes/content/fields.yaml deleted file mode 100644 index 19fe7a75..00000000 --- a/classes/content/fields.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# =================================== -# Field Definitions -# =================================== - -fields: - fileName: - label: cms::lang.editor.filename - attributes: - default-focus: 1 - - toolbar: - type: partial - path: content_toolbar - cssClass: collapse-visible - - components: RainLab\Pages\FormWidgets\Components - -secondaryTabs: - stretch: true - fields: - markup: - tab: cms::lang.editor.content - stretch: true - type: codeeditor - language: html - theme: chrome - showGutter: false - highlightActiveLine: false - fontSize: 13 - cssClass: pagesTextEditor - margin: 20 - - markup_html: - tab: cms::lang.editor.content - stretch: true - type: richeditor - size: huge - valueFrom: markup diff --git a/classes/editorextension/HasContentCrud.php b/classes/editorextension/HasContentCrud.php new file mode 100644 index 00000000..c6bd861f --- /dev/null +++ b/classes/editorextension/HasContentCrud.php @@ -0,0 +1,190 @@ +getRequestContentPath($documentData); + + $content = Content::load($this->getContentTheme(), $path); + if (!$content) { + throw new SystemException(sprintf('The content block %s was not found.', $path)); + } + + return [ + 'document' => $this->contentToDocumentArray($content), + 'metadata' => $this->contentMetadata($content) + ]; + } + + /** + * saveContentDocument creates or updates a content block. + */ + protected function saveContentDocument($controller) + { + $documentData = (array) post('documentData'); + $metadata = (array) post('documentMetadata'); + $forceSave = (bool) post('documentForceSave'); + + $theme = $this->getContentTheme(); + $path = trim((string) array_get($metadata, 'path')); + + $content = strlen($path) + ? Content::load($theme, $path) + : Content::inTheme($theme); + + if (!$content) { + throw new SystemException(sprintf('The content block %s was not found.', $path)); + } + + if ( + strlen($path) && + !$forceSave && + $content->mtime && + array_get($metadata, 'mtime') != $content->mtime + ) { + return ['mtimeMismatch' => true]; + } + + $fileName = (string) array_get($documentData, 'fileName'); + if (strlen($fileName)) { + // Static page storage is managed by the page document type. + if (preg_match('#^/?static-pages(-[^/]+)?(/|$)#', ltrim($fileName, '/'))) { + throw new ApplicationException(__("Content files cannot be saved in the static pages directory.")); + } + + $content->fileName = $fileName; + } + + $markup = (string) array_get($documentData, 'markup'); + if (Config::get('system.convert_line_endings', false) === true) { + $markup = str_replace(["\r\n", "\r"], "\n", $markup); + } + + $content->markup = $markup; + $content->save(); + + Event::fire('cms.template.save', [$controller, $content, 'content']); + + return [ + 'metadata' => $this->contentMetadata($content) + ]; + } + + /** + * deleteContentDocument removes a content block. + */ + protected function deleteContentDocument($controller) + { + $metadata = (array) post('documentMetadata'); + $path = trim((string) array_get($metadata, 'path')); + + $content = Content::load($this->getContentTheme(), $path); + if ($content) { + $content->delete(); + Event::fire('cms.template.delete', [$controller, $content]); + } + } + + /** + * contentToDocumentArray flattens a content block into the client document shape. + */ + protected function contentToDocumentArray(Content $content): array + { + $extension = strtolower(File::extension($content->fileName)); + + return [ + 'fileName' => ltrim($content->fileName, '/'), + 'markup' => $content->markup, + 'language' => $this->contentLanguage($extension) + ]; + } + + /** + * contentLanguage maps a content file extension to an editor surface id. + * htm/html opens in the richeditor, md in the markdown editor, everything + * else in a plain code editor. + */ + protected function contentLanguage(string $extension): string + { + switch ($extension) { + case 'htm': + case 'html': + return 'richeditor'; + case 'md': + return 'markdown'; + case 'txt': + return 'plaintext'; + default: + return 'html'; + } + } + + /** + * contentMetadata builds the navigator/tab metadata for a content block. + */ + protected function contentMetadata(Content $content): array + { + $fileName = ltrim($content->fileName, '/'); + $navigatorPath = dirname($fileName); + if ($navigatorPath === '.') { + $navigatorPath = ''; + } + + return [ + 'mtime' => $content->mtime, + 'path' => $fileName, + 'fileName' => basename($fileName), + 'navigatorPath' => $navigatorPath, + 'uniqueKey' => $fileName, + 'type' => EditorExtension::DOCUMENT_TYPE_CONTENT + ]; + } + + /** + * getRequestContentPath extracts the requested content path from posted data. + */ + protected function getRequestContentPath($documentData): string + { + $path = is_array($documentData) + ? array_get($documentData, 'key', array_get($documentData, 'path')) + : $documentData; + + $path = trim((string) $path); + + if (!strlen($path)) { + throw new SystemException('Missing content path.'); + } + + return $path; + } + + /** + * getContentTheme returns the theme being edited. + */ + protected function getContentTheme(): Theme + { + $theme = Theme::getEditTheme(); + if (!$theme) { + throw new SystemException('The edit theme is not set.'); + } + + return $theme; + } +} diff --git a/classes/editorextension/HasMenuCrud.php b/classes/editorextension/HasMenuCrud.php new file mode 100644 index 00000000..f8a92be5 --- /dev/null +++ b/classes/editorextension/HasMenuCrud.php @@ -0,0 +1,309 @@ +getRequestMenuCode($documentData); + + $menu = Menu::load($this->getMenuTheme(), $code . '.yaml'); + if (!$menu) { + throw new SystemException(sprintf('The menu %s was not found.', $code)); + } + + return [ + 'document' => $this->menuToDocumentArray($menu), + 'metadata' => $this->menuMetadata($menu) + ]; + } + + /** + * command_onSaveMenu creates or updates a menu. + */ + protected function saveMenuDocument($controller) + { + $documentData = (array) post('documentData'); + $metadata = (array) post('documentMetadata'); + $forceSave = (bool) post('documentForceSave'); + + $theme = $this->getMenuTheme(); + $code = trim((string) array_get($metadata, 'path')); + $code = preg_replace('/\.yaml$/', '', $code); + + $menu = strlen($code) + ? Menu::load($theme, $code . '.yaml') + : Menu::inTheme($theme); + + if (!$menu) { + throw new SystemException(sprintf('The menu %s was not found.', $code)); + } + + if ( + strlen($code) && + !$forceSave && + $menu->mtime && + array_get($metadata, 'mtime') != $menu->mtime + ) { + return ['mtimeMismatch' => true]; + } + + $settings = (array) array_get($documentData, 'settings', []); + $items = array_get($documentData, 'items', []); + $items = is_array($items) ? $this->normalizeItems($items) : []; + + // With a non-primary-locale site selected, posted title/url values are + // stored as per-item locale translations and the base values are kept. + if (strlen($code) && ($locale = $this->getEditLocale())) { + $originalItems = (array) array_get($menu->attributes, 'items', []); + $items = $this->localizeItemData($items, $originalItems, $locale); + } + + $menu->fill([ + 'name' => (string) array_get($settings, 'name'), + // The code is a root document property (edited in the header) + 'code' => (string) (array_get($documentData, 'code') ?: array_get($settings, 'code', $code)), + 'itemData' => $items + ]); + + $menu->save(); + + Event::fire('cms.template.save', [$controller, $menu, 'menu']); + + return [ + 'metadata' => $this->menuMetadata($menu) + ]; + } + + /** + * command_onDeleteMenu removes a menu. + */ + protected function deleteMenuDocument($controller) + { + $metadata = (array) post('documentMetadata'); + $code = preg_replace('/\.yaml$/', '', trim((string) array_get($metadata, 'path'))); + + $menu = Menu::load($this->getMenuTheme(), $code . '.yaml'); + if ($menu) { + $menu->delete(); + Event::fire('cms.template.delete', [$controller, $menu]); + } + } + + /** + * menuToDocumentArray flattens a menu into the client document shape. + */ + protected function menuToDocumentArray(Menu $menu): array + { + $items = $this->itemsToArray($menu->items); + + // When the backend site picker selects a non-primary locale, show that + // locale's item translations (viewBag.locale.{locale}.{field}). + if ($locale = $this->getEditLocale()) { + $items = $this->applyItemsEditLocale($items, $locale); + } + + return [ + 'name' => $menu->name, + 'code' => $menu->getBaseFileName(), + 'items' => $items, + 'settings' => [ + 'name' => $menu->name, + 'code' => $menu->getBaseFileName() + ] + ]; + } + + /** + * applyItemsEditLocale replaces item fields with their translated values for + * display in the editor (viewBag.locale.{locale}.{field}). + */ + protected function applyItemsEditLocale(array $items, string $locale): array + { + foreach ($items as &$item) { + $localeFields = (array) array_get($item, 'viewBag.locale.'.$locale, []); + + foreach (['title', 'url'] as $fieldName) { + $value = array_get($localeFields, $fieldName); + if ($value !== null && $value !== '' && array_key_exists($fieldName, $item)) { + $item[$fieldName] = $value; + } + } + + if (!empty($item['items']) && is_array($item['items'])) { + $item['items'] = $this->applyItemsEditLocale($item['items'], $locale); + } + } + + return $items; + } + + /** + * localizeItemData stores the posted title/url values as locale translations + * and restores the base values from the menu on disk. The url is only + * translated for url-type items. + */ + protected function localizeItemData(array $postedItems, array $originalItems, string $locale): array + { + foreach ($postedItems as $index => &$item) { + $original = (array) ($originalItems[$index] ?? []); + $localeData = (array) array_get($original, 'viewBag.locale', []); + + foreach (['title', 'url'] as $fieldName) { + $value = array_get($item, $fieldName); + if ($value === null) { + continue; + } + + // Restore the base value; for items new to this locale session the + // posted value becomes the base value too. + $originalValue = array_get($original, $fieldName, $value); + array_set($item, $fieldName, $originalValue); + + $localeData[$locale][$fieldName] = $value; + } + + // Only url-type items carry a translated URL + if (array_get($item, 'type', 'url') !== 'url') { + foreach ($localeData as &$targetData) { + unset($targetData['url']); + } + unset($targetData); + } + + if ($localeData) { + array_set($item, 'viewBag.locale', $localeData); + } + + if (!empty($item['items']) && is_array($item['items'])) { + $item['items'] = $this->localizeItemData( + $item['items'], + (array) array_get($original, 'items', []), + $locale + ); + } + } + + return $postedItems; + } + + /** + * normalizeItems recursively coerces boolean flags to the string format menus store. + */ + protected function normalizeItems(array $items): array + { + foreach ($items as &$item) { + foreach (['nesting', 'replace'] as $flag) { + if (array_key_exists($flag, $item)) { + $item[$flag] = (!$item[$flag] || $item[$flag] === '0') ? '0' : '1'; + } + } + + if (!empty($item['items']) && is_array($item['items'])) { + $item['items'] = $this->normalizeItems($item['items']); + } + } + + return $items; + } + + /** + * itemsToArray recursively serializes menu items into plain arrays for the client. + */ + protected function itemsToArray($items): array + { + $result = []; + $typeOptions = $this->menuItemTypeOptions(); + + foreach ($items as $item) { + $data = $item->toArray(); + + // typeLabel drives the tree-row subtitle (e.g. "Static page"). + $data['typeLabel'] = array_get($typeOptions, $item->type, $item->type); + + if ($item->items) { + $data['items'] = $this->itemsToArray($item->items); + } + $result[] = $data; + } + + return $result; + } + + /** + * menuItemTypeOptions returns the map of menu item type => human label. + */ + protected function menuItemTypeOptions(): array + { + if ($this->menuItemTypeOptionsCache === null) { + $this->menuItemTypeOptionsCache = (new \RainLab\Pages\Classes\MenuItem)->getTypeOptions(); + } + + return $this->menuItemTypeOptionsCache; + } + + /** + * @var array|null menuItemTypeOptionsCache caches the type option map per request. + */ + protected $menuItemTypeOptionsCache = null; + + /** + * menuMetadata builds the navigator/tab metadata for a menu. + */ + protected function menuMetadata(Menu $menu): array + { + $code = $menu->getBaseFileName(); + + return [ + 'mtime' => $menu->mtime, + 'path' => $code, + 'fileName' => $code, + 'navigatorPath' => '', + 'uniqueKey' => $code, + 'type' => EditorExtension::DOCUMENT_TYPE_MENU + ]; + } + + /** + * getRequestMenuCode extracts the requested menu code from posted data. + */ + protected function getRequestMenuCode($documentData): string + { + $code = is_array($documentData) + ? array_get($documentData, 'key', array_get($documentData, 'path')) + : $documentData; + + $code = preg_replace('/\.yaml$/', '', trim((string) $code)); + + if (!strlen($code)) { + throw new SystemException('Missing menu code.'); + } + + return $code; + } + + /** + * getMenuTheme returns the theme being edited. + */ + protected function getMenuTheme(): Theme + { + $theme = Theme::getEditTheme(); + if (!$theme) { + throw new SystemException('The edit theme is not set.'); + } + + return $theme; + } +} diff --git a/classes/editorextension/HasStaticPageCrud.php b/classes/editorextension/HasStaticPageCrud.php new file mode 100644 index 00000000..de70184a --- /dev/null +++ b/classes/editorextension/HasStaticPageCrud.php @@ -0,0 +1,568 @@ +getRequestPath($documentData); + + $page = StaticPage::load($this->getEditTheme(), $path); + if (!$page) { + throw new SystemException(sprintf('The static page %s was not found.', $path)); + } + + // When the backend site picker selects a non-primary locale, overlay the + // translated mirror (content/static-pages-{locale}/) so the editor shows + // and saves that locale's content. + $document = $this->pageToDocumentArray($page); + $metadata = $this->pageMetadata($page); + + if ($locale = $this->getEditLocale()) { + $mirror = PageLocale::findLocale($locale, $page); + $document = $this->overlayLocaleDocument($document, $page, $mirror, $locale); + $metadata['locale'] = $locale; + $metadata['mtime'] = $mirror ? $mirror->mtime : null; + } + + return [ + 'document' => $document, + 'metadata' => $metadata, + 'previewUrl' => $this->pagePreviewUrl(array_get($document, 'settings.url')), + 'hasContentField' => $this->pageHasContentField($page) + ]; + } + + /** + * pagePreviewUrl returns the frontend URL for a page URL string. + */ + protected function pagePreviewUrl($url): ?string + { + $url = trim((string) $url); + + return strlen($url) ? Url::to($url) : null; + } + + /** + * pageHasContentField checks the layout's staticPage component useContent property, + * which hides the content field when disabled. + */ + protected function pageHasContentField(StaticPage $page): bool + { + $layout = $page->getLayoutObject(); + $component = $layout ? $layout->getComponent('staticPage') : null; + + return $component ? (bool) $component->property('useContent', true) : true; + } + + /** + * getEditLocale returns the locale being edited when the backend site picker + * has a non-primary-locale site selected, otherwise null. + */ + protected function getEditLocale(): ?string + { + if (!Site::hasMultiSite()) { + return null; + } + + $site = Site::getSiteFromContext(); + $primary = Site::getPrimarySite(); + if (!$site || !$primary || $site->id === $primary->id) { + return null; + } + + $locale = (string) $site->hard_locale; + + return (strlen($locale) && $locale !== (string) $primary->hard_locale) ? $locale : null; + } + + /** + * overlayLocaleDocument overrides the base document values with the translated + * mirror's content. The URL comes from viewBag.localeUrl in the base file. + */ + protected function overlayLocaleDocument(array $document, StaticPage $page, ?PageLocale $mirror, string $locale): array + { + // Translated URL lives in the base page's view bag + $localeUrl = array_get($page->viewBag, 'localeUrl.'.$locale); + if ($localeUrl !== null && $localeUrl !== '') { + $document['url'] = $localeUrl; + $document['settings']['url'] = $localeUrl; + } + + if (!$mirror) { + return $document; + } + + // Mirror view bag values (title, syntax field data) override the base + foreach ((array) $mirror->getViewBag()->getProperties() as $name => $value) { + if (in_array($name, ['url', 'layout']) || $value === null || $value === '') { + continue; + } + + $document['settings'][$name] = $value; + if (array_key_exists($name, $document)) { + $document[$name] = $value; + } + } + + if (strlen(trim((string) $mirror->markup))) { + $document['markup'] = $mirror->markup; + } + + // Mirror placeholders override where present + $mirrorPlaceholders = (array) $mirror->placeholders; + foreach ($mirrorPlaceholders as $code => $content) { + if (array_key_exists($code, (array) $document['placeholders']) && strlen(trim((string) $content))) { + $document['placeholders'][$code] = $content; + } + } + + return $document; + } + + /** + * savePageDocument creates or updates a static page. + */ + protected function savePageDocument($controller) + { + $documentData = (array) post('documentData'); + $metadata = (array) post('documentMetadata'); + $forceSave = (bool) post('documentForceSave'); + + $theme = $this->getEditTheme(); + $path = trim((string) array_get($metadata, 'path')); + + // Editing an existing page with a non-primary-locale site selected writes + // to that locale's mirror file instead. New pages always create the base. + if (strlen($path) && ($locale = $this->getEditLocale())) { + return $this->saveLocalizedPageDocument($controller, $locale); + } + + $page = strlen($path) + ? StaticPage::load($theme, $path) + : StaticPage::inTheme($theme); + + if (!$page) { + throw new SystemException(sprintf('The static page %s was not found.', $path)); + } + + // Concurrency guard: refuse to overwrite a file changed on disk. + if ( + strlen($path) && + !$forceSave && + $page->mtime && + array_get($metadata, 'mtime') != $page->mtime + ) { + return ['mtimeMismatch' => true]; + } + + $settings = $this->cleanSyntaxFieldData((array) array_get($documentData, 'settings', [])); + + // New pages nest under a parent when created via "Add subpage". + $parentFileName = trim((string) array_get($metadata, 'parentFileName')); + if (!strlen($path) && strlen($parentFileName)) { + $page->parentFileName = $parentFileName; + } + + $fillData = [ + 'settings' => ['viewBag' => $settings], + 'markup' => $this->convertLineEndings((string) array_get($documentData, 'markup')), + ]; + + // Placeholder content is stored as {% put %} blocks, keyed by placeholder code. + $placeholders = array_get($documentData, 'placeholders'); + if (is_array($placeholders)) { + $fillData['placeholders'] = array_map([$this, 'convertLineEndings'], $placeholders); + } + + $page->fill($fillData); + + // New pages without a chosen layout inherit the parent's child layout, or the + // theme layout marked as default. + if (!strlen($path) && !strlen((string) array_get($settings, 'layout'))) { + $parentPage = strlen($parentFileName) + ? StaticPage::load($theme, $parentFileName) + : null; + + $page->setDefaultLayout($parentPage); + } + + $page->validate(); + $page->save(); + + Event::fire('cms.template.save', [$controller, $page, 'static-page']); + + return [ + 'metadata' => $this->pageMetadata($page), + 'previewUrl' => $this->pagePreviewUrl(array_get($page->viewBag, 'url')), + 'placeholderInfo' => $this->getPlaceholderInfo($page), + 'syntaxFieldGroups' => $this->getSyntaxFieldGroups($page), + 'hasContentField' => $this->pageHasContentField($page) + ]; + } + + /** + * convertLineEndings normalizes CRLF/CR to LF when enabled by configuration. + */ + protected function convertLineEndings($content) + { + if (is_string($content) && Config::get('system.convert_line_endings', false) === true) { + $content = str_replace(["\r\n", "\r"], "\n", $content); + } + + return $content; + } + + /** + * saveLocalizedPageDocument writes the posted document to the locale's mirror + * file (content/static-pages-{locale}/), leaving the base page untouched except + * for the translated URL, which is stored in the base view bag as localeUrl. + */ + protected function saveLocalizedPageDocument($controller, string $locale) + { + $documentData = (array) post('documentData'); + $metadata = (array) post('documentMetadata'); + $forceSave = (bool) post('documentForceSave'); + + $theme = $this->getEditTheme(); + $path = trim((string) array_get($metadata, 'path')); + + $page = StaticPage::load($theme, $path); + if (!$page) { + throw new SystemException(sprintf('The static page %s was not found.', $path)); + } + + $mirror = PageLocale::findLocale($locale, $page); + + // Concurrency guard against the mirror file + if ( + $mirror && + !$forceSave && + $mirror->mtime && + array_get($metadata, 'mtime') != $mirror->mtime + ) { + return ['mtimeMismatch' => true]; + } + + $settings = $this->cleanSyntaxFieldData((array) array_get($documentData, 'settings', [])); + + // A URL differing from the base URL is stored as localeUrl.{locale} in the + // base file, matching the translated URL storage read by HasTranslatableBag. + $postedUrl = trim((string) array_get($settings, 'url')); + $baseUrl = (string) array_get($page->viewBag, 'url'); + $localeUrls = (array) array_get($page->viewBag, 'localeUrl', []); + $newLocaleUrls = $localeUrls; + + if (strlen($postedUrl) && $postedUrl !== $baseUrl) { + $newLocaleUrls[$locale] = $postedUrl; + } + else { + unset($newLocaleUrls[$locale]); + } + + if ($newLocaleUrls != $localeUrls) { + $baseViewBag = (array) $page->getViewBag()->getProperties(); + $baseViewBag['localeUrl'] = $newLocaleUrls; + + $page->fill(['settings' => ['viewBag' => $baseViewBag]]); + $page->save(); + } + + // Mirrors never store structural fields; the layout is copied from the + // base so placeholder pruning resolves against the correct layout. + unset($settings['url']); + $settings['layout'] = array_get($page->viewBag, 'layout'); + + $fillData = [ + 'settings' => ['viewBag' => $settings], + 'markup' => $this->convertLineEndings((string) array_get($documentData, 'markup')), + ]; + + $placeholders = array_get($documentData, 'placeholders'); + if (is_array($placeholders)) { + $fillData['placeholders'] = array_map([$this, 'convertLineEndings'], $placeholders); + } + + $mirror = PageLocale::withLocale($locale, function() use ($theme, $page, $mirror, $fillData) { + if (!$mirror) { + $mirror = PageLocale::inTheme($theme); + $mirror->fileName = $page->fileName; + } + + // Fill the settings first so the layout is resolvable when the + // placeholder fill prunes against the layout's placeholder list. + $mirror->fill(['settings' => $fillData['settings']]); + $mirror->fill(array_diff_key($fillData, ['settings' => true])); + $mirror->save(); + + return $mirror; + }); + + Event::fire('cms.template.save', [$controller, $mirror, 'static-page']); + + $result = $this->pageMetadata($page); + $result['locale'] = $locale; + $result['mtime'] = $mirror->mtime; + + $previewUrl = array_get($page->viewBag, 'localeUrl.'.$locale) + ?: array_get($page->viewBag, 'url'); + + return [ + 'metadata' => $result, + 'previewUrl' => $this->pagePreviewUrl($previewUrl), + 'placeholderInfo' => $this->getPlaceholderInfo($page), + 'syntaxFieldGroups' => $this->getSyntaxFieldGroups($page), + 'hasContentField' => $this->pageHasContentField($page) + ]; + } + + /** + * cleanSyntaxFieldData strips repeater bookkeeping keys from posted viewBag data. + */ + protected function cleanSyntaxFieldData(array $data): array + { + $internalKeys = ['_index', '_group']; + + foreach ($data as $key => &$value) { + if (is_array($value)) { + foreach ($internalKeys as $internalKey) { + unset($value[$internalKey]); + } + $value = $this->cleanSyntaxFieldData($value); + } + } + + return $data; + } + + /** + * deletePageDocument removes a static page and its subpages. + */ + protected function deletePageDocument($controller) + { + $metadata = (array) post('documentMetadata'); + $path = trim((string) array_get($metadata, 'path')); + + $page = StaticPage::load($this->getEditTheme(), $path); + if ($page) { + $page->delete(); + Event::fire('cms.template.delete', [$controller, $page]); + } + } + + /** + * updatePageStructure persists a reordered/re-nested page tree to meta/static-pages.yaml. + */ + protected function updatePageStructure($controller) + { + $documentData = (array) post('documentData'); + $structure = array_get($documentData, 'structure', []); + + // The client JSON-encodes the structure (form encoding drops empty-object leaves). + if (is_string($structure)) { + $structure = json_decode($structure, true) ?: []; + } + + $theme = $this->getEditTheme(); + $pageList = new \RainLab\Pages\Classes\PageList($theme); + + // Only persist filenames that are real pages, preserving the posted hierarchy. + $valid = []; + foreach ($pageList->getPageTree(true) as $pageInfo) { + $this->collectValidPageNames($pageInfo, $valid); + } + + $clean = $this->sanitizePageStructure(is_array($structure) ? $structure : [], $valid); + + // Safety: never wipe the structure. If sanitization produced nothing while real + // pages exist, the payload was malformed — refuse rather than clear the yaml. + if (empty($clean) && !empty($valid)) { + throw new SystemException('Refusing to write an empty page structure.'); + } + + $pageList->updateStructure($clean); + + return ['success' => true]; + } + + /** + * collectValidPageNames gathers every page base filename from the page tree. + */ + protected function collectValidPageNames($pageInfo, array &$valid): void + { + $valid[$pageInfo->page->getBaseFileName()] = true; + + if (!empty($pageInfo->subpages)) { + foreach ($pageInfo->subpages as $subpage) { + $this->collectValidPageNames($subpage, $valid); + } + } + } + + /** + * sanitizePageStructure keeps only known page filenames from the posted structure. + */ + protected function sanitizePageStructure(array $structure, array $valid): array + { + $result = []; + + foreach ($structure as $fileName => $children) { + if (!is_string($fileName) || !isset($valid[$fileName])) { + continue; + } + + $result[$fileName] = is_array($children) + ? $this->sanitizePageStructure($children, $valid) + : []; + } + + return $result; + } + + /** + * pageToDocumentArray flattens a page into the client document shape. + */ + protected function pageToDocumentArray(StaticPage $page): array + { + $viewBag = (array) $page->getViewBag()->getProperties(); + + return [ + 'fileName' => ltrim($page->getBaseFileName(), '/'), + 'markup' => $page->markup, + 'placeholders' => $this->getPlaceholderData($page), + 'placeholderInfo' => $this->getPlaceholderInfo($page), + 'syntaxFieldGroups' => $this->getSyntaxFieldGroups($page), + 'settings' => $viewBag + ] + $viewBag; + } + + /** + * getSyntaxFieldGroups returns the layout syntax fields grouped into editor tabs. + * Each distinct field `tab` becomes one content tab; fields without a tab fall back + * to a single "Fields" group. + */ + protected function getSyntaxFieldGroups(StaticPage $page): array + { + $groups = []; + + foreach ($page->listLayoutSyntaxFields() as $fieldCode => $fieldConfig) { + if (($fieldConfig['type'] ?? null) === 'fileupload') { + continue; + } + + $tab = trim((string) ($fieldConfig['tab'] ?? '')) ?: __("Fields"); + $key = 'syntax:'.md5($tab); + + if (!isset($groups[$key])) { + $groups[$key] = ['key' => $key, 'title' => $tab]; + } + } + + return array_values($groups); + } + + /** + * getPlaceholderData returns the current placeholder content keyed by code. + */ + protected function getPlaceholderData(StaticPage $page): array + { + $result = []; + $content = (array) $page->placeholders; + + foreach ($this->getPlaceholderInfo($page) as $code => $info) { + $result[$code] = (string) array_get($content, $code, ''); + } + + return $result; + } + + /** + * getPlaceholderInfo returns the editable placeholders defined by the page layout. + */ + protected function getPlaceholderInfo(StaticPage $page): array + { + $result = []; + + foreach ($page->listLayoutPlaceholders() as $code => $info) { + if (!empty($info['ignore'])) { + continue; + } + + $result[$code] = [ + 'title' => $info['title'], + 'type' => $info['type'] === 'text' ? 'text' : 'html' + ]; + } + + return $result; + } + + /** + * pageMetadata builds the navigator/tab metadata for a page. + */ + protected function pageMetadata(StaticPage $page): array + { + $fileName = $page->getBaseFileName(); + $navigatorPath = dirname($fileName); + if ($navigatorPath === '.') { + $navigatorPath = ''; + } + + return [ + 'mtime' => $page->mtime, + 'path' => $fileName, + 'fileName' => basename($fileName), + 'navigatorPath' => $navigatorPath, + 'uniqueKey' => $fileName, + 'type' => EditorExtension::DOCUMENT_TYPE_PAGE + ]; + } + + /** + * getRequestPath extracts the requested document path from posted data. + */ + protected function getRequestPath($documentData): string + { + // On open, the client posts documentData as { type, key }. + $path = is_array($documentData) + ? array_get($documentData, 'key', array_get($documentData, 'path')) + : $documentData; + + $path = trim((string) $path); + + if (!strlen($path)) { + throw new SystemException('Missing document path.'); + } + + return $path; + } + + /** + * getEditTheme returns the theme being edited. + */ + protected function getEditTheme(): Theme + { + $theme = Theme::getEditTheme(); + if (!$theme) { + throw new SystemException('The edit theme is not set.'); + } + + return $theme; + } +} diff --git a/classes/menu/fields.yaml b/classes/menu/fields.yaml deleted file mode 100644 index 89afe68e..00000000 --- a/classes/menu/fields.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# =================================== -# Field Definitions -# =================================== - -fields: - name: - span: left - label: rainlab.pages::lang.menu.name - placeholder: rainlab.pages::lang.menu.new_name - attributes: - default-focus: 1 - - code: - span: right - placeholder: rainlab.pages::lang.menu.new_code - label: rainlab.pages::lang.menu.code - preset: - field: name - type: file - - toolbar: - type: partial - path: menu_toolbar - cssClass: collapse-visible - -tabs: - stretch: true - cssClass: master-area - paneCssClass: pane-compact - fields: - items: - stretch: true - tab: rainlab.pages::lang.menu.items - type: RainLab\Pages\FormWidgets\MenuItems diff --git a/classes/menuitem/fields.yaml b/classes/menuitem/fields.yaml index e4fa5285..91b29e7d 100644 --- a/classes/menuitem/fields.yaml +++ b/classes/menuitem/fields.yaml @@ -5,65 +5,65 @@ fields: search: - type: Rainlab\Pages\FormWidgets\MenuItemSearch + type: RainLab\Pages\FormWidgets\MenuItemSearch title: span: left - label: rainlab.pages::lang.menuitem.title + label: Title type: span: right - label: rainlab.pages::lang.menuitem.type + label: Type type: dropdown url: - label: rainlab.pages::lang.menuitem.url + label: URL reference: - label: rainlab.pages::lang.menuitem.reference + label: Reference type: dropdown cssClass: input-sidebar-control cmsPage: - label: rainlab.pages::lang.menuitem.cms_page - comment: rainlab.pages::lang.menuitem.cms_page_comment + label: CMS Page + comment: Select a page to open when the menu item is clicked. type: dropdown cssClass: input-sidebar-control nesting: - label: rainlab.pages::lang.menuitem.allow_nested_items - comment: rainlab.pages::lang.menuitem.allow_nested_items_comment + label: Allow nested items + comment: Nested items could be generated dynamically by static page and some other item types type: checkbox default: true replace: - label: rainlab.pages::lang.menuitem.replace - comment: rainlab.pages::lang.menuitem.replace_comment + label: Replace this item with its generated children + comment: Use this checkbox to push generated menu items to the same level with this item. This item itself will be hidden. type: checkbox default: true tabs: fields: viewBag[isHidden]: - label: rainlab.pages::lang.menuitem.hidden - comment: rainlab.pages::lang.menuitem.hidden_comment + label: Hidden + comment: Hide this menu item from appearing on the front-end. type: checkbox - tab: rainlab.pages::lang.menuitem.display_tab + tab: Display code: - label: rainlab.pages::lang.menuitem.code - comment: rainlab.pages::lang.menuitem.code_comment - tab: rainlab.pages::lang.menuitem.attributes_tab + label: Code + comment: Enter the menu item code if you want to access it with the API. + tab: Attributes span: auto viewBag[cssClass]: - label: rainlab.pages::lang.menuitem.css_class - comment: rainlab.pages::lang.menuitem.css_class_comment - tab: rainlab.pages::lang.menuitem.attributes_tab + label: CSS Class + comment: Enter a CSS class name to give this menu item a custom appearance. + tab: Attributes span: auto viewBag[isExternal]: - label: rainlab.pages::lang.menuitem.external_link - comment: rainlab.pages::lang.menuitem.external_link_comment + label: External link + comment: Open links for this menu item in a new window. type: checkbox - tab: rainlab.pages::lang.menuitem.attributes_tab + tab: Attributes diff --git a/classes/page/fields.yaml b/classes/page/fields.yaml deleted file mode 100644 index d25124a8..00000000 --- a/classes/page/fields.yaml +++ /dev/null @@ -1,67 +0,0 @@ -# =================================== -# Field Definitions -# =================================== - -fields: - viewBag[title]: - span: left - label: rainlab.pages::lang.editor.title - placeholder: rainlab.pages::lang.editor.new_title - attributes: - default-focus: 1 - - viewBag[url]: - span: right - placeholder: / - label: rainlab.pages::lang.editor.url - preset: - field: viewBag[title] - type: url - prefixInput: input[data-parent-url] - - toolbar: - type: partial - path: page_toolbar - cssClass: collapse-visible - -tabs: - cssClass: master-area - fields: - viewBag[layout]: - tab: cms::lang.editor.settings - label: rainlab.pages::lang.page.layout - type: dropdown - options: getLayoutOptions - - viewBag[is_hidden]: - tab: cms::lang.editor.settings - span: left - label: rainlab.pages::lang.editor.hidden - type: checkbox - comment: rainlab.pages::lang.editor.hidden_comment - - viewBag[navigation_hidden]: - tab: cms::lang.editor.settings - span: right - label: rainlab.pages::lang.editor.navigation_hidden - type: checkbox - comment: rainlab.pages::lang.editor.navigation_hidden_comment - - viewBag[meta_title]: - tab: cms::lang.editor.meta - label: cms::lang.editor.meta_title - - viewBag[meta_description]: - tab: cms::lang.editor.meta - label: cms::lang.editor.meta_description - type: textarea - size: tiny - -secondaryTabs: - stretch: true - fields: - markup: - tab: rainlab.pages::lang.editor.content - type: richeditor - stretch: true - size: huge diff --git a/classes/staticpage/Fields.php b/classes/staticpage/Fields.php new file mode 100644 index 00000000..5d60078d --- /dev/null +++ b/classes/staticpage/Fields.php @@ -0,0 +1,51 @@ + [ + 'title' => "File Name", + 'type' => 'string', + 'preset' => ['property' => "title", 'type' => 'file'], + 'validation' => [ + 'required' => ['message' => "The File Name is required."] + ] + ], + 'layout' => [ + 'title' => "Layout", + 'type' => 'dropdown', + 'placeholder' => "Layouts not found" + ], + 'is_hidden' => [ + 'title' => "Hidden", + 'type' => 'checkbox', + 'description' => "Hidden pages are accessible only by logged-in back-end users." + ], + 'navigation_hidden' => [ + 'title' => "Hide in navigation", + 'type' => 'checkbox', + 'description' => "Check this box to hide this page from automatically generated menus and breadcrumbs." + ], + 'meta_title' => [ + 'title' => "Title", + 'type' => 'string', + 'tab' => "Meta" + ], + 'meta_description' => [ + 'title' => "Description", + 'type' => 'text', + 'size' => 'medium', + 'tab' => "Meta" + ] + ]; + } +} diff --git a/components/ChildPages.php b/components/ChildPages.php index 7efba113..9883834b 100644 --- a/components/ChildPages.php +++ b/components/ChildPages.php @@ -2,39 +2,40 @@ use Cms\Classes\ComponentBase; +/** + * ChildPages component displays a list of child pages for the current page. + */ class ChildPages extends ComponentBase { /** - * @var \RainLab\Pages\Components\StaticPage A reference to the static page component + * @var \RainLab\Pages\Components\StaticPage staticPageComponent reference */ protected $staticPageComponent; /** - * @var array Array of \RainLab\Pages\Classes\Page references to the child static page objects for the current page + * @var array childPages references to the child static page objects for the current page */ protected $childPages; /** - * @var array Child pages data - * [ - * 'url' => '', - * 'title' => '', - * 'page' => \RainLab\Pages\Classes\Page, - * 'viewBag' => array, - * 'is_hidden' => bool, - * 'navigation_hidden' => bool, - * ] + * @var array pages data for each child page */ public $pages = []; + /** + * componentDetails + */ public function componentDetails() { return [ - 'name' => 'rainlab.pages::lang.component.child_pages_name', - 'description' => 'rainlab.pages::lang.component.child_pages_description' + 'name' => 'Child pages', + 'description' => 'Displays a list of child pages for the current page' ]; } + /** + * onRun + */ public function onRun() { // Check if the staticPage component is attached to the rendering template diff --git a/components/StaticBreadcrumbs.php b/components/StaticBreadcrumbs.php index a1aa8220..0462c3d2 100644 --- a/components/StaticBreadcrumbs.php +++ b/components/StaticBreadcrumbs.php @@ -5,30 +5,31 @@ use RainLab\Pages\Classes\MenuItemReference; use RainLab\Pages\Classes\Page as StaticPageClass; use Cms\Classes\Theme; -use Request; -use Url; /** - * The static breadcrumbs component. - * - * @package rainlab\pages - * @author Alexey Bobkov, Samuel Georges + * StaticBreadcrumbs component outputs breadcrumbs for a static page. */ class StaticBreadcrumbs extends ComponentBase { /** - * @var array An array of the RainLab\Pages\Classes\MenuItemReference class. + * @var array breadcrumbs of RainLab\Pages\Classes\MenuItemReference objects */ public $breadcrumbs = []; + /** + * componentDetails + */ public function componentDetails() { return [ - 'name' => 'rainlab.pages::lang.component.static_breadcrumbs_name', - 'description' => 'rainlab.pages::lang.component.static_breadcrumbs_description' + 'name' => 'Static breadcrumbs', + 'description' => 'Outputs breadcrumbs for a static page.' ]; } + /** + * onRun + */ public function onRun() { $url = $this->getRouter()->getUrl(); diff --git a/components/StaticMenu.php b/components/StaticMenu.php index 77d3aff7..adc0b678 100644 --- a/components/StaticMenu.php +++ b/components/StaticMenu.php @@ -1,49 +1,52 @@ 'rainlab.pages::lang.component.static_menu_name', - 'description' => 'rainlab.pages::lang.component.static_menu_description' + 'name' => 'Static menu', + 'description' => 'Outputs a menu in a CMS layout.' ]; } + /** + * defineProperties + */ public function defineProperties() { return [ 'code' => [ - 'title' => 'rainlab.pages::lang.component.static_menu_code_name', - 'description' => 'rainlab.pages::lang.component.static_menu_code_description', + 'title' => 'Menu', + 'description' => 'Specify a code of the menu the component should output.', 'type' => 'dropdown' ] ]; } + /** + * getCodeOptions + */ public function getCodeOptions() { $result = []; @@ -58,11 +61,17 @@ public function getCodeOptions() return $result; } + /** + * onRun + */ public function onRun() { $this->page['menuItems'] = $this->menuItems(); } + /** + * menuItems + */ public function menuItems() { if ($this->menuItems !== null) { @@ -85,7 +94,7 @@ public function menuItems() } /** - * Counts the total menu items, including children. + * totalItems counts the total menu items, including children */ public function totalItems() { @@ -107,9 +116,7 @@ public function totalItems() } /** - * Resets the menu code and rebuilds the menu. - * @param string $code - * @return array + * resetMenu resets the menu code and rebuilds the menu */ public function resetMenu($code) { diff --git a/components/StaticPage.php b/components/StaticPage.php index c8c2c40e..49b1bbc4 100644 --- a/components/StaticPage.php +++ b/components/StaticPage.php @@ -6,67 +6,73 @@ use Cms\Models\MaintenanceSetting; /** - * The static page component. - * - * @package rainlab\pages - * @author Alexey Bobkov, Samuel Georges + * StaticPage component outputs a static page in a CMS layout. */ class StaticPage extends ComponentBase { /** - * @var \RainLab\Pages\Classes\Page A reference to the static page object + * @var \RainLab\Pages\Classes\Page pageObject reference to the static page object */ public $pageObject; /** - * @var string The static page title + * @var string title of the static page */ public $title; /** - * @var array Extra data added by syntax fields. + * @var array extraData added by syntax fields */ public $extraData = []; /** - * @var string Content cache. + * @var string contentCached */ protected $contentCached = false; + /** + * componentDetails + */ public function componentDetails() { return [ - 'name' => 'rainlab.pages::lang.component.static_page_name', - 'description' => 'rainlab.pages::lang.component.static_page_description' + 'name' => 'Static page', + 'description' => 'Outputs a static page in a CMS layout.' ]; } + /** + * defineProperties + */ public function defineProperties() { return [ 'useContent' => [ - 'title' => 'rainlab.pages::lang.component.static_page_use_content_name', - 'description' => 'rainlab.pages::lang.component.static_page_use_content_description', + 'title' => 'Use page content field', + 'description' => 'If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.', 'default' => 1, 'type' => 'checkbox', 'showExternalParam' => false ], 'default' => [ - 'title' => 'rainlab.pages::lang.component.static_page_default_name', - 'description' => 'rainlab.pages::lang.component.static_page_default_description', + 'title' => 'Default layout', + 'description' => 'Defines this layout as the default for new pages', 'default' => 0, 'type' => 'checkbox', 'showExternalParam' => false ], 'childLayout' => [ - 'title' => 'rainlab.pages::lang.component.static_page_child_layout_name', - 'description' => 'rainlab.pages::lang.component.static_page_child_layout_description', + 'title' => 'Subpage layout', + 'description' => 'The layout to use as the default for any new subpages', 'type' => 'string', 'showExternalParam' => false ] ]; } + /** + * onRun + */ public function onRun() { $url = $this->getRouter()->getUrl(); @@ -88,21 +94,33 @@ public function onRun() } } + /** + * page returns the static page object + */ public function page() { return $this->pageObject; } + /** + * parent returns the parent static page + */ public function parent() { return $this->pageObject ? $this->pageObject->getParent() : null; } + /** + * children returns the child static pages + */ public function children() { return $this->pageObject ? $this->pageObject->getChildren() : null; } + /** + * content returns the processed page markup + */ public function content() { // Evaluate the content property only when it's requested in the @@ -122,8 +140,7 @@ public function content() } /** - * Find foreign view bag values and add them to - * the component and page vars. + * defineExtraData finds foreign view bag values and adds them to the component and page vars. */ protected function defineExtraData() { @@ -167,9 +184,8 @@ protected function isMaintenanceModeEnabled(): bool } /** - * Implements the getter functionality. + * __get implements the getter functionality for extra data. * @param string $name - * @return void */ public function __get($name) { @@ -181,9 +197,8 @@ public function __get($name) } /** - * Determine if an attribute exists on the object. + * __isset determines if an extra data attribute exists. * @param string $key - * @return void */ public function __isset($key) { diff --git a/composer.json b/composer.json index 2dc249b7..5fd64d1c 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "type": "october-plugin", "description": "Pages plugin for October CMS", "homepage": "https://octobercms.com/plugin/rainlab-pages", - "keywords": ["october", "octobercms", "pages"], + "keywords": ["october cms", "october", "pages", "octobercms"], "license": "MIT", "authors": [ { @@ -18,9 +18,7 @@ } ], "require": { - "php": "^8.0.2", - "october/rain": ">=3.4", - "composer/installers": "~1.0" - }, - "minimum-stability": "dev" + "php": ">=8.2", + "october/rain": ">=4.4" + } } diff --git a/config/config.php b/config/config.php deleted file mode 100644 index eed0b81a..00000000 --- a/config/config.php +++ /dev/null @@ -1,27 +0,0 @@ - false, - - /* - |-------------------------------------------------------------------------- - | Sort Menus List By Filename - |-------------------------------------------------------------------------- - | - | You can force the menus list to be sorted by filename - | Keep empty to use the default sorting order. - | - */ - 'menus_sort_by' => false // fileName, name, mtime -]; diff --git a/controllers/Index.php b/controllers/Index.php index 27214e8e..385098f4 100644 --- a/controllers/Index.php +++ b/controllers/Index.php @@ -1,964 +1,149 @@ theme = Theme::getEditTheme())) { - throw new ApplicationException(Lang::get('cms::lang.theme.edit.not_found')); - } - - if ($this->user) { - if ($this->user->hasAccess('rainlab.pages.manage_pages')) { - new PageList($this, 'pageList'); - $this->vars['activeWidgets'][] = 'pageList'; - } - - if ($this->user->hasAccess('rainlab.pages.manage_menus')) { - new MenuList($this, 'menuList'); - $this->vars['activeWidgets'][] = 'menuList'; - } - - if ($this->user->hasAccess('rainlab.pages.manage_content')) { - new TemplateList($this, 'contentList', function() { - return $this->getContentTemplateList(); - }); - $this->vars['activeWidgets'][] = 'contentList'; - } - } - } - catch (Exception $ex) { - $this->handleError($ex); - } - - $context = [ - 'pageList' => 'pages', - 'menuList' => 'menus', - 'contentList' => 'content', - 'snippetList' => 'snippets', - ]; - - BackendMenu::setContext('RainLab.Pages', 'pages', @$context[$this->vars['activeWidgets'][0]]); - } - - // - // Pages, menus and text blocks - // - - public function index() - { - $this->addJs('/plugins/rainlab/pages/assets/js/october.treeview.js', 'RainLab.Pages'); - $this->addJs('/plugins/rainlab/pages/assets/js/pages-page.js', 'RainLab.Pages'); - $this->addCss('/plugins/rainlab/pages/assets/css/pages.css', 'RainLab.Pages'); - $this->addCss('/plugins/rainlab/pages/assets/css/treeview.css', 'RainLab.Pages'); - - // Preload the code editor class as it could be needed - // before it loads dynamically. - $this->addJs('/modules/backend/formwidgets/codeeditor/assets/js/build-min.js', 'core'); - - $this->bodyClass = 'compact-container sidenav-responsive'; - $this->pageTitle = 'rainlab.pages::lang.plugin.name'; - $this->pageTitleTemplate = Lang::get('rainlab.pages::lang.page.template_title'); - - if (Request::ajax() && Request::input('formWidgetAlias')) { - $this->bindFormWidgetToController(); - } - } - - /** - * index_onOpen - */ - public function index_onOpen() - { - $this->validateRequestTheme(); - - $type = Request::input('type'); - $object = $this->loadObject($type, Request::input('path')); - - /* - * Extensibility - */ - Event::fire('pages.object.load', [$this, $object, $type]); - $this->fireEvent('object.load', [$object, $type]); - - return $this->pushObjectForm($type, $object); - } - - /** - * index_onOpenMultiple - */ - public function index_onOpenMultiple() - { - $result = []; - $openTabs = post('openTabs'); - - if (!is_array($openTabs)) { - return; - } - - $maxTabs = Config::get('rainlab.pages::remember_tabs_max', 5); - if ($maxTabs === false) { - return; - } - - if ($maxTabs !== 0) { - $openTabs = array_slice($openTabs, -($maxTabs)); - } - - foreach ($openTabs as $obj) { - $type = $obj['type'] ?? null; - $path = $obj['path'] ?? null; - if (!$type || !$path) { - continue; - } - - $object = $this->loadObject($type, $path, true); - if (!$object) { - continue; - } - - /* - * Extensibility - */ - Event::fire('pages.object.load', [$this, $object, $type]); - $this->fireEvent('object.load', [$object, $type]); - - $result[] = [ - 'type' => $type, - 'path' => $path, - 'theme' => $this->theme->getDirName() - ] + $this->pushObjectForm($type, $object, null, $path); - } - - return ['multiObjects' => $result]; - } - - /** - * onSave - */ - public function onSave() - { - $this->validateRequestTheme(); - $type = Request::input('objectType'); - - $object = $this->fillObjectFromPost($type); - $object->save(); - - /* - * Extensibility - */ - Event::fire('pages.object.save', [$this, $object, $type]); - $this->fireEvent('object.save', [$object, $type]); - - $result = $this->getUpdateResponse($object, $type); - - $successMessages = [ - 'page' => 'rainlab.pages::lang.page.saved', - 'menu' => 'rainlab.pages::lang.menu.saved', - 'content' => 'rainlab.pages::lang.content.saved', - ]; - - $successMessage = isset($successMessages[$type]) - ? $successMessages[$type] - : $successMessages['page']; - - Flash::success(Lang::get($successMessage)); - - return $result; - } - - public function onCreateObject() - { - $this->validateRequestTheme(); - - $type = Request::input('type'); - $object = $this->createObject($type); - $parent = Request::input('parent'); - $parentPage = null; - - if ($type == 'page') { - if (strlen($parent)) { - $parentPage = StaticPage::load($this->theme, $parent); - } - - $object->setDefaultLayout($parentPage); - } - - $widget = $this->makeObjectFormWidget($type, $object); - $this->vars['objectPath'] = ''; - $this->vars['canCommit'] = $this->canCommitObject($object); - $this->vars['canReset'] = $this->canResetObject($object); - - $result = [ - 'tabTitle' => $this->getTabTitle($type, $object), - 'tab' => $this->makePartial('form_page', [ - 'form' => $widget, - 'objectType' => $type, - 'objectTheme' => $this->theme->getDirName(), - 'objectMtime' => null, - 'objectParent' => $parent, - 'parentPage' => $parentPage - ]) - ]; - - return $result; - } - - public function onDelete() - { - $this->validateRequestTheme(); - - $type = Request::input('objectType'); - - $deletedObjects = $this->loadObject($type, trim(Request::input('objectPath')))->delete(); - - $result = [ - 'deletedObjects' => $deletedObjects, - 'theme' => $this->theme->getDirName() - ]; - - return $result; - } - - public function onDeleteObjects() - { - $this->validateRequestTheme(); - - $type = Request::input('type'); - $objects = Request::input('object'); - - if (!$objects) { - $objects = Request::input('template'); - } - - $error = null; - $deleted = []; - - try { - foreach ($objects as $path => $selected) { - if (!$selected) { - continue; - } - $object = $this->loadObject($type, $path, true); - if (!$object) { - continue; - } - - $deletedObjects = $object->delete(); - if (is_array($deletedObjects)) { - $deleted = array_merge($deleted, $deletedObjects); - } - else { - $deleted[] = $path; - } - } - } - catch (Exception $ex) { - $error = $ex->getMessage(); - } - - return [ - 'deleted' => $deleted, - 'error' => $error, - 'theme' => Request::input('theme') - ]; - } - - public function onOpenConcurrencyResolveForm() - { - return $this->makePartial('concurrency_resolve_form'); - } - - public function onGetMenuItemTypeInfo() - { - $type = Request::input('type'); - - return [ - 'menuItemTypeInfo' => MenuItem::getTypeInfo($type) - ]; - } - - public function onUpdatePageLayout() - { - $this->validateRequestTheme(); - - $type = Request::input('objectType'); - - $object = $this->fillObjectFromPost($type); - - return $this->pushObjectForm($type, $object, Request::input('formWidgetAlias')); - } - - public function onMenuItemReferenceSearch() - { - $alias = Request::input('alias'); - - $widget = $this->makeFormWidget( - 'Rainlab\Pages\FormWidgets\MenuItemSearch', - [], - ['alias' => $alias] - ); - - return $widget->onSearch(); - } - - /** - * onCommit commits the DB changes of a object to the filesystem - * @return array $response - */ - public function onCommit() - { - $this->validateRequestTheme(); - $type = Request::input('objectType'); - $object = $this->loadObject($type, trim(Request::input('objectPath'))); - - if ($this->canCommitObject($object)) { - $datasource = $this->getThemeDatasource(); - $datasource->updateModelAtIndex(1, $object); - $datasource->forceDeleteModelAtIndex(0, $object); - Flash::success(Lang::get('cms::lang.editor.commit_success', ['type' => $type])); - } - - return array_merge($this->getUpdateResponse($object, $type), ['forceReload' => true]); - } - - /** - * Resets a object to the version on the filesystem - * - * @return array $response - */ - public function onReset() - { - $this->validateRequestTheme(); - $type = Request::input('objectType'); - $object = $this->loadObject($type, trim(Request::input('objectPath'))); - - if ($this->canResetObject($object)) { - $datasource = $this->getThemeDatasource(); - $datasource->forceDeleteModelAtIndex(0, $object); - Flash::success(Lang::get('cms::lang.editor.reset_success', ['type' => $type])); - } - - return array_merge($this->getUpdateResponse($object, $type), ['forceReload' => true]); - } - - // - // Methods for internal use - // - - /** - * Get the response to return in an AJAX request that updates an object - * - * @param CmsObject $object The object that has been affected - * @param string $type The type of object being affected - * @return array $result; - */ - protected function getUpdateResponse(CmsObject $object, string $type) - { - $result = [ - 'objectPath' => $type != 'content' ? $object->getBaseFileName() : $object->fileName, - 'objectMtime' => $object->mtime, - 'tabTitle' => $this->getTabTitle($type, $object) - ]; - - if ($type == 'page') { - $result['pageUrl'] = $this->getPreviewPageUrl($object); - PagesPlugin::clearCache(); - } - - $result['canCommit'] = $this->canCommitObject($object); - $result['canReset'] = $this->canResetObject($object); - - return $result; - } + use \RainLab\Pages\Controllers\Index\HasSyntaxFields; + use \RainLab\Pages\Controllers\Index\HasMenuItemForm; /** - * Get the active theme's datasource + * @var array requiredPermissions to view this page. */ - protected function getThemeDatasource() - { - return $this->theme->getDatasource(); - } + public $requiredPermissions = ['rainlab.pages.*']; /** - * Check to see if the provided object can be committed - * Only available in debug mode, the DB layer must be enabled, and the object must exist in the database - * - * @param CmsObject $object - * @return boolean + * @var array implement the core Editor state manager behavior. */ - protected function canCommitObject(CmsObject $object) - { - $result = false; - - if ( - Config::get('app.debug', false) && - $this->theme->secondLayerEnabled() && - $this->getThemeDatasource()->hasModelAtIndex(1, $object) - ) { - $result = true; - } - - return $result; - } + public $implement = [ + \Editor\Behaviors\StateManager::class + ]; /** - * Check to see if the provided object can be reset - * Only available when the DB layer is enabled and the object exists in both the DB & Filesystem - * - * @param CmsObject $object - * @return boolean + * @var string editorContext scopes the Editor state to the Pages context, so only + * Pages extensions are hosted here (read by Editor\Behaviors\StateManager). */ - protected function canResetObject(CmsObject $object) - { - $result = false; - - if ($this->theme->secondLayerEnabled()) { - $datasource = $this->getThemeDatasource(); - $result = $datasource->hasModelAtIndex(0, $object) && - $datasource->hasModelAtIndex(1, $object); - } - - return $result; - } + public $editorContext = EditorExtension::CONTEXT; /** - * validateRequestTheme + * @var string turboRouter forces a full reload, Turbo cannot patch a Vue-mounted DOM. */ - protected function validateRequestTheme() - { - if ($this->theme->getDirName() != Request::input('theme')) { - throw new ApplicationException(trans('cms::lang.theme.edit.not_match')); - } - } + public $turboRouter = 'reload'; /** - * loadObject + * __construct the controller. */ - protected function loadObject($type, $path, $ignoreNotFound = false) + public function __construct() { - $class = $this->resolveTypeClassName($type); - - if (!($object = call_user_func(array($class, 'load'), $this->theme, $path))) { - if (!$ignoreNotFound) { - throw new ApplicationException(trans('rainlab.pages::lang.object.not_found')); - } + parent::__construct(); - return null; - } + BackendMenu::setContext('RainLab.Pages', 'pages'); - return $object; - } + $this->bodyClass = 'compact-container editor-page backend-document-layout'; + $this->pageTitle = 'Pages'; - /** - * createObject - */ - protected function createObject($type) - { - $class = $this->resolveTypeClassName($type); + // Re-bind the syntax fields form on every request so its nested widgets + // (repeater, mediafinder) can resolve their own AJAX handlers. + $this->bindSyntaxFieldsWidget(); - if (!($object = $class::inTheme($this->theme))) { - throw new ApplicationException(trans('rainlab.pages::lang.object.not_found')); + // Re-bind the menu item form so its own widgets resolve their AJAX handlers. + if (post('bindMenuItemForm')) { + $this->makeMenuItemFormWidget(); } - - return $object; } /** - * resolveTypeClassName + * index hosts the Editor Application Vue app. */ - protected function resolveTypeClassName($type) - { - $types = [ - 'page' => \RainLab\Pages\Classes\Page::class, - 'menu' => \RainLab\Pages\Classes\Menu::class, - 'content' => \RainLab\Pages\Classes\Content::class - ]; - - if (!array_key_exists($type, $types)) { - throw new ApplicationException(Lang::get('rainlab.pages::lang.object.invalid_type') . ' - type - ' . $type); - } - - $allowed = false; - if ($type === 'content') { - $allowed = $this->user->hasAccess('rainlab.pages.manage_content'); - } - else { - $allowed = $this->user->hasAccess("rainlab.pages.manage_{$type}s"); - } - - if (!$allowed) { - throw new ApplicationException(Lang::get('rainlab.pages::lang.object.unauthorized_type', ['type' => $type])); - } - - return $types[$type]; - } - - protected function makeObjectFormWidget($type, $object, $alias = null) + public function index() { - $formConfigs = [ - 'page' => '~/plugins/rainlab/pages/classes/page/fields.yaml', - 'menu' => '~/plugins/rainlab/pages/classes/menu/fields.yaml', - 'content' => '~/plugins/rainlab/pages/classes/content/fields.yaml' - ]; - - if (!array_key_exists($type, $formConfigs)) { - throw new ApplicationException(Lang::get('rainlab.pages::lang.object.not_found')); - } + $this->addCss('/modules/editor/assets/css/editor.css'); + $this->addCss('/plugins/rainlab/pages/assets/css/editor.css'); + $this->addJs('/modules/editor/assets/js/editor.page.js', ['type' => 'module']); - $widgetConfig = $this->makeConfig($formConfigs[$type]); - $widgetConfig->model = $object; - $widgetConfig->alias = $alias ?: 'form' . studly_case($type) . md5($object->exists ? $object->getFileName() : uniqid()); - $widgetConfig->context = !$object->exists ? 'create' : 'update'; - $widgetConfig->useTranslatable = false; + $this->registerVueComponent(\Backend\VueComponents\Document::class); + $this->registerVueComponent(\Backend\VueComponents\Tabs::class); + $this->registerVueComponent(\Backend\VueComponents\TreeView::class); + $this->registerVueComponent(\Backend\VueComponents\Splitter::class); + $this->registerVueComponent(\Backend\VueComponents\Modal::class); + $this->registerVueComponent(\Backend\VueComponents\Inspector::class); + $this->registerVueComponent(\Backend\VueComponents\Uploader::class); - $widget = $this->makeWidget('Backend\Widgets\Form', $widgetConfig); + $this->registerVueComponent(\Editor\VueComponents\EditorConflictResolver::class); + $this->registerVueComponent(\Editor\VueComponents\Application::class); - if ($type == 'page') { - $widget->bindEvent('form.extendFieldsBefore', function() use ($widget, $object) { - $this->checkContentField($widget, $object); - $this->addPagePlaceholders($widget, $object); - $this->addPageSyntaxFields($widget, $object); - }); + // The StateManager behavior has already scoped the manager to the Pages context + // (via $editorContext), so these list only Pages-context extension assets. + $manager = ExtensionManager::instance(); + foreach ($manager->listJsFiles() as $jsFile) { + $this->addJs($jsFile, ['type' => 'module']); } - - return $widget; - } - - protected function checkContentField($formWidget, $page) - { - if (!($layout = $page->getLayoutObject())) { - return; - } - - $component = $layout->getComponent('staticPage'); - - if (!$component) { - return; + foreach ($manager->listVueComponents() as $componentClass) { + $this->registerVueComponent($componentClass); } - if (!$component->property('useContent', true)) { - unset($formWidget->secondaryTabs['fields']['markup']); - } + $this->vars['customLogo'] = BrandSetting::getLogo(); + $this->vars['initialState'] = $this->makeInitialState([]); } /** - * addPageSyntaxFields adds syntax defined fields to the form + * index_onCommand routes a client command to an extension in the Pages context. */ - protected function addPageSyntaxFields($formWidget, $page) - { - $fields = $page->listLayoutSyntaxFields(); - - foreach ($fields as $fieldCode => $fieldConfig) { - if ($fieldConfig['type'] === 'fileupload') { - continue; - } - - if (in_array($fieldConfig['type'], ['repeater', 'nestedform'])) { - if (empty($fieldConfig['form']) || !is_string($fieldConfig['form'])) { - $repeaterFields = array_get($fieldConfig, 'fields', []); - $fieldConfig['form']['fields'] = $repeaterFields; - unset($fieldConfig['fields']); - } - } - - /* - * Custom fields placement - */ - $placement = !empty($fieldConfig['placement']) ? $fieldConfig['placement'] : null; - - switch ($placement) { - case 'primary': - $formWidget->tabs['fields']['viewBag[' . $fieldCode . ']'] = $fieldConfig; - break; - - default: - $fieldConfig['cssClass'] = 'secondary-tab ' . array_get($fieldConfig, 'cssClass', ''); - $formWidget->secondaryTabs['fields']['viewBag[' . $fieldCode . ']'] = $fieldConfig; - break; - } - - /* - * Translation support - */ - $translatableTypes = ['text', 'textarea', 'richeditor', 'repeater', 'markdown', 'mediafinder', 'nestedform']; - if (in_array($fieldConfig['type'], $translatableTypes) && array_get($fieldConfig, 'translatable', true)) { - $page->translatable[] = 'viewBag['.$fieldCode.']'; - } - } - } - - protected function addPagePlaceholders($formWidget, $page) - { - $placeholders = $page->listLayoutPlaceholders(); - - foreach ($placeholders as $placeholderCode => $info) { - if ($info['ignore']) { - continue; - } - - $placeholderTitle = $info['title']; - $fieldConfig = [ - 'tab' => $placeholderTitle, - 'stretch' => '1', - 'size' => 'huge' - ]; - - if ($info['type'] != 'text') { - $fieldConfig['type'] = 'richeditor'; - } - else { - $fieldConfig['type'] = 'codeeditor'; - $fieldConfig['language'] = 'text'; - $fieldConfig['theme'] = 'chrome'; - $fieldConfig['showGutter'] = false; - $fieldConfig['highlightActiveLine'] = false; - $fieldConfig['cssClass'] = 'pagesTextEditor'; - $fieldConfig['showInvisibles'] = false; - $fieldConfig['fontSize'] = 13; - $fieldConfig['margin'] = '20'; - } - - $formWidget->secondaryTabs['fields']['placeholders['.$placeholderCode.']'] = $fieldConfig; - - /* - * Translation support - */ - $page->translatable[] = 'placeholders['.$placeholderCode.']'; - } - } - - protected function getTabTitle($type, $object) + public function index_onCommand() { - if ($type == 'page') { - $viewBag = $object->getViewBag(); - $result = $viewBag ? $viewBag->property('title') : false; - if (!$result) { - $result = trans('rainlab.pages::lang.page.new'); - } - - return $result; + $namespace = post('extension'); + if (!is_scalar($namespace) || !strlen($namespace)) { + throw new SystemException('Missing extension name'); } - elseif ($type == 'menu') { - $result = $object->name; - if (!strlen($result)) { - $result = trans('rainlab.pages::lang.menu.new'); - } - return $result; + // Only run commands for extensions belonging to the Pages context, keeping this + // page isolated from the global editor extensions. + $extension = ExtensionManager::instance()->getExtensionByNamespace($namespace); + if ($extension->getEditorContext() !== EditorExtension::CONTEXT) { + throw new SystemException('Unsupported extension: '.$namespace); } - elseif ($type == 'content') { - $result = in_array($type, ['asset', 'content']) - ? $object->getFileName() - : $object->getBaseFileName(); - - if (!$result) { - $result = trans('cms::lang.'.$type.'.new'); - } - return $result; + $command = post('command'); + if (!is_scalar($command) || !strlen($command)) { + throw new SystemException('Missing command'); } - return $object->getFileName(); + return ExtensionManager::instance()->runCommand($namespace, $command, $this); } /** - * fillObjectFromPost + * onListExtensionNavigatorSections refreshes the navigator sections. */ - protected function fillObjectFromPost($type) + public function onListExtensionNavigatorSections() { - $objectPath = trim(Request::input('objectPath')); - $object = $objectPath ? $this->loadObject($type, $objectPath) : $this->createObject($type); - - // Set page layout super early because it cascades to other elements - if ($type === 'page' && ($layout = post('viewBag[layout]'))) { - $object->getViewBag()->setProperty('layout', $layout); + $namespace = post('extension'); + if (!is_scalar($namespace) || !strlen($namespace)) { + throw new SystemException('Missing extension namespace'); } - $formWidget = $this->makeObjectFormWidget($type, $object, Request::input('formWidgetAlias')); - - $saveData = $formWidget->getSaveData(); - $postData = post(); - $objectData = []; - - if ($viewBag = array_get($saveData, 'viewBag')) { - $objectData['settings'] = ['viewBag' => $viewBag]; + $documentType = post('documentType'); + if ($documentType && !is_scalar($documentType)) { + throw new SystemException('Invalid document type'); } - $fields = ['markup', 'code', 'fileName', 'content', 'itemData', 'name']; - - if ($type != 'menu' && $type != 'content') { - $object->parentFileName = Request::input('parentFileName'); - } - - foreach ($fields as $field) { - if (array_key_exists($field, $saveData)) { - $objectData[$field] = $saveData[$field]; - } - elseif (array_key_exists($field, $postData)) { - $objectData[$field] = $postData[$field]; - } - } - - if ($type == 'page') { - $placeholders = array_get($saveData, 'placeholders'); - if (is_array($placeholders) && Config::get('system.convert_line_endings', false) === true) { - $placeholders = array_map([$this, 'convertLineEndings'], $placeholders); - } - - $objectData['placeholders'] = $placeholders; - } - - if ($type == 'content') { - $fileName = $objectData['fileName']; - - if (dirname($fileName) == 'static-pages') { - throw new ApplicationException(trans('rainlab.pages::lang.content.cant_save_to_dir')); - } - - $extension = pathinfo($fileName, PATHINFO_EXTENSION); - - if ($extension === 'htm' || $extension === 'html' || !strlen($extension)) { - $objectData['markup'] = array_get($saveData, 'markup_html'); - } + $extension = ExtensionManager::instance()->getExtensionByNamespace($namespace); + if ($extension->getEditorContext() !== EditorExtension::CONTEXT) { + throw new SystemException('Unsupported extension namespace'); } - if ($type == 'menu') { - // If no item data is sent through POST, this means the menu is empty - if (!isset($objectData['itemData'])) { - $objectData['itemData'] = []; - } else { - $objectData['itemData'] = json_decode($objectData['itemData'], true); - if (json_last_error() !== JSON_ERROR_NONE || !is_array($objectData['itemData'])) { - $objectData['itemData'] = []; - } - } - } - - if (!empty($objectData['markup']) && Config::get('system.convert_line_endings', false) === true) { - $objectData['markup'] = $this->convertLineEndings($objectData['markup']); - } - - /* - * Extensibility - */ - Event::fire('pages.object.fillObject', [$this, $object, &$objectData, $type]); - $this->fireEvent('object.fillObject', [$object, &$objectData, $type]); - - if (!Request::input('objectForceSave') && $object->mtime) { - if (Request::input('objectMtime') != $object->mtime) { - throw new ApplicationException('mtime-mismatch'); - } - } - - $object->fill($objectData); - - /* - * Rehydrate the object viewBag array property where values are sourced. - */ - if ($object instanceof CmsCompoundObject && is_array($viewBag)) { - $object->viewBag = $viewBag + $object->viewBag; - } - - return $object; - } - - /** - * pushObjectForm - */ - protected function pushObjectForm($type, $object, $alias = null, $path = null) - { - $widget = $this->makeObjectFormWidget($type, $object, $alias); - - $this->vars['canCommit'] = $this->canCommitObject($object); - $this->vars['canReset'] = $this->canResetObject($object); - $this->vars['objectPath'] = Request::input('path', $path); - $this->vars['lastModified'] = DateTime::makeCarbon($object->mtime); - - if ($type == 'page') { - $this->vars['pageUrl'] = $this->getPreviewPageUrl($object); - } + $namespace = $extension->getNamespaceNormalized(); return [ - 'tabTitle' => $this->getTabTitle($type, $object), - 'tab' => $this->makePartial('form_page', [ - 'form' => $widget, - 'objectType' => $type, - 'objectTheme' => $this->theme->getDirName(), - 'objectMtime' => $object->mtime, - 'objectParent' => Request::input('parentFileName') - ]) + 'sections' => $this->listExtensionNavigatorSections($extension, $namespace, $documentType) ]; } - - /** - * getPreviewPageUrl - */ - protected function getPreviewPageUrl($object) - { - $pageUrl = $object->getViewBag()->property('url'); - - // Support for October CMS 3.0 and below - if (!class_exists('Site')) { - return Url::to($pageUrl); - } - - /** - * Hook the site picker to determine preview - * @see \Cms\Components\SitePicker - */ - $eventPattern = Event::fire('cms.sitePicker.overridePattern', [ - $object, - $pageUrl, - Site::getEditSite(), - Site::getEditSite() - ], true); - - if ($eventPattern) { - $pageUrl = $eventPattern; - } - - return Cms::fullUrl($pageUrl); - } - - /** - * bindFormWidgetToController - */ - protected function bindFormWidgetToController() - { - $alias = Request::input('formWidgetAlias'); - $type = Request::input('objectType'); - $objectPath = trim(Request::input('objectPath')); - - if (!$objectPath) { - $object = $this->createObject($type); - } - else { - $object = $this->loadObject($type, $objectPath); - } - - // Set page layout super early because it cascades to other elements - if ($type === 'page' && ($layout = post('viewBag[layout]'))) { - $object->getViewBag()->setProperty('layout', $layout); - } - - $widget = $this->makeObjectFormWidget($type, $object, $alias); - $widget->bindToController(); - } - - /** - * Replaces Windows style (/r/n) line endings with unix style (/n) - * line endings. - * @param string $markup The markup to convert to unix style endings - * @return string - */ - protected function convertLineEndings($markup) - { - $markup = str_replace("\r\n", "\n", $markup); - $markup = str_replace("\r", "\n", $markup); - - return $markup; - } - - /** - * Returns a list of content files - * @return \October\Rain\Database\Collection - */ - protected function getContentTemplateList() - { - $templates = Content::listInTheme($this->theme, true); - - /** - * @event pages.content.templateList - * Provides opportunity to filter the items returned to the ContentList widget used by the RainLab.Pages plugin in the backend. - * - * >**NOTE**: Recommended to just use cms.object.listInTheme instead - * - * Parameter provided is `$templates` (a collection of the Content CmsObjects being returned). - * > Note: The `$templates` parameter provided is an object reference to a CmsObjectCollection, to make changes you must use object modifying methods. - * - * Example usage (only shows allowed content files): - * - * \Event::listen('pages.content.templateList', function ($templates) { - * foreach ($templates as $index = $content) { - * if (!in_array($content->fileName, $allowedContent)) { - * $templates->forget($index); - * } - * } - * }); - * - * Or: - * - * \RainLab\Pages\Controller\Index::extend(function ($controller) { - * $controller->bindEvent('content.templateList', function ($templates) { - * foreach ($templates as $index = $content) { - * if (!in_array($content->fileName, $allowedContent)) { - * $templates->forget($index); - * } - * } - * }); - * }); - * } - */ - if ( - ($event = $this->fireEvent('content.templateList', [$templates], true)) || - ($event = Event::fire('pages.content.templateList', [$this, $templates], true)) - ) { - return $event; - } - - return $templates; - } } diff --git a/controllers/index/HasMenuItemForm.php b/controllers/index/HasMenuItemForm.php new file mode 100644 index 00000000..d0f58869 --- /dev/null +++ b/controllers/index/HasMenuItemForm.php @@ -0,0 +1,114 @@ +menuItemFormWidget !== null) { + return $this->menuItemFormWidget; + } + + $menuItem = new MenuItem; + + $config = $this->makeConfig('~/plugins/rainlab/pages/classes/menuitem/fields.yaml'); + $config->model = $menuItem; + $config->alias = $this->getMenuItemFormAlias(); + $config->arrayName = 'menuItem'; + + $widget = $this->makeWidget(\Backend\Widgets\Form::class, $config); + $widget->bindToController(); + + return $this->menuItemFormWidget = $widget; + } + + /** + * getMenuItemFormAlias returns the client-supplied form alias. Each open menu + * document uses its own alias so the generated field ids are unique, keeping + * checkbox labels bound to their own document's inputs. + */ + protected function getMenuItemFormAlias(): string + { + $alias = preg_replace('/[^a-zA-Z0-9]/', '', (string) post('formAlias')); + + return strlen($alias) ? $alias : 'menuItemForm'; + } + + /** + * onLoadMenuItemForm renders the per-item Form widget over AJAX. + */ + public function onLoadMenuItemForm() + { + $this->assertMenuPermissions(); + + $widget = $this->makeMenuItemFormWidget(); + + $containerId = preg_replace('/[^a-zA-Z0-9_\-]/', '', (string) post('containerId')); + if (!strlen($containerId)) { + $containerId = 'pagesMenuItemForm'; + } + + return [ + '#'.$containerId => $widget->render(['useContainer' => false]) + ]; + } + + /** + * onGetMenuItemTypeInfo returns type info (references, cmsPages, nesting) for a menu item type. + */ + public function onGetMenuItemTypeInfo() + { + $this->assertMenuPermissions(); + + $type = trim((string) post('type')); + + return [ + 'menuItemTypeInfo' => MenuItem::getTypeInfo($type) + ]; + } + + /** + * onMenuItemReferenceSearch returns matching references for the reference search field. + */ + public function onMenuItemReferenceSearch() + { + $this->assertMenuPermissions(); + + $alias = trim((string) post('alias')); + + $formField = new \Backend\Classes\FormField([ + 'fieldName' => 'referenceSearch', + 'arrayName' => 'menuItem' + ]); + + $widget = new MenuItemSearch($this, $formField, ['alias' => $alias]); + + return $widget->onSearch(); + } + + /** + * assertMenuPermissions guards the menu item handlers. + */ + protected function assertMenuPermissions() + { + if (!$this->user || !$this->user->hasAnyAccess(['rainlab.pages.manage_menus'])) { + throw new \ApplicationException(__("You don't have permissions to manage :type documents.", ['type' => 'menu'])); + } + } +} diff --git a/controllers/index/HasSyntaxFields.php b/controllers/index/HasSyntaxFields.php new file mode 100644 index 00000000..9a78f8b4 --- /dev/null +++ b/controllers/index/HasSyntaxFields.php @@ -0,0 +1,150 @@ +syntaxFieldsWidget !== null) { + return $this->syntaxFieldsWidget; + } + + $theme = Theme::getEditTheme(); + $page = StaticPage::load($theme, $path); + if (!$page) { + return null; + } + + $config = $this->makeConfig(['fields' => []]); + $config->model = $page; + $config->alias = $this->getSyntaxFieldsAlias(); + $config->arrayName = 'syntaxFields'; + $config->context = $page->exists ? 'update' : 'create'; + + $widget = $this->makeWidget(\Backend\Widgets\Form::class, $config); + + $widget->bindEvent('form.extendFieldsBefore', function () use ($widget, $page, $tab) { + $this->addPageSyntaxFields($widget, $page, $tab); + }); + + $widget->bindToController(); + + return $this->syntaxFieldsWidget = $widget; + } + + /** + * addPageSyntaxFields injects the layout syntax fields into the form widget. + * + * When $tab is provided, only fields whose config tab matches are added (fields without + * a tab belong to the "Fields" group). + */ + protected function addPageSyntaxFields($formWidget, $page, $tab = null) + { + $fields = $page->listLayoutSyntaxFields(); + + foreach ($fields as $fieldCode => $fieldConfig) { + if ($fieldConfig['type'] === 'fileupload') { + continue; + } + + if ($tab !== null) { + $fieldTab = trim((string) ($fieldConfig['tab'] ?? '')) ?: __("Fields"); + if ($fieldTab !== $tab) { + continue; + } + } + + if (in_array($fieldConfig['type'], ['repeater', 'nestedform'])) { + if (empty($fieldConfig['form']) || !is_string($fieldConfig['form'])) { + $repeaterFields = array_get($fieldConfig, 'fields', []); + $fieldConfig['form']['fields'] = $repeaterFields; + unset($fieldConfig['fields']); + } + } + + // Drop the tab hint so the island doesn't render its own tab strip; the editor + // provides the tab. + unset($fieldConfig['tab']); + + $formWidget->addFields(['viewBag[' . $fieldCode . ']' => $fieldConfig]); + } + } + + /** + * getSyntaxFieldsAlias returns the client-supplied form alias. Each open page + * document uses its own alias so the generated field ids are unique, keeping + * checkbox labels bound to their own document's inputs. The alias also rides + * along on nested widget AJAX requests via the pagesSyntaxAlias hidden field. + */ + protected function getSyntaxFieldsAlias(): string + { + $alias = preg_replace('/[^a-zA-Z0-9]/', '', (string) (post('formAlias') ?: post('pagesSyntaxAlias'))); + + return strlen($alias) ? $alias : 'pagesSyntaxForm'; + } + + /** + * bindSyntaxFieldsWidget rebuilds the widget on any request carrying a page path. + * + * Nested widgets (repeater, mediafinder) fire their own AJAX handlers, which require the + * parent form to be re-bound to the controller on every request, not only on initial load. + */ + public function bindSyntaxFieldsWidget() + { + $path = trim((string) post('pagesSyntaxPath')); + if (strlen($path)) { + $tab = post('pagesSyntaxTab'); + $this->makeSyntaxFieldsWidget($path, is_string($tab) && strlen($tab) ? $tab : null); + } + } + + /** + * onLoadSyntaxFields renders one tab group of syntax fields for a page over AJAX. + */ + public function onLoadSyntaxFields() + { + if (!$this->user || !$this->user->hasAnyAccess(['rainlab.pages.manage_pages'])) { + throw new \ApplicationException(__("You don't have permissions to manage :type documents.", ['type' => 'static-page'])); + } + + $path = trim((string) post('path')); + $tab = post('tab'); + $tab = is_string($tab) && strlen($tab) ? $tab : null; + + $widget = $this->makeSyntaxFieldsWidget($path, $tab); + + $containerId = trim((string) post('containerId')) ?: 'pagesSyntaxFieldsForm'; + + if (!$widget) { + return ['#'.$containerId => '']; + } + + // Hidden fields let the per-request rebind (bindSyntaxFieldsWidget) rebuild the exact + // same widget so nested repeater/mediafinder AJAX handlers resolve. + $hidden = '' + .'' + .''; + + return [ + '#'.$containerId => $hidden.$widget->render(['useContainer' => false]) + ]; + } +} diff --git a/controllers/index/_concurrency_resolve_form.htm b/controllers/index/_concurrency_resolve_form.htm deleted file mode 100644 index 5e3d7be2..00000000 --- a/controllers/index/_concurrency_resolve_form.htm +++ /dev/null @@ -1,29 +0,0 @@ -'return false']) ?> - - - - \ No newline at end of file diff --git a/controllers/index/_content_toolbar.htm b/controllers/index/_content_toolbar.htm deleted file mode 100644 index 34f0268b..00000000 --- a/controllers/index/_content_toolbar.htm +++ /dev/null @@ -1,51 +0,0 @@ -
- - - - - - - - - - - - - - -
diff --git a/controllers/index/_form_page.htm b/controllers/index/_form_page.htm deleted file mode 100644 index d7d5873e..00000000 --- a/controllers/index/_form_page.htm +++ /dev/null @@ -1,25 +0,0 @@ - 'position-relative h-100', - 'data-change-monitor' => 'true', - 'data-window-close-confirm' => e(trans('backend::lang.form.confirm_tab_close')), - 'data-object-type' => e($objectType) -]) ?> - render() ?> - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/controllers/index/_menu_toolbar.htm b/controllers/index/_menu_toolbar.htm deleted file mode 100644 index 01207e7b..00000000 --- a/controllers/index/_menu_toolbar.htm +++ /dev/null @@ -1,51 +0,0 @@ -
- - - - - - - - - - - - - - -
diff --git a/controllers/index/_page_toolbar.htm b/controllers/index/_page_toolbar.htm deleted file mode 100644 index 6984ce22..00000000 --- a/controllers/index/_page_toolbar.htm +++ /dev/null @@ -1,61 +0,0 @@ -
- - - - - - - - - - - - - - - - - - - -
diff --git a/controllers/index/_sidepanel.htm b/controllers/index/_sidepanel.htm deleted file mode 100644 index 183a8393..00000000 --- a/controllers/index/_sidepanel.htm +++ /dev/null @@ -1,29 +0,0 @@ -
-
-
- -
- widget->pageList->render() ?> -
- - - -
- widget->menuList->render() ?> -
- - - -
- widget->contentList->render() ?> -
- - - -
- widget->snippetList->render() ?> -
- -
-
-
diff --git a/controllers/index/config_content_list.yaml b/controllers/index/config_content_list.yaml deleted file mode 100644 index 511203b4..00000000 --- a/controllers/index/config_content_list.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# =================================== -# Configures the layout list widget -# =================================== - -titleProperty: 'nice_title' -noRecordsMessage: 'cms::lang.content.no_list_records' -deleteConfirmation: 'cms::lang.content.delete_confirm_multiple' -itemType: content -controlClass: filelist-hero content -ignoreDirectories: - - static-pages* diff --git a/controllers/index/index.htm b/controllers/index/index.htm deleted file mode 100644 index de0f71bd..00000000 --- a/controllers/index/index.htm +++ /dev/null @@ -1,32 +0,0 @@ - - fatalError): ?> - makePartial('sidepanel') ?> - - - - - fatalError): ?> -
- -
-
- -
-
-
-
- -
- - -

fatalError)) ?>

- - diff --git a/controllers/index/index.php b/controllers/index/index.php new file mode 100644 index 00000000..d7ff4347 --- /dev/null +++ b/controllers/index/index.php @@ -0,0 +1,12 @@ + + fatalError): ?> +
+ + +
+ + + +

fatalError)) ?>

+ + diff --git a/docs/menu-item.png b/docs/menu-item.png deleted file mode 100644 index a2d8d17d..00000000 Binary files a/docs/menu-item.png and /dev/null differ diff --git a/docs/menu-management.png b/docs/menu-management.png deleted file mode 100644 index a03f4f2e..00000000 Binary files a/docs/menu-management.png and /dev/null differ diff --git a/docs/snippets-backend.png b/docs/snippets-backend.png deleted file mode 100644 index 83df6ece..00000000 Binary files a/docs/snippets-backend.png and /dev/null differ diff --git a/docs/snippets-partial.png b/docs/snippets-partial.png deleted file mode 100644 index 1be6eda0..00000000 Binary files a/docs/snippets-partial.png and /dev/null differ diff --git a/docs/static-layout.png b/docs/static-layout.png deleted file mode 100644 index cb480c68..00000000 Binary files a/docs/static-layout.png and /dev/null differ diff --git a/docs/static-page.png b/docs/static-page.png deleted file mode 100644 index 42833058..00000000 Binary files a/docs/static-page.png and /dev/null differ diff --git a/formwidgets/Components.php b/formwidgets/Components.php deleted file mode 100644 index a56ae1eb..00000000 --- a/formwidgets/Components.php +++ /dev/null @@ -1,92 +0,0 @@ -listComponents(); - - return $this->makePartial('formcomponents', ['components' => $components]); - } - - /** - * listComponents - */ - protected function listComponents() - { - $result = []; - - if (!isset($this->model->settings['components'])) { - return $result; - } - - $manager = ComponentManager::instance(); - $manager->listComponents(); - - foreach ($this->model->settings['components'] as $name => $properties) { - list($name, $alias) = strpos($name, ' ') ? explode(' ', $name) : [$name, $name]; - - try { - $componentObj = $manager->makeComponent($name, null, $properties); - $componentObj->alias = $alias; - $componentObj->pluginIcon = $manager->findComponentOwnerDetails($componentObj)['icon'] ?? 'icon-puzzle-piece'; - } - catch (Exception $ex) { - $componentObj = new UnknownComponent(null, $properties, $ex->getMessage()); - $componentObj->alias = $alias; - $componentObj->pluginIcon = 'icon-bug'; - } - - $result[] = $componentObj; - } - - return $result; - } - - /** - * getComponentName - */ - protected function getComponentName($component) - { - return ComponentHelpers::getComponentName($component); - } - - /** - * getComponentDescription - */ - protected function getComponentDescription($component) - { - return ComponentHelpers::getComponentDescription($component); - } - - /** - * getComponentsPropertyConfig - */ - protected function getComponentsPropertyConfig($component) - { - return ComponentHelpers::getComponentsPropertyConfig($component); - } - - /** - * getComponentPropertyValues - */ - protected function getComponentPropertyValues($component) - { - return ComponentHelpers::getComponentPropertyValues($component); - } -} diff --git a/formwidgets/MenuItemSearch.php b/formwidgets/MenuItemSearch.php index d1bf49da..e8ec22b6 100644 --- a/formwidgets/MenuItemSearch.php +++ b/formwidgets/MenuItemSearch.php @@ -1,25 +1,18 @@ prepareVars(); - - return $this->makePartial('menuitems'); - } - - /** - * Prepares the list data - */ - public function prepareVars() - { - $menuItem = new MenuItem; - - $this->vars['itemProperties'] = json_encode($menuItem->fillable); - $this->vars['items'] = $this->model->items; - - $emptyItem = new MenuItem; - $emptyItem->title = trans($this->newItemTitle); - $emptyItem->type = 'url'; - $emptyItem->url = '/'; - - $this->vars['emptyItem'] = $emptyItem; - - $widgetConfig = $this->makeConfig('~/plugins/rainlab/pages/classes/menuitem/fields.yaml'); - $widgetConfig->model = $menuItem; - $widgetConfig->alias = $this->alias.'MenuItem'; - - $this->vars['itemFormWidget'] = $this->makeWidget('Backend\Widgets\Form', $widgetConfig); - } - - /** - * {@inheritDoc} - */ - protected function loadAssets() - { - $this->addJs('js/menu-items-editor.js', 'RainLab.Pages'); - } - - /** - * {@inheritDoc} - */ - public function getSaveValue($value) - { - return strlen($value) ? $value : null; - } - - // - // Methods for the internal use - // - - /** - * Returns the item reference description. - * @param \RainLab\Pages\Classes\MenuItem $item Specifies the menu item - * @return string - */ - protected function getReferenceDescription($item) - { - if ($this->typeListCache === false) { - $this->typeListCache = $item->getTypeOptions(); - } - - if (!isset($this->typeInfoCache[$item->type])) { - $this->typeInfoCache[$item->type] = MenuItem::getTypeInfo($item->type); - } - - if (isset($this->typeInfoCache[$item->type])) { - $result = trans( - is_array($this->typeListCache[$item->type]) - ? $this->typeListCache[$item->type][0] - : $this->typeListCache[$item->type] - ); - - if ($item->type !== 'url') { - if (isset($this->typeInfoCache[$item->type]['references'])) { - $result .= ': '.$this->findReferenceName($item->reference, $this->typeInfoCache[$item->type]['references']); - } - } - else { - $result .= ': '.$item->url; - } - - } - else { - $result = trans('rainlab.pages::lang.menuitem.unknown_type'); - } - - return $result; - } - - protected function findReferenceName($search, $typeOptionList) - { - $iterator = function($optionList, $path) use ($search, &$iterator) { - foreach ($optionList as $reference => $info) { - if ($reference == $search) { - $result = $this->getMenuItemTitle($info); - - return strlen($path) ? $path.' / ' .$result : $result; - } - - if (is_array($info) && isset($info['items'])) { - $result = $iterator($info['items'], $path.' / '.$this->getMenuItemTitle($info)); - - if (strlen($result)) { - return strlen($path) ? $path.' / '.$result : $result; - } - } - } - }; - - $result = $iterator($typeOptionList, null); - if (!strlen($result)) { - $result = trans('rainlab.pages::lang.menuitem.unnamed'); - } - - $result = preg_replace('|^\s+\/|', '', $result); - - return $result; - } - - protected function getMenuItemTitle($itemInfo) - { - if (is_array($itemInfo)) { - if (!array_key_exists('title', $itemInfo) || !strlen($itemInfo['title'])) { - return trans('rainlab.pages::lang.menuitem.unnamed'); - } - - return $itemInfo['title']; - } - - return strlen($itemInfo) ? $itemInfo : trans('rainlab.pages::lang.menuitem.unnamed'); - } - - public function makeEditorTemplate() - { - $regEx = '##is'; - $partial = $this->makePartial('editorTemplate'); - - preg_match_all($regEx, $partial, $matches); - - $scripts = implode('', $matches[0]); - $template = preg_replace($regEx, '', $partial); - - return [$template, $scripts]; - } -} diff --git a/formwidgets/MenuPicker.php b/formwidgets/MenuPicker.php index 5ba38f4e..c7d3808a 100644 --- a/formwidgets/MenuPicker.php +++ b/formwidgets/MenuPicker.php @@ -17,17 +17,20 @@ public function render() { $this->prepareVars(); - return $this->makePartial('~/modules/backend/widgets/form/partials/_field_dropdown.htm'); + return $this->makePartial('~/modules/backend/widgets/form/partials/_field_dropdown.php'); } /** - * Prepares the view data + * prepareVars for display */ public function prepareVars() { $this->vars['field'] = $this->makeFormField(); } + /** + * makeFormField as a dropdown listing the theme menus + */ protected function makeFormField(): FormField { $field = clone $this->formField; @@ -37,6 +40,9 @@ protected function makeFormField(): FormField return $field; } + /** + * getOptions for the dropdown + */ protected function getOptions(): array { return Menu::listInTheme(Theme::getEditTheme(), true) diff --git a/formwidgets/PagePicker.php b/formwidgets/PagePicker.php index 5c6e0115..84a754f2 100644 --- a/formwidgets/PagePicker.php +++ b/formwidgets/PagePicker.php @@ -1,17 +1,18 @@ prepareVars(); - return $this->makePartial('~/modules/backend/widgets/form/partials/_field_dropdown.htm'); + + return $this->makePartial('~/modules/backend/widgets/form/partials/_field_dropdown.php'); } /** - * Prepares the view data + * prepareVars for display */ public function prepareVars() { @@ -32,9 +34,9 @@ public function prepareVars() } /** - * @return \Backend\Classes\FormField + * makeFormField as a dropdown listing the page hierarchy */ - protected function makeFormField() + protected function makeFormField(): FormField { $field = clone $this->formField; $field->type = 'dropdown'; @@ -42,15 +44,13 @@ protected function makeFormField() $tree = Page::buildMenuTree(Theme::getEditTheme()); $indent = $field->getConfig('indent', $this->indent); - // Flatten page tree for dropdown options $options = []; $iterator = function($items, $depth = 0) use (&$iterator, &$tree, &$options, $indent) { - foreach ($items as $code) { $itemData = $tree[$code]; $options[$code] = str_repeat($indent, $depth) . $itemData['title']; if (!empty($itemData['items'])) { - $iterator($itemData['items'], $depth+1); + $iterator($itemData['items'], $depth + 1); } } diff --git a/formwidgets/components/partials/_component.htm b/formwidgets/components/partials/_component.htm deleted file mode 100644 index 0707d8ba..00000000 --- a/formwidgets/components/partials/_component.htm +++ /dev/null @@ -1,17 +0,0 @@ -
-
inspectorEnabled): ?>data-inspectable - data-inspector-title="getComponentName($component)) ?>" - data-inspector-description="getComponentDescription($component)) ?>" - data-inspector-config="getComponentsPropertyConfig($component)) ?>" - data-inspector-class=""> - - - alias) ?> - - - - × -
-
\ No newline at end of file diff --git a/formwidgets/components/partials/_formcomponents.htm b/formwidgets/components/partials/_formcomponents.htm deleted file mode 100644 index 0f22cbb2..00000000 --- a/formwidgets/components/partials/_formcomponents.htm +++ /dev/null @@ -1,17 +0,0 @@ -
- - isHidden): ?> - makePartial('component', ['component' => $component]) ?> - - - -
-
- - isHidden): ?> - makePartial('component', ['component' => $component]) ?> - - -
-
-
diff --git a/formwidgets/menuitems/assets/js/menu-items-editor.js b/formwidgets/menuitems/assets/js/menu-items-editor.js deleted file mode 100644 index 7e94ef26..00000000 --- a/formwidgets/menuitems/assets/js/menu-items-editor.js +++ /dev/null @@ -1,651 +0,0 @@ -/* - * The menu item editor. Provides tools for managing the - * menu items. - */ -+function ($) { "use strict"; - var MenuItemsEditor = function (el, options) { - this.$el = $(el) - this.options = options - - this.init() - } - - MenuItemsEditor.prototype.init = function() { - var self = this - - this.alias = this.$el.data('alias') - this.$treeView = this.$el.find('div[data-control="treeview"]') - - this.typeInfo = {} - - // Menu items is clicked - this.$el.on('open.oc.treeview', function(e) { - return self.onItemClick(e.relatedTarget) - }) - - // Submenu item is clicked in the master tabs - this.$el.on('submenu.oc.treeview', $.proxy(this.onSubmenuItemClick, this)) - - this.$el.on('click', 'a[data-control~="add-item"]', function(e) { - self.onCreateItem(e.target) - return false - }) - } - - /* - * Triggered when a submenu item is clicked in the menu editor. - */ - MenuItemsEditor.prototype.onSubmenuItemClick = function(e) { - if ($(e.relatedTarget).data('control') == 'delete-menu-item') - this.onDeleteMenuItem(e.relatedTarget) - - if ($(e.relatedTarget).data('control') == 'create-item') - this.onCreateItem(e.relatedTarget) - - return false - } - - /* - * Removes a menu item - */ - MenuItemsEditor.prototype.onDeleteMenuItem = function(link) { - if (!confirm('Do you really want to delete the menu item? This will also delete the subitems, if any.')) - return - - $(link).trigger('change') - $(link).closest('li[data-menu-item]').remove() - - $(window).trigger('oc.updateUi') - - this.$treeView.treeView('update') - this.$treeView.treeView('fixSubItems') - } - - /* - * Opens the menu item editor - */ - MenuItemsEditor.prototype.onItemClick = function(item, newItemMode) { - var $item = $(item), - $container = $('> div', $item), - self = this - - $container.one('show.oc.popup', function(e) { - self.triggerRenderEvent(); - - self.$popupContainer = $(e.relatedTarget); - self.$itemDataContainer = $container.closest('li') - - $('input[type=checkbox]', self.$popupContainer).removeAttr('checked') - - self.loadProperties(self.$popupContainer, self.$itemDataContainer.data('menu-item')) - self.$popupForm = self.$popupContainer.find('form') - self.itemSaved = false - - var $titleField = $('input[name=title]', self.$popupContainer).focus().select() - var $typeField = $('select[name=type]', self.$popupContainer).change(function(){ - self.loadTypeInfo(false, true) - }) - - $('select[name=reference]', self.$popupContainer).change(function() { - var selectedTitle = $(this).find('option:selected').text(); - // If the saved title is the default new item title, use reference title, - // removing CMS page [base file name] suffix - if (selectedTitle && self.properties.title === self.$popupForm.attr('data-new-item-title')) { - var title = $.trim(selectedTitle.replace(/\s*\[.*\]$/, '')) - $titleField.val(title) - - // Support for RainLab.Translate - var defaultLocale = $('[data-control="multilingual"]').data('default-locale') - if (defaultLocale) { - $('[name="RLTranslate['+defaultLocale+'][title]"]', self.$popupContainer).val(title) - } - } - }) - - self.$popupContainer.on('keydown', function(e) { - if (e.which == 13) - self.applyMenuItem() - }) - - $('button[data-control="apply-btn"]', self.$popupContainer).click($.proxy(self.applyMenuItem, self)) - - var $updateTypeOptionsBtn = $('') - $('div[data-field-name=reference]').addClass('input-sidebar-control').append($updateTypeOptionsBtn) - - $updateTypeOptionsBtn.click(function(){ - self.loadTypeInfo(true) - - return false - }) - - $updateTypeOptionsBtn.keydown(function(ev){ - if (ev.which == 13 || ev.which == 32) { - self.loadTypeInfo(true) - return false - } - }) - - self.$popupContainer.on('change', 'select[name="referenceSearch"]', function() { - var $select = $(this), - val = $select.val(), - parts - - if (!val) return - - // type::reference ID - parts = val.split('::', 2) - - self.referenceSearchOverride = parts[1]; - - $select.empty().trigger('change.select2'); - - $typeField - .val(parts[0]) - .triggerHandler('change') - }) - - var $updateCmsPagesBtn = $updateTypeOptionsBtn.clone(true) - $('div[data-field-name=cmsPage]').addClass('input-sidebar-control').append($updateCmsPagesBtn) - - self.loadTypeInfo() - }) - - $container.one('hide.oc.popup', function(e) { - if (!self.itemSaved && newItemMode) - $item.remove() - - self.$treeView.treeView('update') - self.$treeView.treeView('fixSubItems') - - $container.removeClass('popover-highlight') - }) - - $container.popup({ - content: $('script[data-editor-template]', this.$el).html() - }) - - /* - * Highlight modal target - */ - $container.addClass('popover-highlight') - $container.blur() - - return false - } - - MenuItemsEditor.prototype.loadProperties = function($popupContainer, properties) { - this.properties = properties - - var setPropertyOnElement = function($input, val) { - if ($input.prop('type') == 'checkbox') { - var checked = !(val == '0' || val == 'false' || val == 0 || val == undefined || val == null) - checked ? $input.prop('checked', 'checked') : $input.removeAttr('checked') - } - else if ($input.prop('type') == 'radio') { - $input.filter('[value="'+val+'"]').prop('checked', true) - } - else { - $input.val(val) - $input.change() - } - } - - var defaultLocale = $('[data-control="multilingual"]', $popupContainer).data('default-locale') - $.each(properties, function(property, val) { - if (property == 'viewBag') { - $.each(val, function(vbProperty, vbVal) { - var $input = $('[name="viewBag['+vbProperty+']"]', $popupContainer).not('[type=hidden]') - setPropertyOnElement($input, vbVal) - // Ensure that locale specific data is made available in the RainLab.Translate data holders - if (vbProperty === 'locale') { - $.each(vbVal, function(locale, fields) { - $.each(fields, function(fieldName, fieldValue) { - var $locker = $('[name="RLTranslate['+locale+']['+fieldName+']"]', $popupContainer) - if ($locker) { - $locker.val(fieldValue) - } - }) - }) - } - }) - - /** - * Mediafinder support - */ - var mediafinderElements = $('[data-control="mediafinder"]', $popupContainer); - var storageMediaPath = $('[data-storage-media-path]').data('storage-media-path'); - - $.each(mediafinderElements, function() { - var input = $(this).find('input'), - propertyName = input.attr('name'), - propertyNameSimple; - - if (propertyName && propertyName.length) { - propertyNameSimple = propertyName.substr(8).slice(0, -1); - } - - var propertyValue = ''; - - $.each(val, function(vbProperty, vbVal) { - if (vbProperty == propertyNameSimple) { - propertyValue = vbVal; - } - }); - - if (propertyValue != '') { - // v2 media finder - var dataLocker = $('[data-data-locker]', this); - if (dataLocker.length) { - var items = [{ - path: propertyValue, - publicUrl: storageMediaPath + propertyValue, - thumbUrl: storageMediaPath + propertyValue, - title: propertyValue.substring(1) - }]; - - var mediafinder = $(this).data('oc.mediaFinder') || oc.observeControl(this, 'mediafinder'); - mediafinder.addItems(items); - mediafinder.setValue(); - mediafinder.evalIsPopulated(); - } - // v1 media finder - else { - $(this).toggleClass('is-populated'); - input.attr('value', propertyValue); - - var image = $('[data-find-image]', this); - if (image.length) { - image.attr('src', storageMediaPath + propertyValue); - } - - var file = $('[data-find-file-name]', this); - if (file.length) { - file.text(propertyValue.substring(1)); - } - } - } - }); - - } - else { - var $input = $('[name="'+property+'"]', $popupContainer).not('[type=hidden]') - setPropertyOnElement($input, val) - // If the RainLab.Translate default locale data locker fields are available make sure that they are properly populated - var $defaultLocaleField = $('[name="RLTranslate['+defaultLocale+']['+property+']"]', self.$popupContainer) - if ($defaultLocaleField) { - $defaultLocaleField.val($input.val()); - } - } - }) - } - - MenuItemsEditor.prototype.loadTypeInfo = function(force, focusList) { - var type = $('select[name=type]', this.$popupContainer).val() - - var self = this - - if (!force && this.typeInfo[type] !== undefined) { - self.applyTypeInfo(this.typeInfo[type], type, focusList) - return - } - - $.oc.stripeLoadIndicator.show() - this.$popupForm.request('onGetMenuItemTypeInfo') - .always(function(){ - $.oc.stripeLoadIndicator.hide() - }) - .done(function(data){ - self.typeInfo[type] = data.menuItemTypeInfo - self.applyTypeInfo(data.menuItemTypeInfo, type, focusList) - }) - } - - MenuItemsEditor.prototype.applyTypeInfo = function(typeInfo, type, focusList) { - var $referenceFormGroup = $('div[data-field-name="reference"]', this.$popupContainer), - $optionSelector = $('select', $referenceFormGroup), - $nestingFormGroup = $('div[data-field-name="nesting"]', this.$popupContainer), - $urlFormGroup = $('div[data-field-name="url"]', this.$popupContainer), - $replaceFormGroup = $('div[data-field-name="replace"]', this.$popupContainer), - $cmsPageFormGroup = $('div[data-field-name="cmsPage"]', this.$popupContainer), - $cmsPageSelector = $('select', $cmsPageFormGroup), - prevSelectedReference = $optionSelector.val(), - prevSelectedPage = $cmsPageSelector.val() - - // Search selection - if (this.referenceSearchOverride) { - prevSelectedReference = this.referenceSearchOverride; - this.referenceSearchOverride = null; - } - - if (typeInfo.references) { - $optionSelector.find('option').remove() - $referenceFormGroup.show() - - var iterator = function(options, level, path) { - $.each(options, function(code) { - var $option = $('').attr('value', code), - offset = Array(level*4).join(' '), - isObject = $.type(this) == 'object' - - $option.text(isObject ? this.title : this) - - var optionPath = path.length > 0 - ? (path + ' / ' + $option.text()) - : $option.text() - - $option.data('path', optionPath) - - $option.html(offset + $option.html()) - - $optionSelector.append($option) - - if (isObject) - iterator(this.items, level+1, optionPath) - }) - } - - iterator(typeInfo.references, 0, '') - - $optionSelector - .val(prevSelectedReference ? prevSelectedReference : this.properties.reference) - .triggerHandler('change') - } - else { - $referenceFormGroup.hide() - } - - if (typeInfo.cmsPages) { - $cmsPageSelector.find('option').remove() - $cmsPageFormGroup.show() - - $.each(typeInfo.cmsPages, function(code) { - var $option = $('').attr('value', code) - - $option.text(this).val(code) - $cmsPageSelector.append($option) - }) - - $cmsPageSelector - .val(prevSelectedPage ? prevSelectedPage : this.properties.cmsPage) - .triggerHandler('change') - } - else { - $cmsPageFormGroup.hide() - } - - $nestingFormGroup.toggle(typeInfo.nesting !== undefined && typeInfo.nesting) - $urlFormGroup.toggle(type == 'url') - $replaceFormGroup.toggle(typeInfo.dynamicItems !== undefined && typeInfo.dynamicItems) - - this.triggerRenderEvent(); - - if (focusList) { - var focusElements = [ - $referenceFormGroup, - $cmsPageFormGroup, - $('div.custom-checkbox', $nestingFormGroup), - $('div.custom-checkbox', $replaceFormGroup), - $('input', $urlFormGroup) - ] - - $.each(focusElements, function(){ - if (this.is(':visible')) { - var $self = this - - window.setTimeout(function() { - if ($self.hasClass('dropdown-field')) - $('select', $self).select2('focus', 100) - else $self.focus() - }) - - return false; - } - }) - } - } - - MenuItemsEditor.prototype.applyMenuItem = function() { - var self = this, - data = {}, - propertyNames = this.$el.data('item-properties'), - basicProperties = { - 'title': 1, - 'type': 1, - 'code': 1 - }, - typeInfoPropertyMap = { - reference: 'references', - replace: 'dynamicItems', - cmsPage: 'cmsPages' - }, - typeInfo = {}, - validationErrorFound = false - - // Ensure that locale specific data is made available in the RainLab.Translate data holders - $('[name^="viewBag[locale]"]', self.$popupContainer).each(function() { - var locale = $(this).data('locale') - var fieldName = $(this).data('field-name') - var $localeField = $('[name="RLTranslate['+locale+']['+fieldName+']"]', self.$popupContainer) - $(this).val($localeField.val()) - }); - - var defaultLocale = $('[data-control="multilingual"]').data('default-locale') - - $.each(propertyNames, function() { - var propertyName = this, - $input = $('[name="'+propertyName+'"]', self.$popupContainer).not('[type=hidden]') - - // If the RainLab.Translate default locale data locker fields are available make sure the regular inputs are properly populated - if (defaultLocale) { - var $defaultLocaleField = $('[name="RLTranslate['+defaultLocale+']['+propertyName+']"]', self.$popupContainer) - if ($defaultLocaleField && $defaultLocaleField.val()) { - $input.val($defaultLocaleField.val()) - } - } - - if ($input.prop('type') !== 'checkbox') { - data[propertyName] = $.trim($input.val()) - - if (propertyName == 'type') - typeInfo = self.typeInfo[data.type] - - if (data[propertyName].length == 0) { - var typeInfoProperty = typeInfoPropertyMap[propertyName] !== undefined ? typeInfoPropertyMap[propertyName] : propertyName - - if (typeInfo[typeInfoProperty] !== undefined) { - - $.oc.flashMsg({ - class: 'error', - text: self.$popupForm.attr('data-message-'+propertyName+'-required') - }) - - if ($input.prop("tagName") == 'SELECT') - $input.select2('focus') - else - $input.focus() - - validationErrorFound = true - - return false - } - } - } - else { - data[propertyName] = $input.prop('checked') ? 1 : 0 - } - }) - - if (validationErrorFound) - return - - if (data.type !== 'url') { - delete data['url'] - - $.each(data, function(property) { - if (property == 'type') - return - - var typeInfoProperty = typeInfoPropertyMap[property] !== undefined ? typeInfoPropertyMap[property] : property - if ((typeInfo[typeInfoProperty] === undefined || typeInfo[typeInfoProperty] === false) - && basicProperties[property] === undefined) - delete data[property] - }) - } - else { - $.each(propertyNames, function(){ - if (this != 'url' && basicProperties[this] === undefined) - delete data[this] - }) - } - - if ($.trim(data.title).length == 0) { - $.oc.flashMsg({ - class: 'error', - text: self.$popupForm.data('messageTitleRequired') - }) - - $('[name=title]', self.$popupContainer).focus() - - return - } - - if (data.type == 'url' && $.trim(data.url).length == 0) { - $.oc.flashMsg({ - class: 'error', - text: self.$popupForm.data('messageUrlRequired') - }) - - $('[name=url]', self.$popupContainer).focus() - - return - } - - $('> div span.title', self.$itemDataContainer).text(data.title) - - var referenceDescription = $.trim($('select[name=type] option:selected', self.$popupContainer).text()) - - if (data.type == 'url') { - referenceDescription += ': ' + $('input[name=url]', self.$popupContainer).val() - } - else if (typeInfo.references) { - referenceDescription += ': ' + $.trim($('select[name=reference] option:selected', self.$popupContainer).data('path')) - } - - $('> div span.comment', self.$itemDataContainer).text(referenceDescription) - - this.attachViewBagData(data) - - this.$itemDataContainer.data('menu-item', data) - this.itemSaved = true - this.$popupContainer.trigger('close.oc.popup') - this.$el.trigger('change') - } - - MenuItemsEditor.prototype.attachViewBagData = function(data) { - var fields = this.$popupForm.serializeArray(), - fieldName, - fieldValue - - $.each(fields, function(index, field) { - fieldName = field.name - fieldValue = field.value - - if (fieldName.indexOf('viewBag[') != 0) { - return true // Continue - } - - /* - * Break field name in to elements - */ - var elements = [], - searchResult, - expression = /([^\]\[]+)/g - - while ((searchResult = expression.exec(fieldName))) { - elements.push(searchResult[0]) - } - - /* - * Attach elements to data with value - */ - var currentData = data, - elementsNum = elements.length, - lastIndex = elementsNum - 1, - currentProperty - - for (var i = 0; i < elementsNum; ++i) { - currentProperty = elements[i] - - if (i === lastIndex) { - currentData[currentProperty] = fieldValue - } - else if (currentData[currentProperty] === undefined) { - currentData[currentProperty] = {} - } - - currentData = currentData[currentProperty] - } - }) - } - - MenuItemsEditor.prototype.onCreateItem = function(target) { - var parentList = $(target).closest('li[data-menu-item]').find(' > ol'), - item = $($('script[data-item-template]', this.$el).html()) - - if (!parentList.length) - parentList = $(target).closest('div[data-control=treeview]').find(' > ol') - - parentList.append(item) - this.$treeView.treeView('update') - $(window).trigger('oc.updateUi') - - this.onItemClick(item, true) - } - - MenuItemsEditor.prototype.triggerRenderEvent = function() { - // Vanilla AJAX Framework (v3) - if (window.oc && oc.Events) { - oc.Events.dispatch('render'); - } - // Classic AJAX Framework (v1,2) - else { - $(document).trigger('render'); - } - } - - MenuItemsEditor.DEFAULTS = { - } - - // MENUITEMSEDITOR PLUGIN DEFINITION - // ============================ - - var old = $.fn.menuItemsEditor - - $.fn.menuItemsEditor = function (option) { - var args = Array.prototype.slice.call(arguments, 1) - return this.each(function () { - var $this = $(this) - var data = $this.data('oc.menuitemseditor') - var options = $.extend({}, MenuItemsEditor.DEFAULTS, $this.data(), typeof option == 'object' && option) - if (!data) $this.data('oc.menuitemseditor', (data = new MenuItemsEditor(this, options))) - else if (typeof option == 'string') data[option].apply(data, args) - }) - } - - $.fn.menuItemsEditor.Constructor = MenuItemsEditor - - // MENUITEMSEDITOR NO CONFLICT - // ================= - - $.fn.menuItemsEditor.noConflict = function () { - $.fn.menuItemsEditor = old - return this - } - - // MENUITEMSEDITOR DATA-API - // =============== - - $(document).on('render', function() { - $('[data-control="menu-item-editor"]').menuItemsEditor() - }); -}(window.jQuery); diff --git a/formwidgets/menuitems/partials/_editortemplate.htm b/formwidgets/menuitems/partials/_editortemplate.htm deleted file mode 100644 index fc49f10d..00000000 --- a/formwidgets/menuitems/partials/_editortemplate.htm +++ /dev/null @@ -1,29 +0,0 @@ - - - \ No newline at end of file diff --git a/formwidgets/menuitems/partials/_item.htm b/formwidgets/menuitems/partials/_item.htm deleted file mode 100644 index 2c320a85..00000000 --- a/formwidgets/menuitems/partials/_item.htm +++ /dev/null @@ -1,38 +0,0 @@ - -
  • -
    - - title) ?> - getReferenceDescription($item)) ?> - - - -
    - -
      - items): ?> - makePartial('itemlist', ['items' => $subItems]) ?> - -
    -
  • \ No newline at end of file diff --git a/formwidgets/menuitems/partials/_itemlist.htm b/formwidgets/menuitems/partials/_itemlist.htm deleted file mode 100644 index 8a061b8e..00000000 --- a/formwidgets/menuitems/partials/_itemlist.htm +++ /dev/null @@ -1,5 +0,0 @@ - - - makePartial('item', ['item' => $item]) ?> - - \ No newline at end of file diff --git a/formwidgets/menuitems/partials/_items.htm b/formwidgets/menuitems/partials/_items.htm deleted file mode 100644 index 8f1d805b..00000000 --- a/formwidgets/menuitems/partials/_items.htm +++ /dev/null @@ -1,11 +0,0 @@ -
      - makePartial('itemlist', ['items' => $items]) ?> -
    - - - - \ No newline at end of file diff --git a/formwidgets/menuitems/partials/_menuitems.htm b/formwidgets/menuitems/partials/_menuitems.htm deleted file mode 100644 index e9b5c7bd..00000000 --- a/formwidgets/menuitems/partials/_menuitems.htm +++ /dev/null @@ -1,36 +0,0 @@ -makeEditorTemplate(); -?> -
    -
    -
    - makePartial('items', ['items' => $items]) ?> -
    -
    - - - - - - -
    diff --git a/lang/cs.json b/lang/cs.json new file mode 100644 index 00000000..a7023a52 --- /dev/null +++ b/lang/cs.json @@ -0,0 +1,83 @@ +{ + "Pages": "Stránky", + "Pages & menus features.": "Funkce pro správu stránek a menu.", + "Manage static pages": "Spravovat stránky", + "Manage static menus": "Spravovat menu", + "Manage static content": "Spravovat obsah", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Neplatný formát URL. URL by mělo začínat lomítkem a může obsahovat čísla, písmena a znaky: _-/", + "This URL is already used by another page.": "Toto URL je používáno jinou stránkou.", + "Layouts not found": "Layouty nenalezeny", + "The Code is required": "Pole kód je povinné", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Pole kód obsahuje neplatné znaky. Může obsahovat pouze číslice, písmena a znaky: _-", + "Static page": "Statická stránka", + "All static pages": "Všechny statické stránky", + "Static menu": "Statické menu", + "Static breadcrumbs": "Statická drobečková navigace", + "Child pages": "Podstránky", + "Outputs a static page in a CMS layout.": "Vygeneruje statickou stránku v CMS layoutu.", + "Use page content field": "Použít pole obsahu stránky", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Pokud není zaškrtnuto, sekce obsahu se při úpravě statické stránky nezobrazí. Obsah stránky bude určen výhradně pomocí zástupných symbolů a proměnných.", + "Default layout": "Výchozí layout", + "Defines this layout as the default for new pages": "Definuje tento layout jako výchozí pro nové stránky", + "Subpage layout": "Layout podstránek", + "The layout to use as the default for any new subpages": "Layout, který se použije jako výchozí pro všechny nové podstránky", + "Outputs a menu in a CMS layout.": "Vygeneruje menu v CMS layoutu.", + "Menu": "Menu", + "Specify a code of the menu the component should output.": "Zadejte kód menu, které má komponenta vygenerovat.", + "Outputs breadcrumbs for a static page.": "Vygeneruje drobečkovou navigaci pro statickou stránku.", + "Displays a list of child pages for the current page": "Zobrazí seznam podstránek aktuální stránky", + "Static Pages": "Statické stránky", + "Menus": "Menu", + "Content": "Obsah", + "Add": "Přidat", + "Refresh": "Obnovit", + "Page": "Stránka", + "Content block": "Blok obsahu", + "New page": "Nová stránka", + "New page title": "Nový titulek stránky", + "New menu": "Nové menu", + "New content block": "Nový blok obsahu", + "Title": "Titulek", + "URL": "URL", + "File Name": "Název souboru", + "Layout": "Layout", + "Hidden": "Skrytá", + "Hide in navigation": "Skrýt v menu", + "Description": "Popis", + "Name": "Název", + "Code": "Kód", + "The Title is required.": "Titulek je povinný.", + "The URL is required.": "URL je povinné.", + "The File Name is required.": "Název souboru je povinný.", + "The Name is required.": "Název je povinný.", + "The Code is required.": "Pole kód je povinné.", + "Error loading page": "Chyba při načítání stránky", + "Error loading menu": "Chyba při načítání menu", + "Error loading content block": "Chyba při načítání bloku obsahu", + "Add item": "Přidat položku", + "Add subitem": "Přidat podpoložku", + "New menu item": "Nová položka menu", + "Untitled": "Bez názvu", + "Up": "Nahoru", + "Down": "Dolů", + "Indent": "Odsadit", + "Outdent": "Zmenšit odsazení", + "Delete": "Odstranit", + "No menu items yet. Use \"Add item\" in the toolbar.": "Zatím žádné položky menu. Použijte \"Přidat položku\" v panelu nástrojů.", + "Select a menu item to edit, or add a new one.": "Vyberte položku menu k úpravě, nebo přidejte novou.", + "Custom Fields": "Vlastní pole", + "Search all references...": "Prohledat odkazy...", + "Edit Menu Item": "Upravit položku menu", + "Move up": "Posunout nahoru", + "Move down": "Posunout dolů", + "Apply": "Použít", + "Cancel": "Zrušit", + "New Page": "Nová stránka", + "New Menu": "Nové menu", + "New Content Block": "Nový blok obsahu", + "Fields": "Pole", + "Preview": "Náhled", + "Add subpage": "Přidat podstránku", + "You don't have permissions to manage :type documents.": "Nemáte oprávnění spravovat dokumenty typu :type.", + "Content files cannot be saved in the static pages directory.": "Soubory obsahu nelze ukládat do složky statických stránek." +} diff --git a/lang/cs/lang.php b/lang/cs/lang.php deleted file mode 100644 index 7eca2457..00000000 --- a/lang/cs/lang.php +++ /dev/null @@ -1,105 +0,0 @@ - [ - 'name' => 'Stránky', - 'description' => 'Funkce pro správu stránek a menu.', - ], - 'page' => [ - 'menu_label' => 'Stránky', - 'template_title' => '%s Stránky', - 'delete_confirmation' => 'Opravdu chcete odstranit vybrané stránky? Budou odstraněny i případné podstránky.', - 'no_records' => 'Stránky nenalezeny', - 'delete_confirm_single' => 'Opravu chcete odstranit tuto stránku? Budou odstraněny i případné podstránky.', - 'new' => 'Nová stránka', - 'add_subpage' => 'Přidat podstránku', - 'invalid_url' => 'Neplatný formát URL. URL by mělo začínat lomítkem a může obsahovat čísla, písmena a znaky: _-/', - 'url_not_unique' => 'Toto URL je používáno jinou stránkou.', - 'layout' => 'Layouty', - 'layouts_not_found' => 'Layouts nenalezeny', - 'saved' => 'Stránka byla úspěšně uložena.', - 'tab' => 'Stránky', - 'manage_pages' => 'Spravovat stránky', - 'manage_menus' => 'Spravovat menu', - 'access_snippets' => 'Používat snippety', - 'manage_content' => 'Spravovat obsah', - ], - 'menu' => [ - 'menu_label' => 'Menu', - 'delete_confirmation' => 'Opravdu chcete odstranit vybraná menu?', - 'no_records' => 'Položky nenalezeny', - 'new' => 'Nové menu', - 'new_name' => 'Nové menu', - 'new_code' => 'nove-menu', - 'delete_confirm_single' => 'Opravdu chcete odstranit toto menu?', - 'saved' => 'Menu bylo úspěšně uloženo.', - 'name' => 'Název', - 'code' => 'Kód', - 'items' => 'Položky menu', - 'add_subitem' => 'Přidat položku', - 'code_required' => 'Pole kód je povinné.', - 'invalid_code' => 'Pole kód obsahuje neplatné znaky. Může obsahovat pouze číslice, písmena a znaky: _-', - ], - 'menuitem' => [ - 'title' => 'Titulek', - 'editor_title' => 'Upravit položku menu', - 'type' => 'Typ', - 'allow_nested_items' => 'Povolit vnořené položky', - 'allow_nested_items_comment' => 'Vnořené položky mohou být automaticky vygenerovány statickými stránkami nebo jinými typy stránek.', - 'url' => 'URL', - 'reference' => 'Odkaz', - 'search_placeholder' => 'Prohledat odkazy...', - 'title_required' => 'Titulek je povinný', - 'unknown_type' => 'Neznámý typ položky', - 'unnamed' => 'Nepojmenovaný typ položky', - 'add_item' => 'Přidat položku', - 'new_item' => 'Nová položka menu', - 'replace' => 'Nahradit tuto položku jejími vygenerovanými vnořenými položkami', - 'replace_comment' => 'Toto pole zaškrtněte, pokud si přejete vnořené položky posunout na stejnou úroveň jako má tato položka. Samotná položka zůstane skryta.', - 'cms_page' => 'CMS stránka', - 'cms_page_comment' => 'Vyberte stránku, která se otevře při kliknutí na tuto položku v menu.', - 'reference_required' => 'Odkaz je povinný.', - 'url_required' => 'URL je povinné', - 'cms_page_required' => 'Prosím vyberte CMS stránku', - 'display_tab' => 'Zobrazení', - 'hidden' => 'Skrytá', - 'hidden_comment' => 'Skrýt tuto položku menu pro celý front-end.', - 'attributes_tab' => 'Vlastnosti', - 'code' => 'Kód', - 'code_comment' => 'Zadejte kód položky menu pokud k ní chcete přistupovat přes API.', - 'css_class' => 'CSS třída', - 'css_class_comment' => 'Vložte název CSS třídy k zajištění specifického vzhledu této položky menu.', - 'external_link' => 'Externí odkaz', - 'external_link_comment' => 'Otevřít odkaz této položky v novém okně.', - 'static_page' => 'Statická stránka', - 'all_static_pages' => 'Všechny statické stránky', - ], - 'content' => [ - 'menu_label' => 'Obsah', - 'cant_save_to_dir' => 'Ukládat obsah do složky statických stránek není povoleno.', - ], - 'sidebar' => [ - 'add' => 'Přidat', - ], - 'object' => [ - 'invalid_type' => 'Neznámý typ objektu', - 'not_found' => 'Požadovaný objekt nebyl nalezen.', - ], - 'editor' => [ - 'title' => 'Titulek', - 'new_title' => 'Nový titulek stránky', - 'content' => 'Obsah', - 'url' => 'URL', - 'filename' => 'Název souboru', - 'layout' => 'Layout', - 'description' => 'Popis', - 'preview' => 'Náhled', - 'enter_fullscreen' => 'Vstoupit do režimu celé obrazovky', - 'exit_fullscreen' => 'Opustit režim celé obrazovky', - 'hidden' => 'Skrytá', - 'hidden_comment' => 'Skryté stránky jsou dostupné pouze přihlášeným administrátorům.', - 'navigation_hidden' => 'Skrýt v menu', - 'navigation_hidden_comment' => 'Zaškrtněte toto pole, pokud chcete stránku skrýt z automaticky vygenerovaných menu a drobečkové navigace.', - ], - 'snippet' => [ - 'menu_label' => 'Snippety', - ], -]; diff --git a/lang/de.json b/lang/de.json new file mode 100644 index 00000000..057ee520 --- /dev/null +++ b/lang/de.json @@ -0,0 +1,83 @@ +{ + "Pages": "Seiten", + "Pages & menus features.": "Funktionen für Seiten und Menüs.", + "Manage static pages": "Verwalte statische Seiten", + "Manage static menus": "Verwalte statische Menüs", + "Manage static content": "Verwalte den Inhalt", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Ungültiges URL Format. Die URL sollte mit einem Schrägstrich (Slash) starten und darf Ziffern, Buchstaben und folgenden Symbole enthalten: _-/.", + "This URL is already used by another page.": "Die URL wird schon von einer anderen Seite benutzt.", + "Layouts not found": "Keine Layouts gefunden", + "The Code is required": "Ein Code ist erforderlich", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Ungültiges Code Format. Der Code darf Ziffern, Buchstaben und folgende Symbole enthalten: _-", + "Static page": "Statische Seite", + "All static pages": "Alle statischen Seiten", + "Static menu": "Statisches Menü", + "Static breadcrumbs": "Statische Breadcrumbs", + "Child pages": "Unterseiten", + "Outputs a static page in a CMS layout.": "Gibt eine statische Seite in einem CMS-Layout aus.", + "Use page content field": "Inhaltsfeld der Seite verwenden", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Wenn nicht ausgewählt, wird der Inhaltsbereich beim Bearbeiten der statischen Seite nicht angezeigt. Der Seiteninhalt wird ausschließlich über Platzhalter und Variablen bestimmt.", + "Default layout": "Standard-Layout", + "Defines this layout as the default for new pages": "Definiert dieses Layout als Standard für neue Seiten", + "Subpage layout": "Unterseiten-Layout", + "The layout to use as the default for any new subpages": "Das Layout, das als Standard für alle neuen Unterseiten verwendet wird", + "Outputs a menu in a CMS layout.": "Gibt ein Menü in einem CMS-Layout aus.", + "Menu": "Menü", + "Specify a code of the menu the component should output.": "Geben Sie den Code des Menüs an, das die Komponente ausgeben soll.", + "Outputs breadcrumbs for a static page.": "Gibt Breadcrumbs für eine statische Seite aus.", + "Displays a list of child pages for the current page": "Zeigt eine Liste der Unterseiten der aktuellen Seite an", + "Static Pages": "Statische Seiten", + "Menus": "Menüs", + "Content": "Inhalte", + "Add": "Neu", + "Refresh": "Aktualisieren", + "Page": "Seite", + "Content block": "Inhaltsblock", + "New page": "Neue Seite", + "New page title": "Titel für die neue Seite", + "New menu": "Neues Menü", + "New content block": "Neuer Inhaltsblock", + "Title": "Titel", + "URL": "URL", + "File Name": "Dateiname", + "Layout": "Layout", + "Hidden": "Versteckt", + "Hide in navigation": "In der Navigation verstecken", + "Description": "Beschreibung", + "Name": "Name", + "Code": "Code", + "The Title is required.": "Ein Titel ist erforderlich.", + "The URL is required.": "Eine URL ist erforderlich.", + "The File Name is required.": "Ein Dateiname ist erforderlich.", + "The Name is required.": "Ein Name ist erforderlich.", + "The Code is required.": "Ein Code ist erforderlich.", + "Error loading page": "Fehler beim Laden der Seite", + "Error loading menu": "Fehler beim Laden des Menüs", + "Error loading content block": "Fehler beim Laden des Inhaltsblocks", + "Add item": "Menüpunkt hinzufügen", + "Add subitem": "Unterpunkt hinzufügen", + "New menu item": "Neuer Menüpunkt", + "Untitled": "Unbenannt", + "Up": "Nach oben", + "Down": "Nach unten", + "Indent": "Einrücken", + "Outdent": "Ausrücken", + "Delete": "Löschen", + "No menu items yet. Use \"Add item\" in the toolbar.": "Noch keine Menüpunkte vorhanden. Verwenden Sie \"Menüpunkt hinzufügen\" in der Werkzeugleiste.", + "Select a menu item to edit, or add a new one.": "Wählen Sie einen Menüpunkt zum Bearbeiten aus oder fügen Sie einen neuen hinzu.", + "Custom Fields": "Benutzerdefinierte Felder", + "Search all references...": "Alle Referenzen durchsuchen...", + "Edit Menu Item": "Menüpunkt bearbeiten", + "Move up": "Nach oben verschieben", + "Move down": "Nach unten verschieben", + "Apply": "Übernehmen", + "Cancel": "Abbrechen", + "New Page": "Neue Seite", + "New Menu": "Neues Menü", + "New Content Block": "Neuer Inhaltsblock", + "Fields": "Felder", + "Preview": "Vorschau", + "Add subpage": "Neue Unterseite", + "You don't have permissions to manage :type documents.": "Sie haben keine Berechtigung, :type Dokumente zu verwalten.", + "Content files cannot be saved in the static pages directory.": "Das Speichern von Inhaltsdateien in den Statische-Seiten Ordner ist nicht erlaubt." +} diff --git a/lang/de/lang.php b/lang/de/lang.php deleted file mode 100644 index 918ee487..00000000 --- a/lang/de/lang.php +++ /dev/null @@ -1,93 +0,0 @@ - [ - 'name' => 'Seiten', - 'description' => 'Pages & menus features.', - ], - 'page' => [ - 'menu_label' => 'Seiten', - 'template_title' => '%s Seiten', - 'delete_confirmation' => 'Möchten Sie die ausgewählten Seiten wirklich löschen? Dadurch werden auch mögliche Unterseiten gelöscht.', - 'no_records' => 'Keine Seiten gefunden', - 'delete_confirm_single' => 'Möchten Sie die ausgewählte Seite wirklich löschen? Dadurch werden auch mögliche Unterseiten gelöscht.', - 'new' => 'Neue Seite', - 'add_subpage' => 'Neue Unterseite', - 'invalid_url' => 'Ungültiges URL Format. Die URL sollte mit einem Schrägstrich (Slash) starten und darf Ziffern, Buchstaben und folgenden Symbole enthalten: _-/.', - 'url_not_unique' => 'Die URL wird schon von einer anderen Seite benutzt.', - 'layout' => 'Layout', - 'layouts_not_found' => 'Keine Layouts gefunden', - 'saved' => 'Die Seite wurde erfolgreich gespeichert.', - 'manage_pages' => 'Verwalte statische Seiten', - 'manage_menus' => 'Verwalte statische Menüs', - 'access_snippets' => 'Verwalte Snippets', - 'manage_content' => 'Verwalte den Inhalt', - ], - 'menu' => [ - 'menu_label' => 'Menüs', - 'delete_confirmation' => 'Möchten Sie die ausgewählten Menüs wirklich löschen?', - 'no_records' => 'Keine Menüpunkte gefunden', - 'new' => 'Neues Menü', - 'new_name' => 'Menüname', - 'new_code' => 'menuename', - 'delete_confirm_single' => 'Möchten Sie das ausgewählte Menü wirklich löschen?', - 'saved' => 'Das Menü wurde erfolgreich gespeichert.', - 'name' => 'Name', - 'code' => 'Code', - 'items' => 'Menüpunkte', - 'add_subitem' => 'Neuer Menüpunkt', - 'code_required' => 'Ein Code ist erforderlich', - 'invalid_code' => 'Ungültiges Code Format. Der Code darf Ziffern, Buchstaben und folgenden Symbole enthalten: _-/', - ], - 'menuitem' => [ - 'title' => 'Titel', - 'editor_title' => 'Menüpunkt bearbeiten', - 'type' => 'Typ', - 'allow_nested_items' => 'Erlaube verschachtelte Menüpunkte', - 'allow_nested_items_comment' => 'Verschachtelte Menüpunkte können dynamisch durch statische Seiten und einigen anderen Menüpunkt-Typen erzeugt werden.', - 'url' => 'URL', - 'reference' => 'Referenz', - 'title_required' => 'Ein Titel ist erforderlich', - 'unknown_type' => 'Unbekannter Menüpunkt-Typ', - 'unnamed' => 'Unbekannter Menüpunkt', - 'add_item' => 'Neuer Menüpunkt', - 'new_item' => 'Neuer Menüpunkt', - 'replace' => 'Ersetze diesen Menüpunkt mit seinen Unterpunkten', - 'replace_comment' => 'Verwenden Sie diese Option, um erzeugte Menüpunkte auf die gleiche Ebene von diesem zu bringen. Dieser Menüpunkt selbst wird ausgeblendet.', - 'cms_page' => 'CMS Seite', - 'cms_page_comment' => 'Wählen Sie eine Seite die geöffnet werden soll, wenn dieser Menüpunkt angeklickt wird.', - 'reference_required' => 'Eine Menüpunkt-Referenz ist erforderlich', - 'url_required' => 'Eine URL ist erforderlich', - 'cms_page_required' => 'Bitten wählen Sie eine CMS Seite', - 'code' => 'Code', - 'code_comment' => 'Geben Sie einen Menüpunkt-Code ein, wenn Sie diesen mit der API ansprechen möchten.', - ], - 'content' => [ - 'menu_label' => 'Inhalte', - 'cant_save_to_dir' => 'Das Speichern von Inhaltsdateien in den Statische-Seiten Ordner ist nicht erlaubt.', - ], - 'sidebar' => [ - 'add' => 'Neu', - ], - 'object' => [ - 'invalid_type' => 'Unbekannter Objekttyp', - 'not_found' => 'Das angeforderte Objekt wurde nicht gefunden.', - ], - 'editor' => [ - 'title' => 'Titel', - 'new_title' => 'Titel für die neue Seite', - 'content' => 'Inhalt', - 'url' => 'URL', - 'filename' => 'Dateiname', - 'layout' => 'Layout', - 'description' => 'Beschreibung', - 'preview' => 'Vorschau', - 'enter_fullscreen' => 'Vollbildmodus einschalten', - 'exit_fullscreen' => 'Vollbildmodus verlassen', - 'hidden' => 'Verstecken', - 'hidden_comment' => 'Versteckte Seiten sind nur für eingeloggte administrations Benutzer zugänglich.', - 'navigation_hidden' => 'In der Navigation verstecken', - 'navigation_hidden_comment' => 'Setzen Sie diese Option, um diese Seite von automatisch generierten Menüs und Breadcrumbs zu verstecken.', - ], - 'snippet' => [ - 'menu_label' => 'Snippets', - ], -]; diff --git a/lang/el.json b/lang/el.json new file mode 100644 index 00000000..b0af64c7 --- /dev/null +++ b/lang/el.json @@ -0,0 +1,83 @@ +{ + "Pages": "Σελίδες", + "Pages & menus features.": "Σελίδες και Μενού.", + "Manage static pages": "Διαχείριση στατικών σελίδων", + "Manage static menus": "Διαχείριση στατικών μενού", + "Manage static content": "Διαχείριση στατικού περιεχομένου", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Μή έγκυρη μορφή URL. Το URL πρέπει να αρχίζει με το σύμβολο μπροστινής καθέτου και μπορεί να περιέχει αριθμητικά ψηφία, Λατινικούς χαρακτήρες και τα ακόλουθα σύμβολα: _-/.", + "This URL is already used by another page.": "Αυτό το URL χρησιμοποιήται ήδη από άλλη σελίδα.", + "Layouts not found": "Δεν βρέθηκαν σχέδια", + "The Code is required": "Ο Κωδικός είναι απαραίτητος", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Μή έγκυρη μορφή Κωδικού. Ο Κωδικός μπορεί να περιέχει αριθμητικά ψηφία, Λατινικούς χαρακτήρες και τα ακόλουθα σύμβολα: _-", + "Static page": "Στατική σελίδα", + "All static pages": "Όλες οι Στατικές Σελίδες", + "Static menu": "Στατικό μενού", + "Static breadcrumbs": "Στατικά breadcrumbs", + "Child pages": "Υποσελίδες", + "Outputs a static page in a CMS layout.": "Παράγει μία στατική σελίδα ενσωματωμένη σε ένα σχέδιο CMS.", + "Use page content field": "Χρησιμοποιήστε το πεδίο περιεχομένου σελίδας", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Εάν δεν είναι επιλεγμένο, η περιοχή περιεχομένου δεν θα εμφανίζεται όταν επεξεργάζεται η στατική σελίδα. Το περιεχόμενο της σελίδας θα ορίζεται αποκλειστικά μέσω αντικαταστατών και μεταβλητών.", + "Default layout": "Προεπιλεγμένο σχέδιο", + "Defines this layout as the default for new pages": "Ορίζει αυτό το σχέδιο σαν το προεπιλεγμένο για καινούργιες σελίδες", + "Subpage layout": "Σχέδιο υποσελίδων", + "The layout to use as the default for any new subpages": "Το προεπιλεγμένο σχέδιο για καινούργιες υποσελίδες", + "Outputs a menu in a CMS layout.": "Παράγει ένα μενού σε ένα σχέδιο CMS.", + "Menu": "Μενού", + "Specify a code of the menu the component should output.": "Ορίστε ένα κωδικό του μενού που το Δομικό Στοιχείο θα πρέπει να εξάγει.", + "Outputs breadcrumbs for a static page.": "Παράγει breadcrumbs για μία στατική σελίδα.", + "Displays a list of child pages for the current page": "Εμφανίζει μια λίστα υποσελίδων για την τρέχουσα σελίδα", + "Static Pages": "Στατικές Σελίδες", + "Menus": "Μενού", + "Content": "Περιεχόμενο", + "Add": "Προσθήκη", + "Refresh": "Ανανέωση", + "Page": "Σελίδα", + "Content block": "Μπλοκ περιεχομένου", + "New page": "Νέα σελίδα", + "New page title": "Νέος τίτλος σελίδας", + "New menu": "Νέο μενού", + "New content block": "Νέο μπλοκ περιεχομένου", + "Title": "Τίτλος", + "URL": "URL", + "File Name": "Όνομα αρχείου", + "Layout": "Σχέδιο", + "Hidden": "Κρυφό", + "Hide in navigation": "Απόκρυψη στην πλοήγηση", + "Description": "Περιγραφή", + "Name": "Όνομα", + "Code": "Κωδικός", + "The Title is required.": "Ο Τίτλος είναι απαραίτητος.", + "The URL is required.": "Το URL είναι απαραίτητο.", + "The File Name is required.": "Το Όνομα αρχείου είναι απαραίτητο.", + "The Name is required.": "Το Όνομα είναι απαραίτητο.", + "The Code is required.": "Ο Κωδικός είναι απαραίτητος.", + "Error loading page": "Σφάλμα κατά τη φόρτωση της σελίδας", + "Error loading menu": "Σφάλμα κατά τη φόρτωση του μενού", + "Error loading content block": "Σφάλμα κατά τη φόρτωση του μπλοκ περιεχομένου", + "Add item": "Προσθήκη στοιχείου", + "Add subitem": "Προσθήκη υποστοιχείου", + "New menu item": "Νέο στοιχείο μενού", + "Untitled": "Χωρίς τίτλο", + "Up": "Πάνω", + "Down": "Κάτω", + "Indent": "Αύξηση εσοχής", + "Outdent": "Μείωση εσοχής", + "Delete": "Διαγραφή", + "No menu items yet. Use \"Add item\" in the toolbar.": "Δεν υπάρχουν στοιχεία μενού ακόμα. Χρησιμοποιήστε την \"Προσθήκη στοιχείου\" στη γραμμή εργαλείων.", + "Select a menu item to edit, or add a new one.": "Επιλέξτε ένα στοιχείο μενού για επεξεργασία ή προσθέστε ένα νέο.", + "Custom Fields": "Προσαρμοσμένα Πεδία", + "Search all references...": "Αναζήτηση αναφορών...", + "Edit Menu Item": "Επεξεργασία στοιχείου μενού", + "Move up": "Μετακίνηση πάνω", + "Move down": "Μετακίνηση κάτω", + "Apply": "Εφαρμογή", + "Cancel": "Ακύρωση", + "New Page": "Νέα σελίδα", + "New Menu": "Νέο μενού", + "New Content Block": "Νέο μπλοκ περιεχομένου", + "Fields": "Πεδία", + "Preview": "Προεπισκόπηση", + "Add subpage": "Προσθήκη υποσελίδας", + "You don't have permissions to manage :type documents.": "Δεν έχετε δικαιώματα διαχείρισης εγγράφων :type.", + "Content files cannot be saved in the static pages directory.": "Η αποθήκευση των αρχείων περιεχομένου στο φάκελο στατικών σελίδων δεν επιτρέπεται." +} diff --git a/lang/el/lang.php b/lang/el/lang.php deleted file mode 100644 index d266b40d..00000000 --- a/lang/el/lang.php +++ /dev/null @@ -1,113 +0,0 @@ - [ - 'name' => 'Σελίδες', - 'description' => 'Σελίδες και Μενού.', - ], - 'page' => [ - 'menu_label' => 'Σελίδες', - 'template_title' => '%s Σελίδες', - 'delete_confirmation' => 'Θέλετε πραγματικά να διαγράψετε τις επιλεγμένες σελίδες; Αυτό θα διαγράψει και τις υποσελίδες, εάν υπάρχουν.', - 'no_records' => 'Δεν βρέθηκαν σελίδες', - 'delete_confirm_single' => 'Θέλετε πραγματικά να διαγράψετε αυτή την σελίδα; Αυτό θα διαγράψει και τις υποσελίδες, εάν υπάρχουν.', - 'new' => 'Νέα σελίδα', - 'add_subpage' => 'Προσθήκη υποσελίδας', - 'invalid_url' => 'Μή έγκυρη μορφή URL. Το URL πρέπει να αρχίζει με το σύμβολο μπροστινής καθέτου και μπορεί να περιέχει αριθμητικά ψηφία, Λατινικούς χαρακτήρες και τα ακόλουθα σύμβολα: _-/.', - 'url_not_unique' => 'Αυτό το URL χρησιμοποιήται ήδη από άλλη σελίδα.', - 'layout' => 'Σχέδιο', - 'layouts_not_found' => 'Δεν βρέθηκαν σχέδια', - 'saved' => 'Η σελίδα αποθηκεύτηκε επιτυχώς.', - 'tab' => 'Σελίδες', - 'manage_pages' => 'Διαχείριση στατικών σελίδων', - 'manage_menus' => 'Διαχείριση στατικών μενού', - 'access_snippets' => 'Πρόσβαση στα αποσπάσματα', - 'manage_content' => 'Διαχείριση στατικού περιεχομένου', - ], - 'menu' => [ - 'menu_label' => 'Μενού', - 'delete_confirmation' => 'Θέλετε πραγματικά να διαγράψετε τα επιλεγμένα μενού;', - 'no_records' => 'Δεν βρέθηκανε μενού', - 'new' => 'Νέο μενού', - 'new_name' => 'Νέο μενού', - 'new_code' => 'new-menu', - 'delete_confirm_single' => 'Θέλετε πραγματικά να διαγράψετε αυτό το μενού;', - 'saved' => 'Αυτό το μενού αποθηκεύτηκε επιτυχώς.', - 'name' => 'Όνομα', - 'code' => 'Κωδικός', - 'items' => 'Στοιχεία μενού', - 'add_subitem' => 'Προσθήκη υποστοιχείου', - 'code_required' => 'Ο Κωδικός είναι απαραίτητος', - 'invalid_code' => 'Μή έγκυρη μορφή Κωδικού. Ο Κωδικός μπορεί να περιέχει αριθμητικά ψηφία, Λατινικούς χαρακτήρες και τα ακόλουθα σύμβολα: _-', - ], - 'menuitem' => [ - 'title' => 'Τίτλος', - 'editor_title' => 'Επεξεργασία στοιχείου μενού', - 'type' => 'Τύπος', - 'allow_nested_items' => 'Να επιτρέπονται ένθετα στοιχεία', - 'allow_nested_items_comment' => 'Τα ένθετα στοιχεία μπορούν να δημιουργηθούν δυναμικά από στατικές σελίδες και κάποιους άλλους τύπους στοιχείων', - 'url' => 'URL', - 'reference' => 'Αναφορά', - 'search_placeholder' => 'Αναζήτηση αναφορών...', - 'title_required' => 'Ο Τίτλος είναι απαραίτητος', - 'unknown_type' => 'Άγνωστος τύπος στοιχείου μενού', - 'unnamed' => 'Στοιχείο μενού χωρίς όνομα', - 'add_item' => 'Προσθήκη Στοιχείου', - 'new_item' => 'Νέο στοιχείο μενού', - 'replace' => 'Αντικατάσταση του στοιχείου με τα παραγμένα υποστοιχεία', - 'replace_comment' => 'Χρησιμοποιήστε αυτό το πλαίσιο για να ωθήσετε τα παραγμένα στοιχεία μενού στο ίδιο επίπεδο με αυτό το στοιχείο. Το ίδιο το στοιχείο θα γίνει κρυφό.', - 'cms_page' => 'Σελίδα CMS', - 'cms_page_comment' => 'Επιλέξτε σελίδα που θα ανοίγει όταν αυτό το μενού πατηθεί.', - 'reference_required' => 'Η αναφορά του στοιχείου μενού είναι απαραίτητη.', - 'url_required' => 'Το URL είναι απαραίτητο', - 'cms_page_required' => 'Παρακαλώ διαλέξτε μία σελίδα CMS', - 'code' => 'Κωδικός', - 'code_comment' => 'Εισάγετε το κωδικό του μενού εάν θέλετε να έχετε πρόσβαση μέσω του API.', - 'static_page' => 'Στατική σελίδα', - 'all_static_pages' => 'Όλες οι Στατικές Σελίδες', - ], - 'content' => [ - 'menu_label' => 'Περιεχόμενο', - 'cant_save_to_dir' => 'Η αποθήκευση των αρχείων περιεχομένου στο φάκελο στατικών σελίδων δεν επιτρέπεται.', - ], - 'sidebar' => [ - 'add' => 'Προσθήκη', - ], - 'object' => [ - 'invalid_type' => 'Άγνωστος τύπος αντικειμένου', - 'not_found' => 'Το ζητούμενο αντικείμενο δεν βρέθηκε.', - ], - 'editor' => [ - 'title' => 'Τίτλος', - 'new_title' => 'Νέος τίτλος σελίδας', - 'content' => 'Περιεχόμενο', - 'url' => 'URL', - 'filename' => 'Όνομα αρχείου', - 'layout' => 'Σχέδιο', - 'description' => 'Περιγραφή', - 'preview' => 'Προεπισκόπηση', - 'enter_fullscreen' => 'Πλήρης οθόνη', - 'exit_fullscreen' => 'Έξοδος πλήρους οθόνης', - 'hidden' => 'Κρυφό', - 'hidden_comment' => 'Κρυφές σελίδες είναι προσβάσιμες μόνο στους συνδεδεμένους χρήστες back-end.', - 'navigation_hidden' => 'Απόκρυψη στην πλοήγηση', - 'navigation_hidden_comment' => 'Επιλέξτε αυτό το πλαίσιο για να κρύψετε αυτή την σελίδα από αυτοματοποιημένα μενού και breadcrumbs.', - ], - 'snippet' => [ - 'menu_label' => 'Αποσπάσματα', - ], - 'component' => [ - 'static_page_name' => 'Στατική σελίδα', - 'static_page_description' => 'Παράγει μία στατική σελίδα ενσωματωμένη σε ένα σχέδιο CMS.', - 'static_page_use_content_name' => 'Χρησιμοποιήστε το πεδίο περιεχομένου σελίδας', - 'static_page_use_content_description' => 'Εάν δεν είναι επιλεγμένο, η περιοχή περιεχομένου δεν θα εμφανίζεται όταν επεξεργάζεται η στατική σελίδα. Το περιεχόμενο της σελίδας θα ορίζεται αποκλειστικά μέσω αντικαταστατών και μεταβλητών.', - 'static_page_default_name' => 'Προεπιλεγμένο σχέδιο', - 'static_page_default_description' => 'Ορίζει αυτό το σχέδιο σαν το προεπιλεγμένο για καινούργιες σελίδες', - 'static_page_child_layout_name' => 'Σχέδιο υποσελίδων', - 'static_page_child_layout_description' => 'Το προεπιλεγμένο σχέδιο για καινούργιες υποσελίδες', - 'static_menu_name' => 'Στατικό μενού', - 'static_menu_description' => 'Παράγει ένα μενού σε ένα σχέδιο CMS.', - 'static_menu_code_name' => 'Μενού', - 'static_menu_code_description' => 'Ορίστε ένα κωδικό του μενού που το Δομικό Στοιχείο θα πρέπει να εξάγει.', - 'static_breadcrumbs_name' => 'Στατικά breadcrumbs', - 'static_breadcrumbs_description' => 'Παράγει breadcrumbs για μία στατική σελίδα.', - ], -]; diff --git a/lang/en.json b/lang/en.json new file mode 100644 index 00000000..334a4419 --- /dev/null +++ b/lang/en.json @@ -0,0 +1,83 @@ +{ + "Pages": "Pages", + "Pages & menus features.": "Pages & menus features.", + "Manage static pages": "Manage static pages", + "Manage static menus": "Manage static menus", + "Manage static content": "Manage static content", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.", + "This URL is already used by another page.": "This URL is already used by another page.", + "Layouts not found": "Layouts not found", + "The Code is required": "The Code is required", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-", + "Static page": "Static page", + "All static pages": "All static pages", + "Static menu": "Static menu", + "Static breadcrumbs": "Static breadcrumbs", + "Child pages": "Child pages", + "Outputs a static page in a CMS layout.": "Outputs a static page in a CMS layout.", + "Use page content field": "Use page content field", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.", + "Default layout": "Default layout", + "Defines this layout as the default for new pages": "Defines this layout as the default for new pages", + "Subpage layout": "Subpage layout", + "The layout to use as the default for any new subpages": "The layout to use as the default for any new subpages", + "Outputs a menu in a CMS layout.": "Outputs a menu in a CMS layout.", + "Menu": "Menu", + "Specify a code of the menu the component should output.": "Specify a code of the menu the component should output.", + "Outputs breadcrumbs for a static page.": "Outputs breadcrumbs for a static page.", + "Displays a list of child pages for the current page": "Displays a list of child pages for the current page", + "Static Pages": "Static Pages", + "Menus": "Menus", + "Content": "Content", + "Add": "Add", + "Refresh": "Refresh", + "Page": "Page", + "Content block": "Content block", + "New page": "New page", + "New page title": "New page title", + "New menu": "New menu", + "New content block": "New content block", + "Title": "Title", + "URL": "URL", + "File Name": "File Name", + "Layout": "Layout", + "Hidden": "Hidden", + "Hide in navigation": "Hide in navigation", + "Description": "Description", + "Name": "Name", + "Code": "Code", + "The Title is required.": "The Title is required.", + "The URL is required.": "The URL is required.", + "The File Name is required.": "The File Name is required.", + "The Name is required.": "The Name is required.", + "The Code is required.": "The Code is required.", + "Error loading page": "Error loading page", + "Error loading menu": "Error loading menu", + "Error loading content block": "Error loading content block", + "Add item": "Add item", + "Add subitem": "Add subitem", + "New menu item": "New menu item", + "Untitled": "Untitled", + "Up": "Up", + "Down": "Down", + "Indent": "Indent", + "Outdent": "Outdent", + "Delete": "Delete", + "No menu items yet. Use \"Add item\" in the toolbar.": "No menu items yet. Use \"Add item\" in the toolbar.", + "Select a menu item to edit, or add a new one.": "Select a menu item to edit, or add a new one.", + "Custom Fields": "Custom Fields", + "Search all references...": "Search all references...", + "Edit Menu Item": "Edit Menu Item", + "Move up": "Move up", + "Move down": "Move down", + "Apply": "Apply", + "Cancel": "Cancel", + "New Page": "New Page", + "New Menu": "New Menu", + "New Content Block": "New Content Block", + "Fields": "Fields", + "Preview": "Preview", + "Add subpage": "Add subpage", + "You don't have permissions to manage :type documents.": "You don't have permissions to manage :type documents.", + "Content files cannot be saved in the static pages directory.": "Content files cannot be saved in the static pages directory." +} diff --git a/lang/en/lang.php b/lang/en/lang.php deleted file mode 100644 index 2664c7f2..00000000 --- a/lang/en/lang.php +++ /dev/null @@ -1,130 +0,0 @@ - [ - 'name' => 'Pages', - 'description' => 'Pages & menus features.', - ], - 'page' => [ - 'menu_label' => 'Pages', - 'template_title' => '%s Pages', - 'delete_confirmation' => 'Do you really want to delete selected pages? This will also delete the subpages, if any.', - 'no_records' => 'No pages found', - 'delete_confirm_single' => 'Do you really want delete this page? This will also delete the subpages, if any.', - 'new' => 'New page', - 'add_subpage' => 'Add subpage', - 'invalid_url' => 'Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.', - 'url_not_unique' => 'This URL is already used by another page.', - 'layout' => 'Layout', - 'layouts_not_found' => 'Layouts not found', - 'saved' => 'The page has been successfully saved.', - 'tab' => 'Pages', - 'manage_pages' => 'Manage static pages', - 'manage_menus' => 'Manage static menus', - 'access_snippets' => 'Access snippets', - 'manage_content' => 'Manage static content', - ], - 'menu' => [ - 'menu_label' => 'Menus', - 'delete_confirmation' => 'Do you really want to delete selected menus?', - 'no_records' => 'No menus found', - 'new' => 'New menu', - 'new_name' => 'New menu', - 'new_code' => 'new-menu', - 'delete_confirm_single' => 'Do you really want delete this menu?', - 'saved' => 'The menu has been successfully saved.', - 'name' => 'Name', - 'code' => 'Code', - 'items' => 'Menu items', - 'add_subitem' => 'Add subitem', - 'code_required' => 'The Code is required', - 'invalid_code' => 'Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-', - ], - 'menuitem' => [ - 'title' => 'Title', - 'editor_title' => 'Edit Menu Item', - 'type' => 'Type', - 'allow_nested_items' => 'Allow nested items', - 'allow_nested_items_comment' => 'Nested items could be generated dynamically by static page and some other item types', - 'url' => 'URL', - 'reference' => 'Reference', - 'search_placeholder' => 'Search all references...', - 'title_required' => 'The Title is required', - 'unknown_type' => 'Unknown menu item type', - 'unnamed' => 'Unnamed menu item', - 'add_item' => 'Add Item', - 'new_item' => 'New menu item', - 'replace' => 'Replace this item with its generated children', - 'replace_comment' => 'Use this checkbox to push generated menu items to the same level with this item. This item itself will be hidden.', - 'cms_page' => 'CMS Page', - 'cms_page_comment' => 'Select a page to open when the menu item is clicked.', - 'reference_required' => 'The menu item reference is required.', - 'url_required' => 'The URL is required', - 'cms_page_required' => 'Please select a CMS page', - 'display_tab' => 'Display', - 'hidden' => 'Hidden', - 'hidden_comment' => 'Hide this menu item from appearing on the front-end.', - 'attributes_tab' => 'Attributes', - 'code' => 'Code', - 'code_comment' => 'Enter the menu item code if you want to access it with the API.', - 'css_class' => 'CSS Class', - 'css_class_comment' => 'Enter a CSS class name to give this menu item a custom appearance.', - 'external_link' => 'External link', - 'external_link_comment' => 'Open links for this menu item in a new window.', - 'static_page' => 'Static Page', - 'all_static_pages' => 'All Static Pages', - ], - 'content' => [ - 'menu_label' => 'Content', - 'saved' => 'The content has been successfully saved.', - 'cant_save_to_dir' => 'Saving content files to the static-pages directory is not allowed.', - ], - 'template' => [ - 'order_by' => 'Order by', - 'no_list_records' => 'No records found', - 'delete_confirm' => 'Delete selected templates?', - ], - 'sidebar' => [ - 'add' => 'Add', - ], - 'object' => [ - 'invalid_type' => 'Unknown object type', - 'unauthorized_type' => 'You are not authorized to manage :type objects', - 'not_found' => 'The requested object was not found.', - ], - 'editor' => [ - 'title' => 'Title', - 'new_title' => 'New page title', - 'content' => 'Content', - 'url' => 'URL', - 'filename' => 'File Name', - 'layout' => 'Layout', - 'description' => 'Description', - 'preview' => 'Preview', - 'enter_fullscreen' => 'Enter fullscreen mode', - 'exit_fullscreen' => 'Exit fullscreen mode', - 'hidden' => 'Hidden', - 'hidden_comment' => 'Hidden pages are accessible only by logged-in back-end users.', - 'navigation_hidden' => 'Hide in navigation', - 'navigation_hidden_comment' => 'Check this box to hide this page from automatically generated menus and breadcrumbs.', - ], - 'snippet' => [ - 'menu_label' => 'Snippets', - ], - 'component' => [ - 'static_page_name' => 'Static page', - 'static_page_description' => 'Outputs a static page in a CMS layout.', - 'static_page_use_content_name' => 'Use page content field', - 'static_page_use_content_description' => 'If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.', - 'static_page_default_name' => 'Default layout', - 'static_page_default_description' => 'Defines this layout as the default for new pages', - 'static_page_child_layout_name' => 'Subpage layout', - 'static_page_child_layout_description' => 'The layout to use as the default for any new subpages', - 'static_menu_name' => 'Static menu', - 'static_menu_description' => 'Outputs a menu in a CMS layout.', - 'static_menu_code_name' => 'Menu', - 'static_menu_code_description' => 'Specify a code of the menu the component should output.', - 'static_breadcrumbs_name' => 'Static breadcrumbs', - 'static_breadcrumbs_description' => 'Outputs breadcrumbs for a static page.', - 'child_pages_name' => 'Child pages', - 'child_pages_description' => 'Displays a list of child pages for the current page', - ], -]; diff --git a/lang/es.json b/lang/es.json new file mode 100644 index 00000000..19f78da7 --- /dev/null +++ b/lang/es.json @@ -0,0 +1,83 @@ +{ + "Pages": "Páginas", + "Pages & menus features.": "Funciones de páginas y menús.", + "Manage static pages": "Administrar páginas", + "Manage static menus": "Administrar menús", + "Manage static content": "Administrar contenidos", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Formato de URL no válido. La URL debería comenzar por una barra ('/'). Puede contener letras, números y los siguientes símbolos: _-/", + "This URL is already used by another page.": "Esta URL ya está siendo utilizada por otra página.", + "Layouts not found": "No se han encontrado plantillas", + "The Code is required": "El código es obligatorio", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "El formato del código no es válido. Puede contener letras, números y los siguientes símbolos: _-", + "Static page": "Página estática", + "All static pages": "Todas las páginas estáticas", + "Static menu": "Menú estático", + "Static breadcrumbs": "Migas de pan estáticas", + "Child pages": "Subpáginas", + "Outputs a static page in a CMS layout.": "Muestra una página estática en una plantilla del CMS.", + "Use page content field": "Usar el campo de contenido de la página", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Si no está marcado, la sección de contenido no aparecerá al editar la página estática. El contenido de la página se determinará únicamente mediante marcadores de posición y variables.", + "Default layout": "Plantilla predeterminada", + "Defines this layout as the default for new pages": "Define esta plantilla como predeterminada para las páginas nuevas", + "Subpage layout": "Plantilla de subpáginas", + "The layout to use as the default for any new subpages": "La plantilla que se usará como predeterminada para las nuevas subpáginas", + "Outputs a menu in a CMS layout.": "Muestra un menú en una plantilla del CMS.", + "Menu": "Menú", + "Specify a code of the menu the component should output.": "Especifica el código del menú que debe mostrar el componente.", + "Outputs breadcrumbs for a static page.": "Muestra las migas de pan de una página estática.", + "Displays a list of child pages for the current page": "Muestra una lista de subpáginas de la página actual", + "Static Pages": "Páginas estáticas", + "Menus": "Menús", + "Content": "Contenido", + "Add": "Añadir", + "Refresh": "Actualizar", + "Page": "Página", + "Content block": "Bloque de contenido", + "New page": "Nueva página", + "New page title": "Título de la nueva página", + "New menu": "Nuevo menú", + "New content block": "Nuevo bloque de contenido", + "Title": "Título", + "URL": "URL", + "File Name": "Nombre de archivo", + "Layout": "Plantilla", + "Hidden": "Oculto", + "Hide in navigation": "No mostrar en el menú", + "Description": "Descripción", + "Name": "Nombre", + "Code": "Código", + "The Title is required.": "El título es obligatorio.", + "The URL is required.": "La URL es obligatoria.", + "The File Name is required.": "El nombre de archivo es obligatorio.", + "The Name is required.": "El nombre es obligatorio.", + "The Code is required.": "El código es obligatorio.", + "Error loading page": "Error al cargar la página", + "Error loading menu": "Error al cargar el menú", + "Error loading content block": "Error al cargar el bloque de contenido", + "Add item": "Añadir elemento", + "Add subitem": "Añadir sub-elemento", + "New menu item": "Nuevo elemento de menú", + "Untitled": "Sin título", + "Up": "Arriba", + "Down": "Abajo", + "Indent": "Aumentar sangría", + "Outdent": "Reducir sangría", + "Delete": "Eliminar", + "No menu items yet. Use \"Add item\" in the toolbar.": "Aún no hay elementos de menú. Usa \"Añadir elemento\" en la barra de herramientas.", + "Select a menu item to edit, or add a new one.": "Selecciona un elemento del menú para editarlo o añade uno nuevo.", + "Custom Fields": "Campos personalizados", + "Search all references...": "Buscar en todas las referencias...", + "Edit Menu Item": "Editar elemento del menú", + "Move up": "Mover hacia arriba", + "Move down": "Mover hacia abajo", + "Apply": "Aplicar", + "Cancel": "Cancelar", + "New Page": "Nueva página", + "New Menu": "Nuevo menú", + "New Content Block": "Nuevo bloque de contenido", + "Fields": "Campos", + "Preview": "Vista previa", + "Add subpage": "Añadir sub-página", + "You don't have permissions to manage :type documents.": "No tienes permisos para administrar documentos de tipo :type.", + "Content files cannot be saved in the static pages directory.": "Los archivos de contenido no se pueden guardar en el directorio de páginas estáticas." +} diff --git a/lang/es/lang.php b/lang/es/lang.php deleted file mode 100644 index 88511983..00000000 --- a/lang/es/lang.php +++ /dev/null @@ -1,94 +0,0 @@ - [ - 'name' => 'Páginas', - 'description' => 'Páginas & menus', - ], - 'page' => [ - 'menu_label' => 'Páginas', - 'template_title' => '%s Páginas', - 'delete_confirmation' => 'Estas seguro de querer borrar las páginas seleccionadas? Esto también borrará las sub-páginas que existan.', - 'no_records' => 'No se ha encontrado ninguna página', - 'delete_confirm_single' => 'Estas seguro de querer borrar esta página? Esto también borrará las sub-páginas que existan.', - 'new' => 'Nueva página', - 'add_subpage' => 'Añadir sub-página', - 'invalid_url' => 'Formato de URL no válido. La URL debería comenzar por una barra (\'/\'). Puede contener letras, números, y los siguientes símbolos _ - / ', - 'url_not_unique' => 'Esta URL ya está siendo utilizada por otra página.', - 'layout' => 'Plantilla', - 'layouts_not_found' => 'No se han encontrado plantillas', - 'saved' => 'La página se ha guardado correctamente.', - 'tab' => 'Páginas', - 'manage_pages' => 'Administrar páginas', - 'manage_menus' => 'Administrar menús', - 'access_snippets' => 'Acceder a fragmentos', - 'manage_content' => 'Administrar contenidos', - ], - 'menu' => [ - 'menu_label' => 'Menus', - 'delete_confirmation' => 'Estas seguro de querer borrar los menus seleccionados?', - 'no_records' => 'No se han encontrado elementos.', - 'new' => 'Nuevo menu', - 'new_name' => 'Nuevo menu', - 'new_code' => 'nuevo-menu', - 'delete_confirm_single' => 'Estas seguro de querer borrar este menu?', - 'saved' => 'El menú se ha guardado correctamente.', - 'name' => 'Nombre', - 'code' => 'Código', - 'items' => 'Elementos del menu', - 'add_subitem' => 'Añadir sub-elemento', - 'code_required' => 'El código es obligatorio', - 'invalid_code' => 'El formato del código no es válido. Puede contener letras, números y los siguientes símbolos: _ - ', - ], - 'menuitem' => [ - 'title' => 'Título', - 'editor_title' => 'Editar elemento del menu', - 'type' => 'Tipo', - 'allow_nested_items' => 'Permitir elementos anidados', - 'allow_nested_items_comment' => 'Los elementos anidados se pueden generar automáticamente mediante las páginas y otros tipos de elementos.', - 'url' => 'URL', - 'reference' => 'Referencia', - 'title_required' => 'El título es obligatorio', - 'unknown_type' => 'Este tipo de elemento del menú es desconocido.', - 'unnamed' => 'Elemento del menú sin nombre', - 'add_item' => 'Añadir elemento', - 'new_item' => 'Nuevo elemento', - 'replace' => 'Sustituye este elemento por los sub-elementos que contenga.', - 'replace_comment' => 'Marca esta casilla sustituir este elemento por los sub-elementos que contenga. El elemento proncipal quedará oculto.', - 'cms_page' => 'Página del CMS', - 'cms_page_comment' => 'Selecciona una página a la que enlazar cuando se haga click en este elemento del menu.', - 'reference_required' => 'La referencia al elemento del menú es obligatoria.', - 'url_required' => 'La URL es obligatoria', - 'cms_page_required' => 'Selecciona una página del CMS', - 'code' => 'Código', - 'code_comment' => 'Introduce el código del elemento para acceder mediante la API.', - ], - 'content' => [ - 'menu_label' => 'Contenido', - 'cant_save_to_dir' => 'No está permitido guardar archivos de contenido en el directorio de las páginas.', - ], - 'sidebar' => [ - 'add' => 'Añadir', - ], - 'object' => [ - 'invalid_type' => 'Tipo de objeto desconocido', - 'not_found' => 'No se ha encontrado el objeto solicitado.', - ], - 'editor' => [ - 'title' => 'Título', - 'new_title' => 'Título de la nueva página', - 'content' => 'Contenido', - 'url' => 'URL', - 'filename' => 'Nombre de archivo', - 'layout' => 'Plantilla', - 'description' => 'Descripción', - 'preview' => 'Vista previa', - 'enter_fullscreen' => 'Entrar en modo de pantalla completa', - 'exit_fullscreen' => 'Salir del modo de pantalla completa', - 'hidden' => 'Oculto', - 'hidden_comment' => 'Las páginas ocultas solo son visibles para los administradores que hayan iniciado sesión.', - 'navigation_hidden' => 'No mostrar en el menu', - 'navigation_hidden_comment' => 'Marca esta casilla para ocultar esta página en los menus generados automáticamente.', - ], - 'snippet' => [ - 'menu_label' => 'Fragmentos', - ], -]; diff --git a/lang/fa.json b/lang/fa.json new file mode 100644 index 00000000..2ef8c334 --- /dev/null +++ b/lang/fa.json @@ -0,0 +1,83 @@ +{ + "Pages": "صفحات", + "Pages & menus features.": "مدیریت صفحات و فهرست ها", + "Manage static pages": "مدیریت صفحات استاتیک", + "Manage static menus": "مدیریت فهرست های استاتیک", + "Manage static content": "مدیریت محتوی استاتیک", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "قالب آدرس نا معتبر است. آدرس باید با اسلش شروع شود و میتواند شامل حروف لاتین، حروف فارسی، اعداد و این کاراکتر ها باشد: _-/.", + "This URL is already used by another page.": "این آدرس توسط صفحه ی دیگری استفاده شده است.", + "Layouts not found": "طرح بندی یافت نشد", + "The Code is required": "وارد کردن کد اجباریست", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "قالب کد نا معتبر است. کد میتواند شامل اعداد، حروف لاتین و این کاراکتر ها باشد: _-", + "Static page": "صفحه ی استاتیک", + "All static pages": "تمام صفحات استاتیک", + "Static menu": "فهرست استاتیک", + "Static breadcrumbs": "نشان گرها", + "Child pages": "صفحات زیرمجموعه", + "Outputs a static page in a CMS layout.": "نمایش یک صفحه استاتیک در طرح بندی.", + "Use page content field": "استفاده از فیلد محتوای صفحه", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "اگر غیرفعال باشد، بخش محتوا به هنگام ویرایش صفحه ی استاتیک نمایش داده نخواهد شد. محتوای صفحه تنها با استفاده از جانگهدارها و متغیرها تعیین می شود.", + "Default layout": "طرح بندی پیش فرض", + "Defines this layout as the default for new pages": "این طرح بندی را به عنوان طرح بندی پیش فرض برای صفحات جدید تعیین می کند", + "Subpage layout": "طرح بندی صفحات زیرمجموعه", + "The layout to use as the default for any new subpages": "طرح بندی که به عنوان پیش فرض برای صفحات زیرمجموعه ی جدید استفاده می شود", + "Outputs a menu in a CMS layout.": "نمایش فهرست استاتیک در طرح بندی.", + "Menu": "فهرست", + "Specify a code of the menu the component should output.": "کد فهرستی را که میخواهید به نمایش درآید انتخاب نمایید.", + "Outputs breadcrumbs for a static page.": "نمایش نشان گرهای صفحه.", + "Displays a list of child pages for the current page": "فهرستی از صفحات زیرمجموعه ی صفحه ی فعلی را نمایش می دهد", + "Static Pages": "صفحات استاتیک", + "Menus": "فهرست ها", + "Content": "محتوی", + "Add": "افزودن", + "Refresh": "تازه سازی", + "Page": "صفحه", + "Content block": "بلوک محتوا", + "New page": "صفحه ی جدید", + "New page title": "عنوان صفحه ی جدید", + "New menu": "فهرست جدید", + "New content block": "بلوک محتوای جدید", + "Title": "عنوان", + "URL": "آدرس", + "File Name": "نام فایل", + "Layout": "طرح بندی", + "Hidden": "مخفی", + "Hide in navigation": "مخفی کردن در فهرست", + "Description": "توضیحات", + "Name": "نام", + "Code": "کد", + "The Title is required.": "وارد کردن عنوان اجباریست.", + "The URL is required.": "وارد کردن آدرس الزامیست.", + "The File Name is required.": "وارد کردن نام فایل الزامیست.", + "The Name is required.": "وارد کردن نام الزامیست.", + "The Code is required.": "وارد کردن کد اجباریست.", + "Error loading page": "خطا در بارگذاری صفحه", + "Error loading menu": "خطا در بارگذاری فهرست", + "Error loading content block": "خطا در بارگذاری بلوک محتوا", + "Add item": "افزودن مورد", + "Add subitem": "افزودن زیرمورد", + "New menu item": "مورد جدید برای فهرست", + "Untitled": "بدون عنوان", + "Up": "بالا", + "Down": "پایین", + "Indent": "افزایش تورفتگی", + "Outdent": "کاهش تورفتگی", + "Delete": "حذف", + "No menu items yet. Use \"Add item\" in the toolbar.": "هنوز موردی در فهرست وجود ندارد. از \"افزودن مورد\" در نوار ابزار استفاده کنید.", + "Select a menu item to edit, or add a new one.": "یک مورد فهرست را برای ویرایش انتخاب کنید یا مورد جدیدی اضافه کنید.", + "Custom Fields": "فیلدهای سفارشی", + "Search all references...": "جستجوی همه موارد...", + "Edit Menu Item": "ویرایش مورد فهرست", + "Move up": "انتقال به بالا", + "Move down": "انتقال به پایین", + "Apply": "اعمال", + "Cancel": "انصراف", + "New Page": "صفحه ی جدید", + "New Menu": "فهرست جدید", + "New Content Block": "بلوک محتوای جدید", + "Fields": "فیلدها", + "Preview": "پیش نمایش", + "Add subpage": "افزودن زیر مجموعه", + "You don't have permissions to manage :type documents.": "شما مجوز مدیریت اسناد :type را ندارید.", + "Content files cannot be saved in the static pages directory.": "فایل های محتوا را نمی توان در پوشه ی صفحات استاتیک ذخیره کرد." +} diff --git a/lang/fa/lang.php b/lang/fa/lang.php deleted file mode 100644 index ce72c3cb..00000000 --- a/lang/fa/lang.php +++ /dev/null @@ -1,113 +0,0 @@ - [ - 'name' => 'صفحات', - 'description' => 'مدیریت صفحات و فهرست ها', - ], - 'page' => [ - 'menu_label' => 'صفحات', - 'template_title' => 'صفحات %s', - 'delete_confirmation' => 'آیا از حذف صفحات انتخاب شده اطمینان دارید؟ اگر صفحات دارای زیر صفحه باشند آنها نیز حذف خواهند شد.', - 'no_records' => 'صفحه ای یافت نشد', - 'delete_confirm_single' => 'آیا از حذف این صفحه اطمینان دارید؟ اگر این صفحه دارای زیر مجموعه باشد آنها نیز حذف خواهند شد.', - 'new' => 'صفحه ی جدید', - 'add_subpage' => 'افزودن زیر مجموعه', - 'invalid_url' => 'قالب آدرس نا معتبر است. آدرس باید با اسلش شروع شود و میتواند شامل حروف لاتین، حروف فارسی، اعداد و این کاراکتر ها باشد: _-/.', - 'url_not_unique' => 'این آدرس توسط صفحه ی دیگری استفاده شده است.', - 'layout' => 'طرح بندی', - 'layouts_not_found' => 'طرح بندی برای صفحات استاتیک یافت نشد.', - 'saved' => 'صفحه با موفقیت ذخیره شد.', - 'tab' => 'صفحات', - 'manage_pages' => 'مدیریت صفحات استاتیک', - 'manage_menus' => 'مدیریت فهرست های استاتیک', - 'access_snippets' => 'دسترسی به تکه کد ها', - 'manage_content' => 'مدیریت محتوی استاتیک', - ], - 'menu' => [ - 'menu_label' => 'فهرست ها', - 'delete_confirmation' => 'آیا از حذف فهرست انتخاب شده اطمینان دارید؟', - 'no_records' => 'موردی یافت نشد', - 'new' => 'فهرست جدید', - 'new_name' => 'فهرست جدید', - 'new_code' => 'new-menu', - 'delete_confirm_single' => 'آیا از حذف این فهرست اطمینان دارید؟', - 'saved' => 'فهرست با موفقیت ذخیره شد.', - 'name' => 'نام', - 'code' => 'کد', - 'items' => 'موارد فهرست', - 'add_subitem' => 'افزودن زیر فهرست', - 'code_required' => 'وارد کردن کد اجباریست', - 'invalid_code' => 'قالب کد نا معتبر است. کد میتواند شامل اعداد، حروف لاتین و این کاراکتر ها باشد: _-', - ], - 'menuitem' => [ - 'title' => 'عنوان', - 'editor_title' => 'ویرایش فهرست', - 'type' => 'نوع', - 'allow_nested_items' => 'استفاده از موارد تو در تو', - 'allow_nested_items_comment' => 'موارد تو در تو به صورت خودکار توسط صفحات استاتیک و برخی از دیگر موارد ایجاد می شوند', - 'url' => 'آدرس', - 'reference' => 'مرجع', - 'search_placeholder' => 'جستجوی همه موارد...', - 'title_required' => 'وارد کردن عنوان اجباریست', - 'unknown_type' => 'نوع نامشخص فهرست', - 'unnamed' => 'فهرست بدون نام', - 'add_item' => 'افزودن فهرست', - 'new_item' => 'مورد جدید برای فهرست', - 'replace' => 'جایگرینی این مورد با زیر مورد های ایجاد شده', - 'replace_comment' => 'اگر میخواهید زیر فهرست های ایجاد شده هم سطح با این مورد قرار بگیرند این گزینه را فعال نمایید. خود فهرست بصورت خودکار مخفی خواهد شد.', - 'cms_page' => 'صفحه ی مدیریت محتوی', - 'cms_page_comment' => 'صفحه ای را که میخواهید به هنگام انتخاب این فهرست باز شود را انتخاب نمایید.', - 'reference_required' => 'وارد کردن مرجع برای فهرست الزامیست.', - 'url_required' => 'وارد کردن آدرس الزامیست', - 'cms_page_required' => 'لطفا یک صفحه را انتخاب کنید', - 'code' => 'کد', - 'code_comment' => 'اگر میخواهید از طریق کد ها به این مورد از فهرست دسترسی پیدا کنید کد آن را وارد نمایید.', - 'static_page' => 'صفحات استاتیک', - 'all_static_pages' => 'تمام صفحات استاتیک', - ], - 'content' => [ - 'menu_label' => 'محتوی', - 'cant_save_to_dir' => 'مجوز ذخیره ی داده ها در پوشه ی صفحات استاتسک وجود ندارد.', - ], - 'sidebar' => [ - 'add' => 'افزودن', - ], - 'object' => [ - 'invalid_type' => 'نوع شیء نا مشخص است', - 'not_found' => 'شیء درخواستی یافت نشد.', - ], - 'editor' => [ - 'title' => 'عنوان', - 'new_title' => 'عنوان صفحه ی جدید', - 'content' => 'محتوی', - 'url' => 'آدرس', - 'filename' => 'نام فایل', - 'layout' => 'طرح بندی', - 'description' => 'توضیحات', - 'preview' => 'پیش نمایش', - 'enter_fullscreen' => 'حالت تمام صفحه', - 'exit_fullscreen' => 'خروج از حالت تمام صفحه', - 'hidden' => 'مخفی', - 'hidden_comment' => 'صفحات مخفی توسط کاربران وارد شده به سایت قابل دسترس می باشند.', - 'navigation_hidden' => 'مخفی کردن در فهرست', - 'navigation_hidden_comment' => 'اگر میخواهید صفحه مورد نظر در فهرست هایی که خودکار ایجاد می شوند و یا نشان گرهای صفحه دیده نشوند این گزینه را انتخاب نمایید.', - ], - 'snippet' => [ - 'menu_label' => 'تکه کدها', - ], - 'component' => [ - 'static_page_name' => 'صفحات استاتیک', - 'static_page_description' => 'نمایش یک صفحه استاتیک در طرح بندی.', - 'static_page_use_content_name' => 'استفاده از گرینه ها در محتوی.', - 'static_page_use_content_description' => 'اگر غیر فعال باشد، فیلد ها به هنگام ویرایش صفحات استاتیک در بخش محتوی نمایش داده نخواهند شد. محتوی صفحه با استفاده از متغییر های تعریف شده قابل کنتل می باشند.', - 'static_page_default_name' => 'طرح بندی پیش فرض', - 'static_page_default_description' => 'این طرح بندی به عنوان طرح بندی پیشفرض به هنگام ایجاد صفحه جدید در نظر گرفته شود؟', - 'static_page_child_layout_name' => 'طرح بندی صفحات زیرمجموعه', - 'static_page_child_layout_description' => 'این طرح بندی به عنوان طرح بندی تمام صفحات زیر مجموعه در نظر گرفنه شود؟', - 'static_menu_name' => 'فهرست استاتیک', - 'static_menu_description' => 'نمایش فهرست استاتیک در طرح بندی.', - 'static_menu_code_name' => 'فهرست', - 'static_menu_code_description' => 'کد فهرستی را که میخواهید به نمایش درآید انتخاب نمایید.', - 'static_breadcrumbs_name' => 'نشان گرها', - 'static_breadcrumbs_description' => 'نمایش نشان گرهای صفحه.', - ], -]; diff --git a/lang/fi.json b/lang/fi.json new file mode 100644 index 00000000..97fd8c5a --- /dev/null +++ b/lang/fi.json @@ -0,0 +1,83 @@ +{ + "Pages": "Sivut", + "Pages & menus features.": "Sivu- ja valikko-ominaisuudet.", + "Manage static pages": "Hallitse staattisia sivuja", + "Manage static menus": "Hallitse staattisia valikkoja", + "Manage static content": "Hallitse staattista sisältöä", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Kelvoton URL formaatti. URL pitäisi alkaa kautta -merkillä ja voi sisältää kokonaislukuja, latinalaisia kirjamia, ja seuraavia merkkejä: _-/.", + "This URL is already used by another page.": "Tämä URL on toisen sivun käyttämä.", + "Layouts not found": "Ulkoasuja ei löytynyt", + "The Code is required": "Koodi on vaadittu", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Koodilla on kelvoton formaatti. Koodi voi sisältää kokonaislukuja, latinalaisia kirjamia, ja seuraavia merkkejä: _-", + "Static page": "Staattinen sivu", + "All static pages": "Kaikki staattiset sivut", + "Static menu": "Staattinen valikko", + "Static breadcrumbs": "Staattinen murupolku", + "Child pages": "Alasivut", + "Outputs a static page in a CMS layout.": "Näyttää staattisen sivun CMS ulkoasussa.", + "Use page content field": "Käytä sivun sisältökenttää", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Jos valitsematta, sisältö -kohta ei tule näkyviin staattista sivua muokattaessa. Sivun sisältö määritetään vain paikkamerkkien ja muuttujien kautta.", + "Default layout": "Oletusulkoasu", + "Defines this layout as the default for new pages": "Määrittelee tämän ulkoasun oletukseksi uusille sivuille", + "Subpage layout": "Alasivun ulkoasu", + "The layout to use as the default for any new subpages": "Oletusulkoasu kaikille uusille alasivuille", + "Outputs a menu in a CMS layout.": "Näyttää valikon CMS ulkoasussa.", + "Menu": "Valikko", + "Specify a code of the menu the component should output.": "Määritä valikon koodi joka pitäisi näyttää.", + "Outputs breadcrumbs for a static page.": "Näyttää murupolun staattisella sivulla.", + "Displays a list of child pages for the current page": "Näyttää listan nykyisen sivun alasivuista", + "Static Pages": "Staattiset sivut", + "Menus": "Valikot", + "Content": "Sisältö", + "Add": "Lisää", + "Refresh": "Päivitä", + "Page": "Sivu", + "Content block": "Sisältölohko", + "New page": "Uusi sivu", + "New page title": "Uuden sivun otsikko", + "New menu": "Uusi valikko", + "New content block": "Uusi sisältölohko", + "Title": "Otsikko", + "URL": "URL", + "File Name": "Tiedostonimi", + "Layout": "Ulkoasu", + "Hidden": "Piilotettu", + "Hide in navigation": "Piilota navigaatiosta", + "Description": "Kuvaus", + "Name": "Nimi", + "Code": "Koodi", + "The Title is required.": "Otsikko on vaadittu.", + "The URL is required.": "URL on vaadittu.", + "The File Name is required.": "Tiedostonimi on vaadittu.", + "The Name is required.": "Nimi on vaadittu.", + "The Code is required.": "Koodi on vaadittu.", + "Error loading page": "Virhe sivun latauksessa", + "Error loading menu": "Virhe valikon latauksessa", + "Error loading content block": "Virhe sisältölohkon latauksessa", + "Add item": "Lisää kohde", + "Add subitem": "Lisää alakohde", + "New menu item": "Uusi valikkokohde", + "Untitled": "Nimetön", + "Up": "Ylös", + "Down": "Alas", + "Indent": "Sisennä", + "Outdent": "Poista sisennys", + "Delete": "Poista", + "No menu items yet. Use \"Add item\" in the toolbar.": "Ei vielä valikkokohteita. Käytä työkalupalkin \"Lisää kohde\" -toimintoa.", + "Select a menu item to edit, or add a new one.": "Valitse muokattava valikkokohde tai lisää uusi.", + "Custom Fields": "Mukautetut kentät", + "Search all references...": "Hae kaikista viitteistä...", + "Edit Menu Item": "Muokkaa valikkokohdetta", + "Move up": "Siirrä ylös", + "Move down": "Siirrä alas", + "Apply": "Käytä", + "Cancel": "Peruuta", + "New Page": "Uusi sivu", + "New Menu": "Uusi valikko", + "New Content Block": "Uusi sisältölohko", + "Fields": "Kentät", + "Preview": "Esikatsele", + "Add subpage": "Lisää alasivu", + "You don't have permissions to manage :type documents.": "Sinulla ei ole oikeuksia hallita :type-dokumentteja.", + "Content files cannot be saved in the static pages directory.": "Sisältötiedostoja ei voi tallentaa staattisten sivujen hakemistoon." +} diff --git a/lang/fi/lang.php b/lang/fi/lang.php deleted file mode 100644 index ef64dbf1..00000000 --- a/lang/fi/lang.php +++ /dev/null @@ -1,151 +0,0 @@ - [ - 'name' => 'Sivut', - 'description' => 'Sivu- ja valikko-ominaisuudet.', - ], - 'page' => [ - 'menu_label' => 'Sivut', - 'template_title' => '%s Sivut', - 'delete_confirmation' => 'Haluatko varmasti poistaa valitut sivut? Tämä poistaa myös alasivut, jos sellaisia on.', - 'no_records' => 'Sivuja ei löytynyt', - 'delete_confirm_single' => 'Haluatko varmasti poistaa tämän sivun? Tämä poistaa myös alasivut, jos sellaisia on.', - 'new' => 'Uusi sivu', - 'add_subpage' => 'Lisää alasivu', - 'invalid_url' => 'Kelvoton URL formaatti. URL pitäisi alkaa kautta -merkillä ja voi sisältää kokonaislukuja, latinalaisia kirjamia, ja seuraavia merkkejä: _-/.', - 'url_not_unique' => 'Tämä URL on toisen sivun käyttämä.', - 'layout' => 'Ulkoasu', - 'layouts_not_found' => 'Ulkoasuja ei löytynyt', - 'saved' => 'Sivu on tallennettu onnistuneesti.', - 'tab' => 'Sivut', - 'manage_pages' => 'Hallitse staattisia sivuja', - 'manage_menus' => 'Hallitse staattisia valikkoja', - 'access_snippets' => 'Hallitse koodinpätkiä', - 'manage_content' => 'Hallitse staattista sisältöä', - ], - 'menu' => [ - 'menu_label' => 'Valikot', - 'delete_confirmation' => 'Haluatko varmasti poista valitut valikot?', - 'no_records' => 'Vakujjiha ei löytynyt', - 'new' => 'Uusi valikko', - 'new_name' => 'Uusi valikko', - 'new_code' => 'uusi-valikko', - 'delete_confirm_single' => 'Haluatko varmasti poistaa tämän valikon?', - 'saved' => 'Tämä valikko on tallennettu onnistuneesti.', - 'name' => 'Nimi', - 'code' => 'Koodi', - 'items' => 'Valikon kohteet', - 'add_subitem' => 'Lisää alakohde', - 'code_required' => 'Koodi on vaadittu', - 'invalid_code' => 'Koodilla on kelvoton formaatti. Koodi voi sisältää kokonaislukuja, latinalaisia kirjamia, ja seuraavia merkkejä: _-', - ], - 'menuitem' => [ - 'title' => 'Otsikko', - 'editor_title' => 'Muokkaa valikkokohdetta', - 'type' => 'Tyyppi', - 'allow_nested_items' => 'Salli sisäkkäiset kohteet', - 'allow_nested_items_comment' => 'Sisäkkäiset kohteet staattisissa sivuissa ja muissa kohdetyypeissä voidaan generoida dynaamisesti', - 'url' => 'URL', - 'reference' => 'Viite', - 'search_placeholder' => 'Hae kaikista viitteistä...', - 'title_required' => 'Otsikko on vaadittu', - 'unknown_type' => 'Tuntematon valikkokohteen tyyppi', - 'unnamed' => 'Nimeämätön valikkokohde', - 'add_item' => 'Lisää Kohde', - 'new_item' => 'Uusi valikkokohde', - 'replace' => 'Korvaa valikko sen generoimilla alikohteilla', - 'replace_comment' => 'Käytä tätä valintaruutua työntääksesi valikon kohteet samalle tasolle tämän kohteen kanssa. Tämä kohde itsessään on piilotettu.', - 'cms_page' => 'CMS sivu', - 'cms_page_comment' => 'Valitse sivu joka avataan, kun valikkokohtaa napsautetaan.', - 'reference_required' => 'Valikkokohteen viite on vaadittu.', - 'url_required' => 'URL on vaadittu', - 'cms_page_required' => 'Valitse CMS sivu', - 'display_tab' => 'Näkyvyys', - 'hidden' => 'Piilotettu', - 'hidden_comment' => 'Piilota tämän valikon kohde näkyvistä käyttäjän näkymässä.', - 'attributes_tab' => 'Attribuutti', - 'code' => 'Koodi', - 'code_comment' => 'Syötä valikkokohten koodi jos haluat käyttää sitä API:n kanssa.', - 'css_class' => 'CSS-luokka', - 'css_class_comment' => 'Anna tämän valikon kohteelle oma CSS-luokka ulkoasua varten.', - 'external_link' => 'Ulkoinen linkki', - 'external_link_comment' => 'Avaa linkki tämän valikon kohteesta uudessa ikkunassa.', - 'static_page' => 'Staattinen sivu', - 'all_static_pages' => 'Kaikki staattiset sivut' - ], - 'content' => [ - 'menu_label' => 'Sisältö', - 'saved' => 'Sisältö tallennettu onnistuneesti.', - 'cant_save_to_dir' => 'Sisältötiedostojen tallentaminen staatisen-sivujen hakemistoon ei ole sallittua.', - ], - 'sidebar' => [ - 'add' => 'Lisää', - 'search' => 'Hae...' - ], - 'object' => [ - 'invalid_type' => 'Tuntematon kohdetyyppi', - 'unauthorized_type' => 'Sinulla ei ole oikeuksia muokata kohdetta :type', - 'not_found' => 'Pyydettyä kohdetta ei löytynyt.', - ], - 'editor' => [ - 'title' => 'Otsikko', - 'new_title' => 'Uuden sivun otsikko', - 'content' => 'Sisältö', - 'url' => 'URL', - 'filename' => 'Tiedostonimi', - 'layout' => 'Ulkoasu', - 'description' => 'Kuvaus', - 'preview' => 'Esikatsele', - 'enter_fullscreen' => 'Kokoruudun tila', - 'exit_fullscreen' => 'Poistu kokoruudun tilasta', - 'hidden' => 'Piilotettu', - 'hidden_comment' => 'Piilotetut sivut ovat saatavilla ainoastaan hallintaan kirjautuneille.', - 'navigation_hidden' => 'Piilota navigaatiosta', - 'navigation_hidden_comment' => 'Käytä tätä valintaruutua piilottaaksesi tämä sivu automaattisesti generoiduista valikoista ja leivänmuruista.', - ], - 'snippet' => [ - 'partialtab' => 'Osat', - 'settings_popup_title' => 'Staattisten sivujen koodinpätkä', - 'code' => 'Osan koodi', - 'code_comment' => 'Syötä koodi, jotta tämä osio on käytettävissä Staattisten sivujen -lisäosassa.', - 'code_required' => 'Ole hyvä ja lisää koodin pätkä', - 'name' => 'Nimi', - 'name_comment' => 'Nimi näkyy osiolistassa Staattisten sivujen -sivupalkissa ja sivuilla kun osa on lisätty.', - 'name_required' => 'Pätkällä on oltava nimi', - 'no_records' => 'Osia ei löydy', - 'menu_label' => 'Osat', - 'properties' => 'Koodin pätkän ominaisuudet', - 'column_property' => 'Ominaisuuden otsikko', - 'title_required' => 'Ole hyvä ja lisää ominaisuuden otsikko', - 'type_required' => 'Valitse ominaisuuden tyyppi', - 'property_required' => 'Ominaisuuden nimi on pakollinen', - 'column_type' => 'Tyyppi', - 'column_type_placeholder' => 'Valitse', - 'column_code' => 'Koodi', - 'column_default' => 'Oletus', - 'column_options' => 'Vaihtoehdot', - 'column_type_string' => 'Merkkijono', - 'column_type_checkbox' => 'Valintaruutu', - 'column_type_dropdown' => 'Alasvetovalikko', - 'not_found' => 'Osaa pyydetyllä koodilla :code ei löytynyt teemasta.', - 'property_format_error' => 'Ominaisuuden koodin tulisi alkaa latinalaisella kirjaimella ja voi sisältää ainoastaan latinalaisia merkkejä ja kokonaislukuja', - 'invalid_option_key' => 'Kelvoton alasvetovalikon vaihtoehtoavain :key. Vaihtoehtojen avaimet voivat sisältää ainoastaan kokonaislukuja, latinalaisia merkkejä, ja merkkejä _ ja -', - ], - 'component' => [ - 'static_page_name' => 'Staattinen sivu', - 'static_page_description' => 'Näyttää staattisen sivun CMS ulkoasussa.', - 'static_page_use_content_name' => 'Käytä sivun sisältökenttää Use page content field', - 'static_page_use_content_description' => 'Jos valitsematta, sisältö -kohta ei tule näkyviin staattista sivua muokattaessa. Sivun sisältö määritetään vain paikkamerkkien ja muuttujien kautta.', - 'static_page_default_name' => 'Oletusulkoasu', - 'static_page_default_description' => 'Määrittelee tämän ulkoasun oletukseksi uusille sivuille', - 'static_page_child_layout_name' => 'Alasivun ulkoasu', - 'static_page_child_layout_description' => 'Oletusulkoasu kaikille uusille alasivuille', - 'static_menu_name' => 'Staattinen valikko', - 'static_menu_description' => 'Näyttää valikon CMS ulkoasussa.', - 'static_menu_code_name' => 'Valikko', - 'static_menu_code_description' => 'Määritä valikon koodi joka pitäisi näyttää.', - 'static_breadcrumbs_name' => 'Staattinen murupolku', - 'static_breadcrumbs_description' => 'Näyttää murupolun staattisella sivulla.', - 'child_pages_name' => 'Alasivut', - 'child_pages_description' => 'Näyttää listan nykyisen sivun alasivuista', - ] -]; diff --git a/lang/fr.json b/lang/fr.json new file mode 100644 index 00000000..1ef1c672 --- /dev/null +++ b/lang/fr.json @@ -0,0 +1,83 @@ +{ + "Pages": "Pages", + "Pages & menus features.": "Fonctionnalités de pages et menus statiques.", + "Manage static pages": "Gérer les pages statiques", + "Manage static menus": "Gérer les menus statiques", + "Manage static content": "Gérer le contenu statique", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Le format d’URL est invalide. L’URL doit commencer par un / et peut contenir des chiffres, des lettres et les symboles suivants : _-/.", + "This URL is already used by another page.": "Cette URL est déjà utilisée par une autre page.", + "Layouts not found": "Aucune maquette trouvée", + "The Code is required": "Le Code est requis", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Le format du Code est invalide. Le Code peut contenir des chiffres, des lettres et les symboles suivants : _-", + "Static page": "Page Statique", + "All static pages": "Toutes les pages statiques", + "Static menu": "Menu Statique", + "Static breadcrumbs": "Fil d’Ariane statique", + "Child pages": "Pages enfants", + "Outputs a static page in a CMS layout.": "Affiche une page statique dans une maquette du CMS.", + "Use page content field": "Affiche la section de contenu", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Si cette case n'est pas cochée, la section de contenu n'apparaîtra pas lors de la modification de la page statique. Le contenu de la page sera déterminé uniquement à l'aide d'espaces réservés et de variables.", + "Default layout": "Disposition par défaut", + "Defines this layout as the default for new pages": "Définit cette mise en page par défaut pour les nouvelles pages", + "Subpage layout": "Mise en page de la sous-page", + "The layout to use as the default for any new subpages": "La mise en page à utiliser par défaut pour les nouvelles sous-pages", + "Outputs a menu in a CMS layout.": "Affiche un menu dans une maquette du CMS.", + "Menu": "Menu", + "Specify a code of the menu the component should output.": "Spécifiez le code du menu que le composant devrait afficher.", + "Outputs breadcrumbs for a static page.": "Affiche le fil d’Ariane pour une page statique.", + "Displays a list of child pages for the current page": "Affiche une liste de pages enfants pour la page en cours", + "Static Pages": "Pages statiques", + "Menus": "Menus", + "Content": "Contenu", + "Add": "Ajouter", + "Refresh": "Actualiser", + "Page": "Page", + "Content block": "Bloc de contenu", + "New page": "Nouvelle page", + "New page title": "Nouveau titre de la page", + "New menu": "Nouveau menu", + "New content block": "Nouveau bloc de contenu", + "Title": "Titre", + "URL": "URL", + "File Name": "Nom du fichier", + "Layout": "Maquette", + "Hidden": "Caché", + "Hide in navigation": "Masquer dans la navigation", + "Description": "Description", + "Name": "Nom", + "Code": "Code", + "The Title is required.": "Le Titre est requis.", + "The URL is required.": "L’URL est requise.", + "The File Name is required.": "Le Nom du fichier est requis.", + "The Name is required.": "Le Nom est requis.", + "The Code is required.": "Le Code est requis.", + "Error loading page": "Erreur lors du chargement de la page", + "Error loading menu": "Erreur lors du chargement du menu", + "Error loading content block": "Erreur lors du chargement du bloc de contenu", + "Add item": "Ajouter un élément", + "Add subitem": "Ajouter un sous-élément", + "New menu item": "Nouvel élément du menu", + "Untitled": "Sans titre", + "Up": "Monter", + "Down": "Descendre", + "Indent": "Indenter", + "Outdent": "Désindenter", + "Delete": "Supprimer", + "No menu items yet. Use \"Add item\" in the toolbar.": "Aucun élément de menu pour le moment. Utilisez \"Ajouter un élément\" dans la barre d’outils.", + "Select a menu item to edit, or add a new one.": "Sélectionnez un élément de menu à modifier ou ajoutez-en un nouveau.", + "Custom Fields": "Champs personnalisés", + "Search all references...": "Rechercher toutes les références...", + "Edit Menu Item": "Modifier l’élément du menu", + "Move up": "Déplacer vers le haut", + "Move down": "Déplacer vers le bas", + "Apply": "Appliquer", + "Cancel": "Annuler", + "New Page": "Nouvelle page", + "New Menu": "Nouveau menu", + "New Content Block": "Nouveau bloc de contenu", + "Fields": "Champs", + "Preview": "Aperçu", + "Add subpage": "Ajouter une sous-page", + "You don't have permissions to manage :type documents.": "Vous n’avez pas les permissions pour gérer les documents :type.", + "Content files cannot be saved in the static pages directory.": "Les fichiers de contenu ne peuvent pas être enregistrés dans le répertoire des pages statiques." +} diff --git a/lang/fr/lang.php b/lang/fr/lang.php deleted file mode 100644 index 2d9b8af4..00000000 --- a/lang/fr/lang.php +++ /dev/null @@ -1,123 +0,0 @@ - [ - 'name' => 'Pages', - 'description' => 'Fonctionnalités de pages et menus statiques.', - ], - 'page' => [ - 'menu_label' => 'Pages', - 'template_title' => '%s Pages', - 'delete_confirmation' => 'Confirmez-vous la suppression des pages sélectionnées ? Les sous-pages seront également supprimées.', - 'no_records' => 'Aucune page trouvée', - 'delete_confirm_single' => 'Confirmez-vous la suppression de cette page ? Les sous-pages seront également supprimées.', - 'new' => 'Nouvelle page', - 'add_subpage' => 'Ajouter une sous-page', - 'invalid_url' => 'Le format d’URL est invalide. L’URL doit commencer par un / et peut contenir des chiffres, des lettres et les symboles suivants : _-/.', - 'url_not_unique' => 'Cette URL est déjà utilisée par une autre page.', - 'layout' => 'Maquette', - 'layouts_not_found' => 'Aucune maquette trouvée', - 'saved' => 'La page a été sauvegardée avec succès.', - 'tab' => 'Pages', - 'manage_pages' => 'Gérer les pages statiques', - 'manage_menus' => 'Gérer les menus statiques', - 'access_snippets' => 'Accès aux fragments', - 'manage_content' => 'Gérer le contenu statique', - ], - 'menu' => [ - 'menu_label' => 'Menus', - 'delete_confirmation' => 'Confirmez-vous la suppression des menus sélectionnés ?', - 'no_records' => 'Aucun menu trouvé', - 'new' => 'Nouveau menu', - 'new_name' => 'Nouveau menu', - 'new_code' => 'nouveau-menu', - 'delete_confirm_single' => 'Confirmez-vous la suppression de ce menu ?', - 'saved' => 'Le menu a été sauvegardé avec succès.', - 'name' => 'Nom', - 'code' => 'Code', - 'items' => 'Éléments du menu', - 'add_subitem' => 'Ajouter un élément', - 'code_required' => 'Le Code est requis', - 'invalid_code' => 'Le format du Code est invalide. Le Code peut contenir des chiffres, des lettres et les symboles suivants : _-', - ], - 'menuitem' => [ - 'title' => 'Titre', - 'editor_title' => 'Modifier l’élément du menu', - 'type' => 'Type', - 'allow_nested_items' => 'Autoriser les sous-éléments', - 'allow_nested_items_comment' => 'Les sous-éléments peuvent être générés dynamiquement par les pages statiques et certains des autres types d’élément', - 'url' => 'URL', - 'reference' => 'Référence', - 'search_placeholder' => 'Rechercher toutes les références...', - 'title_required' => 'Le Titre est requis', - 'unknown_type' => 'Type d’élément du menu inconnu', - 'unnamed' => 'Élément de menu sans nom', - 'add_item' => 'Ajouter un élément', - 'new_item' => 'Nouvel élément du menu', - 'replace' => 'Remplacer cet élément part ses sous-éléments générés', - 'replace_comment' => 'Utiliser cette case à cocher pour envoyer les sous-éléments générés au même niveau que cet élément. Cet élément sera lui-même masqué.', - 'cms_page' => 'Page CMS', - 'cms_page_comment' => 'Sélectionnez une page à ouvrir lors d’un clic sur cet élément du menu.', - 'reference_required' => 'La référence de l’élément du menu est requis.', - 'url_required' => 'L’URL est requise', - 'cms_page_required' => 'Sélectionnez une page CMS s’il vous plaît', - 'display_tab' => 'Affichage', - 'hidden' => 'Caché', - 'hidden_comment' => 'Empêcher ce menu d\'apparaître sur le site web.', - 'attributes_tab' => 'Attributs', - 'code' => 'Code', - 'code_comment' => 'Entrez le code de l’élément du menu si vous souhaitez y accéder via l’API.', - 'css_class' => 'Classe CSS', - 'css_class_comment' => 'Entrez un nom de classe CSS pour donner à cet élément de menu une apparence personnalisée.', - 'external_link' => 'Lien externe', - 'external_link_comment' => 'Ouvrir les liens pour ce menu dans une nouvelle fenêtre.', - 'static_page' => 'Page Statique', - 'all_static_pages' => 'Toutes les pages', - ], - 'content' => [ - 'menu_label' => 'Contenu', - 'cant_save_to_dir' => 'L’enregistrement des fichiers de contenu dans le répertoire des pages statiques n’est pas autorisé.', - ], - 'sidebar' => [ - 'add' => 'Ajouter', - ], - 'object' => [ - 'invalid_type' => 'Type d’objet inconnu', - 'not_found' => 'L’objet demandé n’a pas été trouvé.', - ], - 'editor' => [ - 'title' => 'Titre', - 'new_title' => 'Nouveau titre de la page', - 'content' => 'Contenu', - 'url' => 'URL', - 'filename' => 'Nom du fichier', - 'layout' => 'Maquette', - 'description' => 'Description', - 'preview' => 'Aperçu', - 'enter_fullscreen' => 'Activer le mode plein écran', - 'exit_fullscreen' => 'Annuler le mode plein écran', - 'hidden' => 'Caché', - 'hidden_comment' => 'Les pages cachées sont seulement accessibles aux administrateurs connectés.', - 'navigation_hidden' => 'Masquer dans la navigation', - 'navigation_hidden_comment' => 'Cochez cette case pour masquer cette page dans les menus et le fil d’ariane générés automatiquement.', - ], - 'snippet' => [ - 'menu_label' => 'Fragments', - ], - 'component' => [ - 'static_page_name' => 'Page Statique', - 'static_page_description' => 'Affiche une page statique dans une maquette du CMS.', - 'static_page_use_content_name' => 'Affiche la section de contenu', - 'static_page_use_content_description' => 'Si cette case n\'est pas cochée, la section de contenu n\'apparaîtra pas lors de la modification de la page statique. Le contenu de la page sera déterminé uniquement à l\'aide d\'espaces réservés et de variables.', - 'static_page_default_name' => 'Disposition par défault', - 'static_page_default_description' => 'Définit cette mise en page par défault pour les nouvelles pages', - 'static_page_child_layout_name' => 'Mise en page de la sous-page', - 'static_page_child_layout_description' => 'La mise en page à utiliser par défault pour les nouvelles sous-pages', - 'static_menu_name' => 'Menu Statique', - 'static_menu_description' => 'Affiche un menu dans une maquette du CMS.', - 'static_menu_code_name' => 'Menu', - 'static_menu_code_description' => 'Spécifiez le code du menu que le composant devrait afficher.', - 'static_breadcrumbs_name' => 'Breadcrumbs statique', - 'static_breadcrumbs_description' => 'Affiche l\' aide à la navigation dans une page statique.', - 'child_pages_name' => 'Pages enfants', - 'child_pages_description' => 'Affiche une liste de pages enfants pour la page en cours', - ], -]; diff --git a/lang/hu.json b/lang/hu.json new file mode 100644 index 00000000..59556671 --- /dev/null +++ b/lang/hu.json @@ -0,0 +1,83 @@ +{ + "Pages": "Oldalak", + "Pages & menus features.": "Oldalak és menük kezelése.", + "Manage static pages": "Oldalak kezelése", + "Manage static menus": "Menük kezelése", + "Manage static content": "Tartalom kezelése", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Érvénytelen a webcím formátuma. Perjellel kell kezdődnie, és számokat, latin betűket, valamint a következő szimbólumokat tartalmazhatja: _-/.", + "This URL is already used by another page.": "Egy másik oldal már használja ezt a webcímet.", + "Layouts not found": "Nincs létrehozva elrendezés", + "The Code is required": "A Kód kötelező", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Érvénytelen a kód formátuma. Csak számokat, latin betűket és a következő szimbólumokat tartalmazhatja: _-", + "Static page": "Statikus oldal", + "All static pages": "Összes oldal", + "Static menu": "Statikus menü", + "Static breadcrumbs": "Statikus kenyérmorzsa", + "Child pages": "Aloldalak", + "Outputs a static page in a CMS layout.": "Oldalak megjelenítése.", + "Use page content field": "Tartalom mező használata", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Ha nem engedélyezi ezt, akkor a tartalmi rész nem fog megjelenni az oldal szerkesztésénél. Az oldal tartalmát kizárólag a változók fogják meghatározni.", + "Default layout": "Alapértelmezett elrendezés", + "Defines this layout as the default for new pages": "Minden új oldal ezt az elrendezést fogja használni alapértelmezettként.", + "Subpage layout": "Aloldal elrendezés", + "The layout to use as the default for any new subpages": "Minden új aloldal ezt az elrendezést fogja használni alapértelmezettként.", + "Outputs a menu in a CMS layout.": "Menük megjelenítése.", + "Menu": "Menü", + "Specify a code of the menu the component should output.": "Speciális kód a megjelenő menünek.", + "Outputs breadcrumbs for a static page.": "Kenyérmorzsa megjelenítése.", + "Displays a list of child pages for the current page": "Megjeleníti az aktuális oldal aloldalainak listáját.", + "Static Pages": "Statikus oldalak", + "Menus": "Menük", + "Content": "Tartalom", + "Add": "Hozzáadás", + "Refresh": "Frissítés", + "Page": "Oldal", + "Content block": "Tartalomblokk", + "New page": "Új oldal", + "New page title": "Új oldal címe", + "New menu": "Új menü", + "New content block": "Új tartalomblokk", + "Title": "Cím", + "URL": "Webcím", + "File Name": "Fájlnév", + "Layout": "Elrendezés", + "Hidden": "Rejtett", + "Hide in navigation": "Elrejtés a navigációban", + "Description": "Leírás", + "Name": "Név", + "Code": "Kód", + "The Title is required.": "A cím megadása kötelező.", + "The URL is required.": "A webcím megadása kötelező.", + "The File Name is required.": "A fájlnév megadása kötelező.", + "The Name is required.": "A név megadása kötelező.", + "The Code is required.": "A Kód kötelező.", + "Error loading page": "Hiba az oldal betöltésekor", + "Error loading menu": "Hiba a menü betöltésekor", + "Error loading content block": "Hiba a tartalomblokk betöltésekor", + "Add item": "Menüpont hozzáadása", + "Add subitem": "Almenü hozzáadása", + "New menu item": "Új menüpont", + "Untitled": "Névtelen", + "Up": "Fel", + "Down": "Le", + "Indent": "Behúzás", + "Outdent": "Kihúzás", + "Delete": "Törlés", + "No menu items yet. Use \"Add item\" in the toolbar.": "Még nincsenek menüpontok. Használja a \"Menüpont hozzáadása\" gombot az eszköztáron.", + "Select a menu item to edit, or add a new one.": "Válasszon egy menüpontot a szerkesztéshez, vagy adjon hozzá egy újat.", + "Custom Fields": "Egyéni mezők", + "Search all references...": "Keresés...", + "Edit Menu Item": "Menüpont szerkesztése", + "Move up": "Mozgatás felfelé", + "Move down": "Mozgatás lefelé", + "Apply": "Alkalmaz", + "Cancel": "Mégse", + "New Page": "Új oldal", + "New Menu": "Új menü", + "New Content Block": "Új tartalomblokk", + "Fields": "Mezők", + "Preview": "Előnézet", + "Add subpage": "Aloldal hozzáadása", + "You don't have permissions to manage :type documents.": "Nem jogosult a következő dokumentumok kezelésére: :type", + "Content files cannot be saved in the static pages directory.": "A tartalomfájlok nem menthetők a statikus oldalak könyvtárába." +} diff --git a/lang/hu/lang.php b/lang/hu/lang.php deleted file mode 100644 index 6803ec79..00000000 --- a/lang/hu/lang.php +++ /dev/null @@ -1,125 +0,0 @@ - [ - 'name' => 'Oldalak', - 'description' => 'Oldalak, menük, tartalmak és kódrészletek menedzselése.', - ], - 'page' => [ - 'menu_label' => 'Oldalak', - 'template_title' => '%s Oldalak', - 'delete_confirmation' => 'Valóban törölni akarja a kijelölt oldalakat és azok aloldalait?', - 'no_records' => 'Nincs létrehozva oldal', - 'delete_confirm_single' => 'Valóban törölni akarja ezt az oldalt és aloldalait?', - 'new' => 'Új oldal', - 'add_subpage' => 'Aloldal hozzáadása', - 'invalid_url' => 'Érvénytelen a webcím formátuma. Perjellel kell kezdődnie, és számokat, latin betűket, valamint a következő szimbólumokat tartalmazhatja: _-/.', - 'url_not_unique' => 'Egy másik oldal már használja ezt a webcímet.', - 'layout' => 'Elrendezés', - 'layouts_not_found' => 'Nincs létrehozva elrendezés.', - 'saved' => 'Az oldal mentése sikerült.', - 'tab' => 'Oldalak', - 'manage_pages' => 'Oldalak kezelése', - 'manage_menus' => 'Menük kezelése', - 'access_snippets' => 'Kódrészletek kezelése', - 'manage_content' => 'Tartalom kezelése', - ], - 'menu' => [ - 'menu_label' => 'Menük', - 'delete_confirmation' => 'Valóban törölni akarja a kijelölt menüket?', - 'no_records' => 'Nincs létrehozva menü', - 'new' => 'Új menü', - 'new_name' => 'Új menü', - 'new_code' => 'uj-menu', - 'delete_confirm_single' => 'Valóban törölni akarja ezt a menüt?', - 'saved' => 'A menü mentése sikerült.', - 'name' => 'Név', - 'code' => 'Kód', - 'items' => 'Menüpont', - 'add_subitem' => 'Almenü hozzáadása', - 'code_required' => 'A Kód kötelező', - 'invalid_code' => 'Érvénytelen a kód formátuma. Csak számokat, latin betűket és a következő szimbólumokat tartalmazhatja: _-', - ], - 'menuitem' => [ - 'title' => 'Cím', - 'editor_title' => 'Menüpont szerkesztése', - 'type' => 'Típus', - 'allow_nested_items' => 'Beágyazott menüpontok engedélyezése', - 'allow_nested_items_comment' => 'A beágyazott menüpontokat az oldal és néhány más menüpont típus dinamikusan generálhatja', - 'url' => 'Webcím', - 'reference' => 'Hivatkozás', - 'search_placeholder' => 'Keresés...', - 'title_required' => 'A cím megadása kötelező', - 'unknown_type' => 'Ismeretlen menüponttípus', - 'unnamed' => 'Névtelen menüpont', - 'add_item' => 'Menüpont hozzáadása', - 'new_item' => 'Új menüpont', - 'replace' => 'A menüpont kicserélése a generált gyermekeire', - 'replace_comment' => 'Ennek a jelölőnégyzetnek a használatával viheti a generált menüpontokat az ezen menüpont által azonos szintre. Maga ez a menüpont rejtett marad.', - 'cms_page' => 'Oldal', - 'cms_page_comment' => 'Válassza ki a menüre kattintáskor megnyitni kívánt oldalt.', - 'reference_required' => 'A menüpont hivatkozás kitöltése kötelező.', - 'url_required' => 'A webcím megadása kötelező', - 'cms_page_required' => 'Válasszon egy oldalt', - 'display_tab' => 'Megjelenés', - 'hidden' => 'Rejtett', - 'hidden_comment' => 'Nem jelenik meg a felhasználói felületen.', - 'attributes_tab' => 'Tulajdonságok', - 'code' => 'Kód', - 'code_comment' => 'Az API eléréshez szükséges egyedi azonosító.', - 'css_class' => 'CSS osztály', - 'css_class_comment' => 'Egyedi megjelenés esetén szükséges megadni.', - 'external_link' => 'Külső hivatkozás', - 'external_link_comment' => 'A link új ablakban fog megjelenni.', - 'static_page' => 'Oldalak', - 'all_static_pages' => 'Összes oldal', - ], - 'content' => [ - 'menu_label' => 'Tartalom', - 'saved' => 'A tartalom mentése sikerült.', - 'cant_save_to_dir' => 'A fájlok mentése a "static-pages" könyvtárba nem engedélyezett.', - ], - 'sidebar' => [ - 'add' => 'Hozzáadás', - ], - 'object' => [ - 'invalid_type' => 'Ismeretlen objektumtípus', - 'unauthorized_type' => 'Nem jogosult a következő objektum(ok) kezelésére: :type', - 'not_found' => 'A kért objektum nem található.', - ], - 'editor' => [ - 'title' => 'Cím', - 'new_title' => 'Új oldal címe', - 'content' => 'Tartalom', - 'url' => 'Webcím', - 'filename' => 'Fájlnév', - 'layout' => 'Elrendezés', - 'description' => 'Leírás', - 'preview' => 'Előnézet', - 'enter_fullscreen' => 'Váltás teljes képernyős módra', - 'exit_fullscreen' => 'Kilépés a teljes képernyős módból', - 'hidden' => 'Rejtett', - 'hidden_comment' => 'A rejtett oldalakhoz csak a bejelentkezett kiszolgáló oldali felhasználók férhetnek hozzá.', - 'navigation_hidden' => 'Elrejtés a navigációban', - 'navigation_hidden_comment' => 'Jelölje be ezt a jelölőnégyzetet ennek a oldalnak az automatikusan generált menükből és útkövetésekből való elrejtéséhez.', - ], - 'snippet' => [ - 'menu_label' => 'Kódrészletek', - ], - 'component' => [ - 'static_page_name' => 'Statikus oldal', - 'static_page_description' => 'Oldalak megjelenítése.', - 'static_page_use_content_name' => 'Tartalom mező használata', - 'static_page_use_content_description' => 'Ha nem engedélyezi ezt, akkor a tartalmi rész nem fog megjelenni az oldal szerkesztésénél. Az oldal tartalmát kizárólag a változók fogják meghatározni.', - 'static_page_default_name' => 'Alapértelmezett elrendezés', - 'static_page_default_description' => 'Minden új oldal ezt az elrendezést fogja hasznáni alapértelmezettként.', - 'static_page_child_layout_name' => 'Aloldal elrendezés', - 'static_page_child_layout_description' => 'Minden új aloldal ezt az elrendezést fogja használni alapértelmezettként.', - 'static_menu_name' => 'Statikus menü', - 'static_menu_description' => 'Menük megjelenítése.', - 'static_menu_code_name' => 'Menü', - 'static_menu_code_description' => 'Speciális kód a megjelenő menünek.', - 'static_breadcrumbs_name' => 'Statikus kenyérmorzsa', - 'static_breadcrumbs_description' => 'Kenyérmorzsa megjelenítése.', - 'child_pages_name' => 'Aloldalak', - 'child_pages_description' => 'Megjeleníti az aktuális oldal aloldalainak listáját.', - ], -]; diff --git a/lang/it.json b/lang/it.json new file mode 100644 index 00000000..828b725f --- /dev/null +++ b/lang/it.json @@ -0,0 +1,83 @@ +{ + "Pages": "Pagine", + "Pages & menus features.": "Funzionalità di pagine & menu.", + "Manage static pages": "Gestisci pagine", + "Manage static menus": "Gestisci menu", + "Manage static content": "Gestisci contenuti", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Formato dell'URL non valido. L'URL deve iniziare con una barra e può contenere numeri, lettere latine e i seguenti simboli: _-/.", + "This URL is already used by another page.": "L'URL è già utilizzato da un'altra pagina.", + "Layouts not found": "Nessun layout trovato", + "The Code is required": "Il Codice è obbligatorio", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Formato del Codice non valido. Il Codice può contenere numeri, lettere latine e i seguenti simboli: _-", + "Static page": "Pagina statica", + "All static pages": "Tutte le pagine", + "Static menu": "Menu statico", + "Static breadcrumbs": "Breadcrumb statici", + "Child pages": "Pagine figlie", + "Outputs a static page in a CMS layout.": "Mostra una pagina statica in un layout del CMS.", + "Use page content field": "Usa il campo contenuto della pagina", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Se deselezionato, la sezione contenuto non apparirà durante la modifica della pagina statica. Il contenuto della pagina sarà determinato esclusivamente tramite segnaposto e variabili.", + "Default layout": "Layout predefinito", + "Defines this layout as the default for new pages": "Definisce questo layout come predefinito per le nuove pagine", + "Subpage layout": "Layout delle sottopagine", + "The layout to use as the default for any new subpages": "Il layout da usare come predefinito per le nuove sottopagine", + "Outputs a menu in a CMS layout.": "Mostra un menu in un layout del CMS.", + "Menu": "Menu", + "Specify a code of the menu the component should output.": "Specifica il codice del menu che il componente deve mostrare.", + "Outputs breadcrumbs for a static page.": "Mostra i breadcrumb per una pagina statica.", + "Displays a list of child pages for the current page": "Mostra un elenco di pagine figlie della pagina corrente", + "Static Pages": "Pagine statiche", + "Menus": "Menu", + "Content": "Contenuti", + "Add": "Aggiungi", + "Refresh": "Aggiorna", + "Page": "Pagina", + "Content block": "Blocco di contenuto", + "New page": "Nuova pagina", + "New page title": "Titolo nuova pagina", + "New menu": "Nuovo menu", + "New content block": "Nuovo blocco di contenuto", + "Title": "Titolo", + "URL": "URL", + "File Name": "Nome file", + "Layout": "Layout", + "Hidden": "Nascosto", + "Hide in navigation": "Nascondi dalla navigazione", + "Description": "Descrizione", + "Name": "Nome", + "Code": "Codice", + "The Title is required.": "Il Titolo è obbligatorio.", + "The URL is required.": "L'URL è obbligatorio.", + "The File Name is required.": "Il Nome file è obbligatorio.", + "The Name is required.": "Il Nome è obbligatorio.", + "The Code is required.": "Il Codice è obbligatorio.", + "Error loading page": "Errore durante il caricamento della pagina", + "Error loading menu": "Errore durante il caricamento del menu", + "Error loading content block": "Errore durante il caricamento del blocco di contenuto", + "Add item": "Aggiungi elemento", + "Add subitem": "Aggiungi sottomenu", + "New menu item": "Nuova voce di menu", + "Untitled": "Senza titolo", + "Up": "Su", + "Down": "Giù", + "Indent": "Aumenta rientro", + "Outdent": "Riduci rientro", + "Delete": "Elimina", + "No menu items yet. Use \"Add item\" in the toolbar.": "Nessuna voce di menu presente. Usa \"Aggiungi elemento\" nella barra degli strumenti.", + "Select a menu item to edit, or add a new one.": "Seleziona una voce di menu da modificare o aggiungine una nuova.", + "Custom Fields": "Campi personalizzati", + "Search all references...": "Cerca in tutti i riferimenti...", + "Edit Menu Item": "Modifica voce di menu", + "Move up": "Sposta su", + "Move down": "Sposta giù", + "Apply": "Applica", + "Cancel": "Annulla", + "New Page": "Nuova pagina", + "New Menu": "Nuovo menu", + "New Content Block": "Nuovo blocco di contenuto", + "Fields": "Campi", + "Preview": "Anteprima", + "Add subpage": "Aggiungi sottopagina", + "You don't have permissions to manage :type documents.": "Non hai i permessi per gestire i documenti di tipo :type.", + "Content files cannot be saved in the static pages directory.": "I file di contenuto non possono essere salvati nella directory delle pagine statiche." +} diff --git a/lang/it/lang.php b/lang/it/lang.php deleted file mode 100644 index 2ba69071..00000000 --- a/lang/it/lang.php +++ /dev/null @@ -1,96 +0,0 @@ - [ - 'name' => 'Pages', - 'description' => 'Funzionalità di pagine & menu.', - ], - 'page' => [ - 'menu_label' => 'Pagine', - 'template_title' => '%s Pagine', - 'delete_confirmation' => 'Vuoi davvero eliminare le pagine selezionate? L\'operazione cancellerà anche le sottopagine, se presenti.', - 'no_records' => 'Nessuna pagina trovata', - 'delete_confirm_single' => 'Vuoi davvero eliminare questa pagina? L\'operazione cancellerà anche le sottopagine, se presenti.', - 'new' => 'Nuova pagina', - 'add_subpage' => 'Aggiungi sottopagina', - 'invalid_url' => 'Formato dell\'URL non valido. L\'URL deve iniziare con una barra e può contenere numeri, lettere latine e i seguenti simboli: _-/.', - 'url_not_unique' => 'L\'URL è già utilizzato da un\'altra pagina.', - 'layout' => 'Layout', - 'layouts_not_found' => 'Layouts non trovato', - 'saved' => 'Pagina salvata con successo.', - 'tab' => 'Pagine', - 'manage_pages' => 'Gestisci pagine', - 'manage_menus' => 'Gestisci menu', - 'access_snippets' => 'Accedi agli snippet', - 'manage_content' => 'Gestisci contenuti', - ], - 'menu' => [ - 'menu_label' => 'Menu', - 'delete_confirmation' => 'Vuoi davvero eliminare i menu selezionati?', - 'no_records' => 'Nessun menu trovato', - 'new' => 'Nuovo menu', - 'new_name' => 'Nuovo menu', - 'new_code' => 'nuovo-menu', - 'delete_confirm_single' => 'Vuoi davvero eliminare questo menu?', - 'saved' => 'Menu salvato con successo.', - 'name' => 'Nome', - 'code' => 'Codice', - 'items' => 'Voci di menu', - 'add_subitem' => 'Aggiungi sottomenu', - 'code_required' => 'Il Codice è obbligatorio', - 'invalid_code' => 'Formato del Codice non valido. Il Codice può contenere numeri, lettere latine e i seguenti simboli: _-', - ], - 'menuitem' => [ - 'title' => 'Titolo', - 'editor_title' => 'Modifica voce di menu', - 'type' => 'Tipo', - 'allow_nested_items' => 'Consenti elementi nidificati', - 'allow_nested_items_comment' => 'Gli elementi nidificati possono essere generati dinamicamente dalle pagine e altre tipologie di elementi', - 'url' => 'URL', - 'reference' => 'Riferimento', - 'title_required' => 'Il Titolo è obbligatorio', - 'unknown_type' => 'Tipologia di menu sconosciuta', - 'unnamed' => 'Voce di menu senza nome', - 'add_item' => 'Aggiungi elemento', - 'new_item' => 'Nuova voce di menu', - 'replace' => 'Sostituisci questo elemento con i figli generati', - 'replace_comment' => 'Usa questa checkbox per inserire le voci di menu generate allo stesso livello di questo elemento. Questa voce verrà nascosta.', - 'cms_page' => 'Pagine CMS', - 'cms_page_comment' => 'Seleziona una pagina del CMS da aprire quando viene selezionata la voce di menu.', - 'reference_required' => 'Il riferimento della voce di menu è obbligatorio.', - 'url_required' => 'L\'URL è obbligatorio', - 'cms_page_required' => 'Seleziona una pagina CMS', - 'code' => 'Codice', - 'code_comment' => 'Inserisci il codice della voce di menu se vuoi accedervi con l\'API.', - 'static_page' => 'Pagine', - 'all_static_pages' => 'Tutte le pagine', - ], - 'content' => [ - 'menu_label' => 'Contenuti', - 'cant_save_to_dir' => 'Salvataggio dei file di contenuto nella directory static-pages non consentito.', - ], - 'sidebar' => [ - 'add' => 'Aggiungi', - ], - 'object' => [ - 'invalid_type' => 'Tipo di oggetto sconosciuto', - 'not_found' => 'Oggetto richiesto non trovato.', - ], - 'editor' => [ - 'title' => 'Titolo', - 'new_title' => 'Titolo nuova pagina', - 'content' => 'Contenuto', - 'url' => 'URL', - 'filename' => 'Nome file', - 'layout' => 'Layout', - 'description' => 'Descrizione', - 'preview' => 'Anteprima', - 'enter_fullscreen' => 'Abilita visualizzazione a schermo intero', - 'exit_fullscreen' => 'Esci dalla visualizzazione a schermo intero', - 'hidden' => 'Nascosto', - 'hidden_comment' => 'Le pagine nascoste sono accessibili soltanto dagli utenti che hanno effettuato l\'accesso al pannello di controllo.', - 'navigation_hidden' => 'Nascondi dalla navigazione', - 'navigation_hidden_comment' => 'Seleziona questa checkbox per nascondere questa pagina dai menu e dalle barre di navigazione generate automaticamente.', - ], - 'snippet' => [ - 'menu_label' => 'Snippet', - ], -]; diff --git a/lang/lv.json b/lang/lv.json new file mode 100644 index 00000000..bc78977b --- /dev/null +++ b/lang/lv.json @@ -0,0 +1,83 @@ +{ + "Pages": "Lapas", + "Pages & menus features.": "Lapu un izvēļņu funkcijas.", + "Manage static pages": "Pieeja labot statiskās lapas", + "Manage static menus": "Pieeja labot statiskās izvēlnes", + "Manage static content": "Pieeja labot statisko saturu", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Nekorekts saites formāts. Saitei vajadzētu sākties ar slīpsvītru un tā var saturēt ciparus, latīnu alfabēta burtus, slīpsvītras un sekojošos simbolus: _-/.", + "This URL is already used by another page.": "Šādu saiti izmanto jau kāda cita lapa.", + "Layouts not found": "Izkārtojumi netika atrasti", + "The Code is required": "Kods ir obligāts lauks", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Nepareizs koda formāts. Kods var saturēt ciparus, latīnu alfabēta burtus un sekojošos simbolus: _-", + "Static page": "Statiska lapa", + "All static pages": "Visas statiskās lapas", + "Static menu": "Statiska izvēlne", + "Static breadcrumbs": "Statisks navigācijas ceļš", + "Child pages": "Apakšlapas", + "Outputs a static page in a CMS layout.": "Izvada statisku lapu CMS izkārtojumā.", + "Use page content field": "Izmantot lapas satura lauku", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Ja nav atzīmēts, satura sadaļa neparādīsies, rediģējot statisko lapu. Lapas saturs tiks noteikts tikai ar vietturiem un mainīgajiem.", + "Default layout": "Noklusējuma izkārtojums", + "Defines this layout as the default for new pages": "Definē šo izkārtojumu kā noklusējumu jaunām lapām", + "Subpage layout": "Apakšlapas izkārtojums", + "The layout to use as the default for any new subpages": "Izkārtojums, ko izmantot kā noklusējumu jaunām apakšlapām", + "Outputs a menu in a CMS layout.": "Izvada izvēlni CMS izkārtojumā.", + "Menu": "Izvēlne", + "Specify a code of the menu the component should output.": "Norādi izvēlnes kodu, kuru komponentei izvadīt.", + "Outputs breadcrumbs for a static page.": "Izvada navigācijas ceļu statiskai lapai.", + "Displays a list of child pages for the current page": "Attēlo pašreizējās lapas apakšlapu sarakstu", + "Static Pages": "Statiskās lapas", + "Menus": "Izvēlnes", + "Content": "Saturs", + "Add": "Pievienot", + "Refresh": "Atsvaidzināt", + "Page": "Lapa", + "Content block": "Satura bloks", + "New page": "Jauna lapa", + "New page title": "Jaunās lapas nosaukums", + "New menu": "Jauna izvēlne", + "New content block": "Jauns satura bloks", + "Title": "Nosaukums", + "URL": "Saite", + "File Name": "Faila nosaukums", + "Layout": "Izkārtojums", + "Hidden": "Paslēpts", + "Hide in navigation": "Paslēpt navigācijā", + "Description": "Skaidrojums", + "Name": "Vārds", + "Code": "Kods", + "The Title is required.": "Nosaukuma lauks ir obligāts.", + "The URL is required.": "Saite ir obligāti jāievada.", + "The File Name is required.": "Faila nosaukums ir obligāts.", + "The Name is required.": "Vārds ir obligāts.", + "The Code is required.": "Kods ir obligāts lauks.", + "Error loading page": "Kļūda ielādējot lapu", + "Error loading menu": "Kļūda ielādējot izvēlni", + "Error loading content block": "Kļūda ielādējot satura bloku", + "Add item": "Pievienot priekšmetu", + "Add subitem": "Pievienot apakšpriekšmetu", + "New menu item": "Jauns izvēlnes priekšmets", + "Untitled": "Bez nosaukuma", + "Up": "Uz augšu", + "Down": "Uz leju", + "Indent": "Palielināt atkāpi", + "Outdent": "Samazināt atkāpi", + "Delete": "Dzēst", + "No menu items yet. Use \"Add item\" in the toolbar.": "Vēl nav izvēlnes priekšmetu. Izmanto \"Pievienot priekšmetu\" rīkjoslā.", + "Select a menu item to edit, or add a new one.": "Izvēlies izvēlnes priekšmetu, ko labot, vai pievieno jaunu.", + "Custom Fields": "Pielāgotie lauki", + "Search all references...": "Meklēt visās atsaucēs...", + "Edit Menu Item": "Labot izvēlnes priekšmetu", + "Move up": "Pārvietot uz augšu", + "Move down": "Pārvietot uz leju", + "Apply": "Pielietot", + "Cancel": "Atcelt", + "New Page": "Jauna lapa", + "New Menu": "Jauna izvēlne", + "New Content Block": "Jauns satura bloks", + "Fields": "Lauki", + "Preview": "Priekšskats", + "Add subpage": "Pievienot apakšlapu", + "You don't have permissions to manage :type documents.": "Tev nav tiesību pārvaldīt :type dokumentus.", + "Content files cannot be saved in the static pages directory.": "Satura failu saglabāšana statisko lapu direktorijā nav atļauta." +} diff --git a/lang/lv/lang.php b/lang/lv/lang.php deleted file mode 100644 index 2a6dfeb9..00000000 --- a/lang/lv/lang.php +++ /dev/null @@ -1,101 +0,0 @@ - [ - 'name' => 'Lapas', - 'description' => 'Lapu un izvēļņu funkcijas.', - ], - 'page' => [ - 'menu_label' => 'Lapas', - 'template_title' => '%s Lapas', - 'delete_confirmation' => 'Vai tu tiešām vēlies dzēst izvēlētās lapas? Šī operācija izdzēsīs arī apakšlapas (ja tādas ir).', - 'no_records' => 'Neviena lapa netika atrasta', - 'delete_confirm_single' => 'Vai tu tiešām vēlies dzēst izvēlēto lapu? Šī operācija izdzēsīs arī apakšlapas (ja tādas ir).', - 'new' => 'Jauna lapa', - 'add_subpage' => 'Pievienot apakšlapu', - 'invalid_url' => 'Nekorekts saites formāts. Saitei vajadzētu sākties ar slīpsvītru un tā var saturēt ciparus, latīnu alfabēta burtus, slīpsvītras un sekojošos sibolus: _-/.', - 'url_not_unique' => 'Šādu saiti izmanto jau kāda cita lapa.', - 'layout' => 'Izkārtojums', - 'layouts_not_found' => 'Izkārtojumi netika atrasti', - 'saved' => 'Lapa tika veiksmīgi saglabāta.', - 'tab' => 'Lapas', - 'manage_pages' => 'Pieeja labot statiskās lapas', - 'manage_menus' => 'Pieeja labot statiskās izvēlnes', - 'access_snippets' => 'Pieeja koda fragmentiem', - 'manage_content' => 'Pieeja labot statisko saturu', - ], - 'menu' => [ - 'menu_label' => 'Izvēlnes', - 'delete_confirmation' => 'Vai tu tiešām vēlies dzēst izvēlētās izvēlnes?', - 'no_records' => 'Izvēlnes netika atrastas', - 'new' => 'Jauna izvēlne', - 'new_name' => 'Jauna izvēlne', - 'new_code' => 'jauna-izvelne', - 'delete_confirm_single' => 'Vai tu tiešām vēlies dzēst šo izvēlni?', - 'saved' => 'Izvēlne tika veiksmīgi saglabāta', - 'name' => 'Vārds', - 'code' => 'Kods', - 'items' => 'Izvēlnes priekšmeti', - 'add_subitem' => 'Pievienot apakšpriekšmetu', - 'code_required' => 'Kods ir obligāts lauks.', - 'invalid_code' => 'Nepreaizs koda formāts. Kods var saturēt ciparus, latīnu alfabēta burtus un sekojošos simbolus: _-', - ], - 'menuitem' => [ - 'title' => 'Nosaukums', - 'editor_title' => 'Labot izvēlnes priekšmetu', - 'type' => 'Tips', - 'allow_nested_items' => 'Atļaut iegultos priekšmetus', - 'allow_nested_items_comment' => 'Iegultie priekšmeti var tikt dinamiski ģenerēti', - 'url' => 'Saite', - 'reference' => 'Atsauce', - 'title_required' => 'Nosaukuma lauks ir obligāts', - 'unknown_type' => 'Nezināms izvēlnes tips', - 'unnamed' => 'Izvēlnes priekšmets bez nosaukums', - 'add_item' => 'Pievienot Priekšmetu', - 'new_item' => 'Jauns izvēlnes priekšmets', - 'replace' => 'Aizvietot šo priekšmetu ar tā ģenerētajiem bērniem', - 'cms_page' => 'CMS lapa', - 'cms_page_comment' => 'Izvēlies lapu, kas atvērsies, kad tiks noklikšķiāts uz šī izvēlnes priekšmeta.', - 'reference_required' => 'Izvēlnes priekšmeta atsauce ir obligāts lauks.', - 'url_required' => 'Saite ir obligāti jāievada', - 'cms_page_required' => 'Lūdzu, izvēlies CMS lapu', - 'code' => 'Kods', - 'code_comment' => 'Ievadi izvēlnes priekšmeta kodu, ja tam vēlies piekļūt izmantojot API.', - 'static_page' => 'Statiska lapa', - 'all_static_pages' => 'Visas statiskās lapas', - ], - 'content' => [ - 'menu_label' => 'Saturs', - 'cant_save_to_dir' => 'Satura failu saglabāšana statisko lapu direktorijā nav atļauta.S', - ], - 'sidebar' => [ - 'add' => 'Pievienot', - ], - 'object' => [ - 'invalid_type' => 'Nezināms objekta tips', - 'not_found' => 'Pieprasītais objekts netika atrasts.', - ], - 'editor' => [ - 'title' => 'Nosaukums', - 'new_title' => 'Jaunās lapas nosaukums', - 'content' => 'Saturs', - 'url' => 'Saite', - 'filename' => 'Faila nosaukums', - 'layout' => 'Izkārtojums', - 'description' => 'Skaidrojums', - 'preview' => 'Priekšskats', - 'enter_fullscreen' => 'Atvērt pilnekrāna režīmu', - 'exit_fullscreen' => 'Aizvērt pilnekrāna režīmu', - 'hidden' => 'Paslēpts', - 'hidden_comment' => 'Paslēptās lapas varēs redzēt tikai ielogojušies back-end lietotāji.', - 'navigation_hidden' => 'Paslēpt navigācijā', - 'navigation_hidden_comment' => 'Atķeksē šo kasti, lai automātiski ģenerētu izvēlnes un breadcrumbus.', - ], - 'snippet' => [ - 'menu_label' => 'Koda fragmenti', - ], - 'component' => [ - 'static_page_description' => 'Izvada statisku lapu CMS iegultnē.', - 'static_menu_description' => 'Izvada izvēlni CMS iegultnē.', - 'static_menu_menu_code' => 'Specificē komponentes kodu, ko izvadīt', - 'static_breadcrumbs_description' => 'Izvada breadcrumbus CMS iegultnē.', - ], -]; diff --git a/lang/nb-no.json b/lang/nb-no.json new file mode 100644 index 00000000..f14ac4d1 --- /dev/null +++ b/lang/nb-no.json @@ -0,0 +1,83 @@ +{ + "Pages": "Sider", + "Pages & menus features.": "Side- og menyfunksjoner.", + "Manage static pages": "Administrer statiske sider", + "Manage static menus": "Administrer statiske menyer", + "Manage static content": "Administrer statisk innhold", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Ugyldig URL-format. URL-en skal starte med en skråstrek og kan bare inneholde tall, latinske bokstaver og følgende symboler: _-/.", + "This URL is already used by another page.": "URL-en er allerede i bruk av en annen side.", + "Layouts not found": "Ingen layouts funnet", + "The Code is required": "En kode kreves", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Ugyldig kode-format. Koden kan inneholde tall, latinske bokstaver og følgende symboler: _-", + "Static page": "Statisk side", + "All static pages": "Alle statiske sider", + "Static menu": "Statisk meny", + "Static breadcrumbs": "Statiske brødsmuler", + "Child pages": "Undersider", + "Outputs a static page in a CMS layout.": "Viser en statisk side i en CMS-layout.", + "Use page content field": "Bruk sidens innholdsfelt", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Hvis ikke avhuket, vil innholdsseksjonen ikke vises når den statiske siden redigeres. Sideinnholdet bestemmes utelukkende av plassholdere og variabler.", + "Default layout": "Standard layout", + "Defines this layout as the default for new pages": "Definerer denne layouten som standard for nye sider", + "Subpage layout": "Layout for undersider", + "The layout to use as the default for any new subpages": "Layouten som brukes som standard for nye undersider", + "Outputs a menu in a CMS layout.": "Viser en meny i en CMS-layout.", + "Menu": "Meny", + "Specify a code of the menu the component should output.": "Angi koden til menyen komponenten skal vise.", + "Outputs breadcrumbs for a static page.": "Viser brødsmuler for en statisk side.", + "Displays a list of child pages for the current page": "Viser en liste over undersider for gjeldende side", + "Static Pages": "Statiske sider", + "Menus": "Menyer", + "Content": "Innhold", + "Add": "Legg til", + "Refresh": "Oppdater", + "Page": "Side", + "Content block": "Innholdsblokk", + "New page": "Ny side", + "New page title": "Tittel på siden", + "New menu": "Ny meny", + "New content block": "Ny innholdsblokk", + "Title": "Tittel", + "URL": "URL", + "File Name": "Filnavn", + "Layout": "Layout", + "Hidden": "Skjult", + "Hide in navigation": "Gjem i menyer", + "Description": "Beskrivelse", + "Name": "Navn", + "Code": "Kode", + "The Title is required.": "Tittel kreves.", + "The URL is required.": "En URL kreves.", + "The File Name is required.": "Filnavn kreves.", + "The Name is required.": "Navn kreves.", + "The Code is required.": "En kode kreves.", + "Error loading page": "Feil ved lasting av side", + "Error loading menu": "Feil ved lasting av meny", + "Error loading content block": "Feil ved lasting av innholdsblokk", + "Add item": "Legg til element", + "Add subitem": "Nytt underelement", + "New menu item": "Nytt element", + "Untitled": "Uten tittel", + "Up": "Opp", + "Down": "Ned", + "Indent": "Rykk inn", + "Outdent": "Rykk ut", + "Delete": "Slett", + "No menu items yet. Use \"Add item\" in the toolbar.": "Ingen menyelementer ennå. Bruk \"Legg til element\" i verktøylinjen.", + "Select a menu item to edit, or add a new one.": "Velg et menyelement for å redigere, eller legg til et nytt.", + "Custom Fields": "Egendefinerte felter", + "Search all references...": "Søk i alle referanser...", + "Edit Menu Item": "Endre element", + "Move up": "Flytt opp", + "Move down": "Flytt ned", + "Apply": "Bruk", + "Cancel": "Avbryt", + "New Page": "Ny side", + "New Menu": "Ny meny", + "New Content Block": "Ny innholdsblokk", + "Fields": "Felter", + "Preview": "Forhåndsvis", + "Add subpage": "Ny underside", + "You don't have permissions to manage :type documents.": "Du har ikke tillatelse til å administrere :type-dokumenter.", + "Content files cannot be saved in the static pages directory.": "Innholdsfiler kan ikke lagres i mappen for statiske sider." +} diff --git a/lang/nb-no/lang.php b/lang/nb-no/lang.php deleted file mode 100644 index 43a246f1..00000000 --- a/lang/nb-no/lang.php +++ /dev/null @@ -1,93 +0,0 @@ - [ - 'name' => 'Sider', - 'description' => 'Side- og menyfunksjoner.', - ], - 'page' => [ - 'menu_label' => 'Sider', - 'template_title' => '%s Sider', - 'delete_confirmation' => 'Vil du virkelig slette de valgte sidene? Hvis siden har undersider, blir de også slettet.', - 'no_records' => 'Ingen sider funnet', - 'delete_confirm_single' => 'Vil du virkelig slette denne siden? Hvis siden har undersider, blir de også slettet.', - 'new' => 'Ny side', - 'add_subpage' => 'Ny underside', - 'invalid_url' => 'Ugyldig URL-format. URL-en skal starte med en skråstrek og kan bare inneholde tall, latinske bokstaver og følgende symboler: _-/.', - 'url_not_unique' => 'URL-en er allerede i bruk av en annen side.', - 'layout' => 'Layout', - 'layouts_not_found' => 'Ingen layouts funnet', - 'saved' => 'Siden har blitt lagret.', - 'manage_pages' => 'Administrer statiske sider', - 'manage_menus' => 'Administrer statiske menyer', - 'access_snippets' => 'Tilgang til snippets', - 'manage_content' => 'Administrer statisk innhold', - ], - 'menu' => [ - 'menu_label' => 'Menyer', - 'delete_confirmation' => 'Vil du virkelig slette valgte menyer?', - 'no_records' => 'Ingen elementer funnet', - 'new' => 'Ny meny', - 'new_name' => 'Ny meny', - 'new_code' => 'new-menu', - 'delete_confirm_single' => 'Vil du virkelig slette denne menyen?', - 'saved' => 'Menyen har blitt lagret.', - 'name' => 'Navn', - 'code' => 'Kode', - 'items' => 'Elementer', - 'add_subitem' => 'Nytt underelement', - 'code_required' => 'En kode kreves.', - 'invalid_code' => 'Ugyldig kode-format. Koden kan inneholde tall, latinske bokstaver og følgende symboler: _-', - ], - 'menuitem' => [ - 'title' => 'Tittel', - 'editor_title' => 'Endre element', - 'type' => 'Type', - 'allow_nested_items' => 'Tillat underelementer', - 'allow_nested_items_comment' => 'Underelementer kan bli generert dynamisk av statiske sider og andre elementtyper', - 'url' => 'URL', - 'reference' => 'Referanse', - 'title_required' => 'Tittel kreves.', - 'unknown_type' => 'Ukjent elementtype', - 'unnamed' => 'Navnløs elementtype', - 'add_item' => 'Legg til element', - 'new_item' => 'Nytt element', - 'replace' => 'Erstatt dette elementet med sine underelementer', - 'replace_comment' => 'Huk av denne boksen for å skjule dette elementet. Underelementer blir fremdeles synlige.', - 'cms_page' => 'CMS-side', - 'cms_page_comment' => 'Velg hvilken side som skal åpnes når man trykker på linken.', - 'reference_required' => 'En referanse kreves.', - 'url_required' => 'En URL kreves.', - 'cms_page_required' => 'Vennligst velg en CMS-side', - 'code' => 'Kode', - 'code_comment' => 'Velg en elementkode hvis du trenger tilgang via API-en. (valgfritt)', - ], - 'content' => [ - 'menu_label' => 'Innhold', - 'cant_save_to_dir' => 'Å lagre innhold til files i static-pages-mappen er ikke tillatt.', - ], - 'sidebar' => [ - 'add' => 'Legg til', - ], - 'object' => [ - 'invalid_type' => 'Ukjent objekttype', - 'not_found' => 'Det forespurte objektet ble ikke funnet.', - ], - 'editor' => [ - 'title' => 'Tittel', - 'new_title' => 'Tittel på siden', - 'content' => 'Innhold', - 'url' => 'URL', - 'filename' => 'Filnavn', - 'layout' => 'Layout', - 'description' => 'Beskrivelse', - 'preview' => 'Forhåndsvis', - 'enter_fullscreen' => 'Fullskjermmodus', - 'exit_fullscreen' => 'Avslutt fullskjermmodus', - 'hidden' => 'Skjult', - 'hidden_comment' => 'Kun backend-brukere har tilgang til skjulte sider.', - 'navigation_hidden' => 'Gjem i menyer', - 'navigation_hidden_comment' => 'Huk av denne boksen for å skjule denne siden i genererte menyer', - ], - 'snippet' => [ - 'menu_label' => 'Snippets', - ], -]; diff --git a/lang/nl.json b/lang/nl.json new file mode 100644 index 00000000..985f164f --- /dev/null +++ b/lang/nl.json @@ -0,0 +1,83 @@ +{ + "Pages": "Pagina's", + "Pages & menus features.": "Pagina & menu functionaliteit.", + "Manage static pages": "Beheer statische pagina's", + "Manage static menus": "Beheer statische menu's", + "Manage static content": "Beheer statische inhoud", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Ongeldige URL-structuur. De URL moet beginnen met een slash en kan enkel cijfers, Latijnse letters en deze symbolen bevatten: _-/.", + "This URL is already used by another page.": "Deze URL wordt al gebruikt door een andere pagina.", + "Layouts not found": "Geen layouts gevonden", + "The Code is required": "Code is verplicht", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Ongeldige code-structuur. De Code kan enkel cijfers, Latijnse letters en deze symbolen bevatten: _-", + "Static page": "Statische pagina", + "All static pages": "Alle statische pagina's", + "Static menu": "Statisch menu", + "Static breadcrumbs": "Statisch kruimelpad", + "Child pages": "Subpagina's", + "Outputs a static page in a CMS layout.": "Toont een statische pagina in een CMS layout.", + "Use page content field": "Gebruik het inhoudsveld van de pagina", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Indien niet aangevinkt, zal de inhoudssectie niet verschijnen bij het bewerken van de statische pagina. De pagina-inhoud wordt dan uitsluitend bepaald door placeholders en variabelen.", + "Default layout": "Standaard layout", + "Defines this layout as the default for new pages": "Definieert deze layout als de standaard voor nieuwe pagina's", + "Subpage layout": "Subpagina layout", + "The layout to use as the default for any new subpages": "De layout die standaard gebruikt wordt voor nieuwe subpagina's", + "Outputs a menu in a CMS layout.": "Toont een menu in een CMS layout.", + "Menu": "Menu", + "Specify a code of the menu the component should output.": "Geef de code op van het menu dat de component moet tonen.", + "Outputs breadcrumbs for a static page.": "Toont een kruimelpad voor een statische pagina.", + "Displays a list of child pages for the current page": "Toont een lijst van subpagina's voor de huidige pagina", + "Static Pages": "Statische pagina's", + "Menus": "Menu's", + "Content": "Inhoud", + "Add": "Toevoegen", + "Refresh": "Vernieuwen", + "Page": "Pagina", + "Content block": "Inhoudsblok", + "New page": "Nieuwe pagina", + "New page title": "Nieuwe pagina titel", + "New menu": "Nieuw menu", + "New content block": "Nieuw inhoudsblok", + "Title": "Titel", + "URL": "URL", + "File Name": "Bestandsnaam", + "Layout": "Layout", + "Hidden": "Verborgen", + "Hide in navigation": "Verbergen in de navigatie", + "Description": "Beschrijving", + "Name": "Naam", + "Code": "Code", + "The Title is required.": "Titel is verplicht.", + "The URL is required.": "Een URL is verplicht.", + "The File Name is required.": "Bestandsnaam is verplicht.", + "The Name is required.": "Naam is verplicht.", + "The Code is required.": "Code is verplicht.", + "Error loading page": "Fout bij het laden van de pagina", + "Error loading menu": "Fout bij het laden van het menu", + "Error loading content block": "Fout bij het laden van het inhoudsblok", + "Add item": "Item toevoegen", + "Add subitem": "Subitem toevoegen", + "New menu item": "Nieuw menu item", + "Untitled": "Naamloos", + "Up": "Omhoog", + "Down": "Omlaag", + "Indent": "Inspringen", + "Outdent": "Uitspringen", + "Delete": "Verwijderen", + "No menu items yet. Use \"Add item\" in the toolbar.": "Nog geen menu items. Gebruik \"Item toevoegen\" in de werkbalk.", + "Select a menu item to edit, or add a new one.": "Selecteer een menu item om te bewerken, of voeg een nieuw item toe.", + "Custom Fields": "Aangepaste velden", + "Search all references...": "Doorzoek alle referenties...", + "Edit Menu Item": "Bewerk Menu Item", + "Move up": "Omhoog verplaatsen", + "Move down": "Omlaag verplaatsen", + "Apply": "Toepassen", + "Cancel": "Annuleren", + "New Page": "Nieuwe pagina", + "New Menu": "Nieuw menu", + "New Content Block": "Nieuw inhoudsblok", + "Fields": "Velden", + "Preview": "Voorbeeld", + "Add subpage": "Subpagina toevoegen", + "You don't have permissions to manage :type documents.": "U heeft geen rechten om :type documenten te beheren.", + "Content files cannot be saved in the static pages directory.": "Inhoudsbestanden kunnen niet worden opgeslagen in de map met statische pagina's." +} diff --git a/lang/nl/lang.php b/lang/nl/lang.php deleted file mode 100644 index c74d8fc5..00000000 --- a/lang/nl/lang.php +++ /dev/null @@ -1,96 +0,0 @@ - [ - 'name' => 'Pagina\'s', - 'description' => 'Pagina & menu functionaliteit.', - ], - 'page' => [ - 'menu_label' => 'Pagina\'s', - 'template_title' => '%s Pagina\'s', - 'delete_confirmation' => 'Weet u zeker dat u de geselecteerde pagina\'s wilt verwijderen? Ook eventuele subpagina\'s zullen hierdoor verwijderd worden.', - 'no_records' => 'Geen pagina\'s gevonden', - 'delete_confirm_single' => 'Weet u zeker dat u deze pagina wilt verwijderen? Ook eventuele subpagina\'s zullen hierdoor verwijderd worden.', - 'new' => 'Nieuwe pagina', - 'add_subpage' => 'Subpagina toevoegen', - 'invalid_url' => 'Ongeldige URL-structuur. De URL moet beginnen met een slash en kan enkel cijfers, Latijnse letters en deze symbolen bevatten: _-/.', - 'url_not_unique' => 'Deze URL wordt al gebruikt door een andere pagina.', - 'layout' => 'Layout', - 'layouts_not_found' => 'Geen layouts gevonden', - 'saved' => 'De pagina is succesvol opgeslagen.', - 'tab' => 'Pagina\'s', - 'manage_pages' => 'Beheer statische pagina\'s', - 'manage_menus' => 'Beheer statische menu\'s', - 'access_snippets' => 'Toegang tot blokken', - 'manage_content' => 'Beheer statische inhoud', - ], - 'menu' => [ - 'menu_label' => 'Menu\'s', - 'delete_confirmation' => 'Weet u zeker dat u de geselecteerde menu\'s wilt verwijderen?', - 'no_records' => 'Geen menu\'s gevonden', - 'new' => 'Nieuw menu', - 'new_name' => 'Nieuw menu', - 'new_code' => 'nieuw-menu', - 'delete_confirm_single' => 'Weet u zeker dat u dit menu wilt verwijderen?', - 'saved' => 'Het menu is opgeslagen.', - 'name' => 'Naam', - 'code' => 'Code', - 'items' => 'Menu items', - 'add_subitem' => 'Subitem toevoegen', - 'code_required' => 'Code is verplicht', - 'invalid_code' => 'Ongeldige code-structuur. De Code kan enkel cijfers, Latijnse letters en deze symbolen bevatten: _-', - ], - 'menuitem' => [ - 'title' => 'Titel', - 'editor_title' => 'Bewerk Menu Item', - 'type' => 'Type', - 'allow_nested_items' => 'Accepteer geneste items', - 'allow_nested_items_comment' => 'Geneste items worden dynamisch gegenereerd door statische pagina\'s en sommige andere types.', - 'url' => 'URL', - 'reference' => 'Referentie', - 'title_required' => 'Titel is verplicht', - 'unknown_type' => 'Onbekend menu item type', - 'unnamed' => 'Onbenoemd menu item', - 'add_item' => 'Item toevoegen', - 'new_item' => 'Nieuw menu item', - 'replace' => 'Vervang dit item door de gegenereerde subitems', - 'replace_comment' => 'Wanneer u deze optie aanvinkt, zullen de gegenereerd menu items worden getoond op het niveau van dit item. Dit item zelf zal verborgen blijven.', - 'cms_page' => 'CMS Pagina', - 'cms_page_comment' => 'Selecteer een pagina om te openen wanneer op het menu item geklikt wordt.', - 'reference_required' => 'Een referentie is verplicht.', - 'url_required' => 'Een URL is verplicht', - 'cms_page_required' => 'Gelieve een CMS Pagina te selecteren', - 'code' => 'Code', - 'code_comment' => 'Geef de menu item code op indien u deze wilt benaderen via de API.', - 'static_page' => 'Statische pagina', - 'all_static_pages' => 'Alle statische pagina\'s', - ], - 'content' => [ - 'menu_label' => 'Inhoud', - 'cant_save_to_dir' => 'Het is niet toegelaten bestanden met inhoud op te slaan in de static-pages map.', - ], - 'sidebar' => [ - 'add' => 'Toevoegen', - ], - 'object' => [ - 'invalid_type' => 'Onbekend object type', - 'not_found' => 'Het gevraagde object is niet gevonden.', - ], - 'editor' => [ - 'title' => 'Titel', - 'new_title' => 'Nieuwe pagina titel', - 'content' => 'Inhoud', - 'url' => 'URL', - 'filename' => 'Bestandsnaam', - 'layout' => 'Layout', - 'description' => 'Beschrijving', - 'preview' => 'Voorbeeld', - 'enter_fullscreen' => 'Volledig scherm openen', - 'exit_fullscreen' => 'Volledig scherm afsluiten', - 'hidden' => 'Verborgen', - 'hidden_comment' => 'Verborgen pagina\'s zijn alleen toegankelijk voor ingelogde gebruikers.', - 'navigation_hidden' => 'Verbergen in de navigatie', - 'navigation_hidden_comment' => 'Indien aangevinkt, zal deze pagina niet weergegeven worden in automatisch gegenereerde menu\'s en kruimelpaden (breadcrumbs).', - ], - 'snippet' => [ - 'menu_label' => 'Blokken', - ], -]; diff --git a/lang/pl.json b/lang/pl.json new file mode 100644 index 00000000..3d1d1aff --- /dev/null +++ b/lang/pl.json @@ -0,0 +1,83 @@ +{ + "Pages": "Strony", + "Pages & menus features.": "Strony statyczne oraz menu.", + "Manage static pages": "Zarządzaj stronami statycznymi", + "Manage static menus": "Zarządzaj menu statycznymi", + "Manage static content": "Zarządzaj treścią statyczną", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Nieprawidłowy format URL. Adres URL powinien zaczynać się od ukośnika i może zawierać cyfry, litery łacińskie oraz następujące symbole: _-/.", + "This URL is already used by another page.": "Podany URL istnieje już w bazie.", + "Layouts not found": "Brak układów", + "The Code is required": "Kod systemowy jest wymagany", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Nieprawidłowy kod systemowy. Nie może zawierać znaków specjalnych", + "Static page": "Strona statyczna", + "All static pages": "Wszystkie strony statyczne", + "Static menu": "Menu statyczne", + "Static breadcrumbs": "Okruszki (Breadcrumbs)", + "Child pages": "Strony podrzędne", + "Outputs a static page in a CMS layout.": "Dodaje zawartość statycznej strony.", + "Use page content field": "Użyj pola zawartości strony", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Jeżeli odznaczone, sekcja treści nie pojawi się podczas edycji strony statycznej. Zawartość strony będzie ustalana wyłącznie na podstawie symboli zastępczych i zmiennych.", + "Default layout": "Domyślny układ", + "Defines this layout as the default for new pages": "Definiuje ten układ jako domyślny dla nowych stron", + "Subpage layout": "Układ podstrony", + "The layout to use as the default for any new subpages": "Układ, który będzie używany jako domyślny dla każdej nowej podstrony", + "Outputs a menu in a CMS layout.": "Dodaje menu do strony.", + "Menu": "Menu", + "Specify a code of the menu the component should output.": "Podaj kod systemowy menu, które ma zostać wyświetlone.", + "Outputs breadcrumbs for a static page.": "Zwraca ścieżkę stron statycznych.", + "Displays a list of child pages for the current page": "Wyświetla listę stron podrzędnych dla aktualnej strony", + "Static Pages": "Strony statyczne", + "Menus": "Menu", + "Content": "Treść", + "Add": "Dodaj", + "Refresh": "Odśwież", + "Page": "Strona", + "Content block": "Blok treści", + "New page": "Nowa strona", + "New page title": "Nowy tytuł strony", + "New menu": "Nowe menu", + "New content block": "Nowy blok treści", + "Title": "Tytuł", + "URL": "URL", + "File Name": "Nazwa pliku", + "Layout": "Układ", + "Hidden": "Ukryta", + "Hide in navigation": "Ukryj stronę w nawigacji", + "Description": "Opis", + "Name": "Nazwa", + "Code": "Kod systemowy", + "The Title is required.": "Tytuł jest wymagany.", + "The URL is required.": "URL jest wymagany.", + "The File Name is required.": "Nazwa pliku jest wymagana.", + "The Name is required.": "Nazwa jest wymagana.", + "The Code is required.": "Kod systemowy jest wymagany.", + "Error loading page": "Błąd podczas wczytywania strony", + "Error loading menu": "Błąd podczas wczytywania menu", + "Error loading content block": "Błąd podczas wczytywania bloku treści", + "Add item": "Dodaj element", + "Add subitem": "Dodaj podelement", + "New menu item": "Nowy element menu", + "Untitled": "Bez tytułu", + "Up": "W górę", + "Down": "W dół", + "Indent": "Zwiększ wcięcie", + "Outdent": "Zmniejsz wcięcie", + "Delete": "Usuń", + "No menu items yet. Use \"Add item\" in the toolbar.": "Brak elementów menu. Użyj \"Dodaj element\" na pasku narzędzi.", + "Select a menu item to edit, or add a new one.": "Wybierz element menu do edycji lub dodaj nowy.", + "Custom Fields": "Pola niestandardowe", + "Search all references...": "Przeszukaj wszystkie referencje...", + "Edit Menu Item": "Edytuj element", + "Move up": "Przenieś w górę", + "Move down": "Przenieś w dół", + "Apply": "Zastosuj", + "Cancel": "Anuluj", + "New Page": "Nowa strona", + "New Menu": "Nowe menu", + "New Content Block": "Nowy blok treści", + "Fields": "Pola", + "Preview": "Podgląd", + "Add subpage": "Dodaj podstronę", + "You don't have permissions to manage :type documents.": "Nie masz uprawnień do zarządzania dokumentami typu :type.", + "Content files cannot be saved in the static pages directory.": "Pliki treści nie mogą być zapisywane w katalogu stron statycznych." +} diff --git a/lang/pl/lang.php b/lang/pl/lang.php deleted file mode 100644 index d0e54685..00000000 --- a/lang/pl/lang.php +++ /dev/null @@ -1,124 +0,0 @@ - [ - 'name' => 'Strony', - 'description' => 'Strony statyczne oraz menu.', - ], - 'page' => [ - 'menu_label' => 'Strony', - 'template_title' => '%s Strony', - 'delete_confirmation' => 'Czy na pewno chcesz usunąć wybrane strony? Podstrony również zostaną usnięte.', - 'no_records' => 'Nie znaleziono stron', - 'delete_confirm_single' => 'Czy na pewno chcesz usunąć stronę? Podstrony również zostaną usnięte.', - 'new' => 'Nowa strona', - 'add_subpage' => 'Dodaj podstronę', - 'invalid_url' => 'Niewprawidłowy format URL lub niedozwolone znaki.', - 'url_not_unique' => 'Podany URL istnieje już w bazie.', - 'layout' => 'Układ', - 'layouts_not_found' => 'Brak układów', - 'saved' => 'Strona została zapisana poprawnie.', - 'tab' => 'Strony', - 'manage_pages' => 'Zarządzaj stronami statycznymi', - 'manage_menus' => 'Zarządzaj menu statycznymi', - 'access_snippets' => 'Fragmenty', - 'manage_content' => 'Zarządzaj treścią statyczną', - ], - 'menu' => [ - 'menu_label' => 'Menu', - 'delete_confirmation' => 'Czy na pewno chcesz usunąć wybrane menu?', - 'no_records' => 'Nie znaleziono menu', - 'new' => 'Nowe menu', - 'new_name' => 'Nowe menu', - 'new_code' => 'nowe-menu', - 'delete_confirm_single' => 'Czy na pewno chcesz usunąć wybrane menu?', - 'saved' => 'Menu zostało zapisane poprawnie.', - 'name' => 'Nazwa', - 'code' => 'Kod systemowy', - 'items' => 'Elementy menu', - 'add_subitem' => 'Dodaj element', - 'code_required' => 'Kod systemowy jest wymagany', - 'invalid_code' => 'Nieprawidłowy kod systemowy. Nie może zawierać znaków specjalnych', - ], - 'menuitem' => [ - 'title' => 'Tytuł', - 'editor_title' => 'Edytuj element', - 'type' => 'Typ', - 'allow_nested_items' => 'Pozwól na zagnieżdżanie elementów', - 'allow_nested_items_comment' => 'Zagnieżdżone elementy mogą być utworzone dynamicznie np ze stron statycznych', - 'url' => 'URL', - 'reference' => 'Referencja', - 'search_placeholder' => 'Przeszukaj wszystkie referencje...', - 'title_required' => 'Tytuł jest wymagany', - 'unknown_type' => 'Nieznany typ elementu', - 'unnamed' => 'Brak nazwy typu elementu', - 'add_item' => 'Dodaj Element', - 'new_item' => 'Nowy element menu', - 'replace' => 'Zamień element na elementy wenątrz tego elementu', - 'replace_comment' => 'Użyj tej opcji aby elementy znajdujące sie w tym elemencie były wygenerowane na poziomie tego elementu a sam element zostanie ukryty.', - 'cms_page' => 'Strona CMS', - 'cms_page_comment' => 'Wybierz stronę z cms do której ma kierować', - 'reference_required' => 'Referencja jest wymagana', - 'url_required' => 'URL jest wymagany', - 'cms_page_required' => 'Wybierz stronę z CMS', - 'display_tab' => 'Wyświetlanie', - 'hidden' => 'Ukryj', - 'hidden_comment' => 'Ukryj ten element menu na stronie', - 'attributes_tab' => 'Atrybuty', - 'code' => 'Kod systemowy', - 'code_comment' => 'Wprowadź kod systemowy aby móc go używać w API.', - 'css_class' => 'Klasa CSS', - 'css_class_comment' => 'Wprowadź klasę CSS, aby nadać temu elementowi niestandardowy wygląd', - 'external_link' => 'Zewnętrzny link', - 'external_link_comment' => 'Adres linku zostanie otworzony w nowym oknie', - 'static_page' => 'Strona statyczna', - 'all_static_pages' => 'Wszystkie strony statyczne', - ], - 'content' => [ - 'menu_label' => 'Treść', - 'cant_save_to_dir' => 'Zapis treści jest niemożliwy. Sprawdź dostęp do folderu treści statycznych.', - ], - 'sidebar' => [ - 'add' => 'Dodaj', - ], - 'object' => [ - 'invalid_type' => 'Nieznany typ obiektu', - 'not_found' => 'Nie znaleziono objektu.', - ], - 'editor' => [ - 'title' => 'Tytuł', - 'new_title' => 'Nowy tytuł strony', - 'content' => 'Treść', - 'url' => 'URL', - 'filename' => 'Nazwa pliku', - 'layout' => 'Układ', - 'description' => 'Opis', - 'preview' => 'Podgląd', - 'enter_fullscreen' => 'Pełny ekran', - 'exit_fullscreen' => 'Zamknij pełny ekran', - 'hidden' => 'Ukryta', - 'hidden_comment' => 'Ukryte strony są dostępne tylko dla zalogowanych administratorów.', - 'navigation_hidden' => 'Ukryj stronę w nawigacji', - 'navigation_hidden_comment' => 'Zaznacz aby usunąć strone z automatycznego generowania menu oraz ścieżek (breadcrumbs).', - ], - 'snippet' => [ - 'menu_label' => 'Fragmenty', - ], - 'component' => [ - 'static_page_name' => 'Strona statyczna', - 'static_page_description' => 'Dodaje zawartość statycznej strony.', - 'static_page_use_content_name' => 'Użyj pola zawartości strony', - 'static_page_use_content_description' => 'Jeżeli odznaczone, sekcja treści nie pojawi się podczas edycji strony statycznej. Zawartość strony będzie ustalana wyłącznie na podstawie symboli zastępczych i zmiennych', - 'static_page_default_name' => 'Domyślny układ', - 'static_page_default_description' => 'Definiuje ten układ jako domyślny dla nowych stron', - 'static_page_child_layout_name' => 'Układ podstrony', - 'static_page_child_layout_description' => 'Układ, który będzie używany jako domyślny dla każdej nowej podstrony', - 'static_menu_name' => 'Menu', - 'static_menu_description' => 'Dodaje menu do strony.', - 'static_menu_code_name' => 'Menu', - 'static_menu_code_description' => 'Podaj kod systemowy menu, które ma zostać wyświetlone.', - 'static_breadcrumbs_name' => 'Okruszki (Breadcrumbs)', - 'static_breadcrumbs_description' => 'Zwraca ścieżkę stron statycznych.', - 'child_pages_name' => 'Strony podrzędne', - 'child_pages_description' => 'Wyświetla listę stron podrzędnych dla aktualnej strony', - 'static_menu_menu_code' => 'Podaj kod systemowy menu', - ], -]; diff --git a/lang/pt-br.json b/lang/pt-br.json new file mode 100644 index 00000000..4eca18ff --- /dev/null +++ b/lang/pt-br.json @@ -0,0 +1,83 @@ +{ + "Pages": "Páginas", + "Pages & menus features.": "Gerenciar páginas e menus.", + "Manage static pages": "Gerenciar páginas estáticas", + "Manage static menus": "Gerenciar menus estáticos", + "Manage static content": "Gerenciar conteúdos estáticos", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Formato inválido de URL. A URL deve iniciar com o símbolo / e pode conter apenas dígitos, letras latinas e os seguintes símbolos: _-/.", + "This URL is already used by another page.": "Esta URL já está sendo utilizada por outra página.", + "Layouts not found": "Nenhum layout encontrado", + "The Code is required": "O código é necessário", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Formato inválido de código. O código pode conter dígitos, letras latinas e os seguintes símbolos: _-", + "Static page": "Página estática", + "All static pages": "Todas as páginas estáticas", + "Static menu": "Menu estático", + "Static breadcrumbs": "Trilha de navegação estática", + "Child pages": "Páginas filhas", + "Outputs a static page in a CMS layout.": "Exibe uma página estática em um layout do CMS.", + "Use page content field": "Usar campo de conteúdo da página", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Se desmarcado, a seção de conteúdo não aparecerá ao editar a página estática. O conteúdo da página será determinado exclusivamente por placeholders e variáveis.", + "Default layout": "Layout padrão", + "Defines this layout as the default for new pages": "Define este layout como padrão para novas páginas", + "Subpage layout": "Layout de subpágina", + "The layout to use as the default for any new subpages": "O layout usado como padrão para novas subpáginas", + "Outputs a menu in a CMS layout.": "Exibe um menu em um layout do CMS.", + "Menu": "Menu", + "Specify a code of the menu the component should output.": "Especifique o código do menu que o componente deve exibir.", + "Outputs breadcrumbs for a static page.": "Exibe a trilha de navegação de uma página estática.", + "Displays a list of child pages for the current page": "Exibe uma lista de páginas filhas da página atual", + "Static Pages": "Páginas Estáticas", + "Menus": "Menus", + "Content": "Conteúdo", + "Add": "Adicionar", + "Refresh": "Atualizar", + "Page": "Página", + "Content block": "Bloco de conteúdo", + "New page": "Nova página", + "New page title": "Título da nova página", + "New menu": "Novo menu", + "New content block": "Novo bloco de conteúdo", + "Title": "Título", + "URL": "URL", + "File Name": "Nome do arquivo", + "Layout": "Layout", + "Hidden": "Ocultar", + "Hide in navigation": "Ocultar na navegação", + "Description": "Descrição", + "Name": "Nome", + "Code": "Código", + "The Title is required.": "O título é necessário.", + "The URL is required.": "A URL é necessária.", + "The File Name is required.": "O nome do arquivo é necessário.", + "The Name is required.": "O nome é necessário.", + "The Code is required.": "O código é necessário.", + "Error loading page": "Erro ao carregar a página", + "Error loading menu": "Erro ao carregar o menu", + "Error loading content block": "Erro ao carregar o bloco de conteúdo", + "Add item": "Adicionar item", + "Add subitem": "Adicionar subitem", + "New menu item": "Novo item de menu", + "Untitled": "Sem título", + "Up": "Para cima", + "Down": "Para baixo", + "Indent": "Aumentar recuo", + "Outdent": "Diminuir recuo", + "Delete": "Excluir", + "No menu items yet. Use \"Add item\" in the toolbar.": "Ainda não há itens de menu. Use \"Adicionar item\" na barra de ferramentas.", + "Select a menu item to edit, or add a new one.": "Selecione um item de menu para editar ou adicione um novo.", + "Custom Fields": "Campos Personalizados", + "Search all references...": "Pesquisar todas as referências...", + "Edit Menu Item": "Editar item", + "Move up": "Mover para cima", + "Move down": "Mover para baixo", + "Apply": "Aplicar", + "Cancel": "Cancelar", + "New Page": "Nova página", + "New Menu": "Novo menu", + "New Content Block": "Novo bloco de conteúdo", + "Fields": "Campos", + "Preview": "Visualizar", + "Add subpage": "Adicionar subpágina", + "You don't have permissions to manage :type documents.": "Você não tem permissão para gerenciar documentos do tipo :type.", + "Content files cannot be saved in the static pages directory.": "Não é permitido salvar arquivos de conteúdo no diretório de páginas estáticas." +} diff --git a/lang/pt-br/lang.php b/lang/pt-br/lang.php deleted file mode 100644 index 56e0bb65..00000000 --- a/lang/pt-br/lang.php +++ /dev/null @@ -1,94 +0,0 @@ - [ - 'name' => 'Páginas', - 'description' => 'Gerenciar páginas e menus.', - ], - 'page' => [ - 'menu_label' => 'Páginas', - 'template_title' => '%s Páginas', - 'delete_confirmation' => 'Tem certeza que deseja excluir as páginas selecionadas? Todas as subpáginas também serão excluídas.', - 'no_records' => 'Nenhuma página encontrada', - 'delete_confirm_single' => 'Tem certeza que deseja excluir a página selecionada? Todas as subpáginas também serão excluídas.', - 'new' => 'Nova página', - 'add_subpage' => 'Adicionar subpágina', - 'invalid_url' => 'Formato inválido de URL. A URL deve iniciar com o símbolo / e pode conter apenas dígitos, letras latinas e os seguintes símbolos: _-/.', - 'url_not_unique' => 'Esta URL já está sendo utilizada por outra página.', - 'layout' => 'Layout', - 'layouts_not_found' => 'Nenhum layout encontrado', - 'saved' => 'Página salva com sucesso.', - 'tab' => 'Páginas', - 'manage_pages' => 'Gerenciar páginas estáticas', - 'manage_menus' => 'Gerenciar menus estáticos', - 'access_snippets' => 'Acessar fragmentos', - 'manage_content' => 'Gerenciar conteúdos estáticos', - ], - 'menu' => [ - 'menu_label' => 'Menus', - 'delete_confirmation' => 'Tem certeza que deseja excluir os menus selecionados?', - 'no_records' => 'Nenhum item encontrado', - 'new' => 'Novo menu', - 'new_name' => 'Novo menu', - 'new_code' => 'novo-menu', - 'delete_confirm_single' => 'Tem certeza que deseja excluir este menu?', - 'saved' => 'Menu salvo com sucesso.', - 'name' => 'Nome', - 'code' => 'Código', - 'items' => 'Itens do menu', - 'add_subitem' => 'Adicionar subitem', - 'code_required' => 'O código é necessário', - 'invalid_code' => 'Formato inválido de código. O código pode conter dígitos, letras latinas e os seguintes símbolos: _-', - ], - 'menuitem' => [ - 'title' => 'Título', - 'editor_title' => 'Editar item', - 'type' => 'Tipo', - 'allow_nested_items' => 'Permitir itens aninhados', - 'allow_nested_items_comment' => 'Itens aninhados podem ser gerados dinamicamente por páginas estáticas e outros tipos de itens', - 'url' => 'URL', - 'reference' => 'Referência', - 'title_required' => 'O título é necessário', - 'unknown_type' => 'Tipo de item desconhecido', - 'unnamed' => 'Item de menu sem nome', - 'add_item' => 'Adicionar item', - 'new_item' => 'Novo item', - 'replace' => 'Substituir este item com seus filhos gerados', - 'replace_comment' => 'Use esta opção para empurrar os itens de menu gerados para o mesmo nível que este item. Este item em si será ocultado.', - 'cms_page' => 'Página CMS', - 'cms_page_comment' => 'Selecione uma página para abrir quando o item for clicado.', - 'reference_required' => 'A referência do item é necessária.', - 'url_required' => 'A URL é necessária', - 'cms_page_required' => 'Por favor, selecione uma página CMS', - 'code' => 'Código', - 'code_comment' => 'Entre com o código do item se deseja acessar com a API.', - ], - 'content' => [ - 'menu_label' => 'Conteúdo', - 'cant_save_to_dir' => 'Não é permitido salvar arquivos de conteúdo no diretório de páginas estáticas.', - ], - 'sidebar' => [ - 'add' => 'Adicionar', - ], - 'object' => [ - 'invalid_type' => 'Tipo de objeto desconhecido', - 'not_found' => 'O objeto requisitado não foi encontrado.', - ], - 'editor' => [ - 'title' => 'Título', - 'new_title' => 'Título da nova página', - 'content' => 'Conteúdo', - 'url' => 'URL', - 'filename' => 'Nome do arquivo', - 'layout' => 'Layout', - 'description' => 'Descrição', - 'preview' => 'Visualizar', - 'enter_fullscreen' => 'Entrar no modo tela cheia', - 'exit_fullscreen' => 'Sair do modo tela cheia', - 'hidden' => 'Ocultar', - 'hidden_comment' => 'Páginas ocultas são acessíveis apenas para administradores.', - 'navigation_hidden' => 'Ocultar na navegação', - 'navigation_hidden_comment' => 'Marque esta opção para ocultar esta página de menus gerados automaticamente e itens de hierarquia de navegação.', - ], - 'snippet' => [ - 'menu_label' => 'Fragmentos', - ], -]; diff --git a/lang/ru.json b/lang/ru.json new file mode 100644 index 00000000..9cdbde2c --- /dev/null +++ b/lang/ru.json @@ -0,0 +1,83 @@ +{ + "Pages": "Страницы", + "Pages & menus features.": "Страницы и меню.", + "Manage static pages": "Управление страницами", + "Manage static menus": "Управление меню", + "Manage static content": "Управление содержимым", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Некорректный формат URL. URL должен начинаться с прямого слеша и может содержать цифры, латинские буквы и следующие символы: _-/.", + "This URL is already used by another page.": "Этот URL уже используется другой страницей.", + "Layouts not found": "Шаблоны не найдены", + "The Code is required": "Поле Код обязательно", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Некорректный формат Кода. Код может содержать цифры, латинские буквы и следующие символы: _-", + "Static page": "Статическая страница", + "All static pages": "Все статические страницы", + "Static menu": "Статическое меню", + "Static breadcrumbs": "Статические хлебные крошки", + "Child pages": "Дочерние страницы", + "Outputs a static page in a CMS layout.": "Выводит страницу в CMS шаблоне.", + "Use page content field": "Использовать поле содержимого страницы", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Если не отмечено, секция содержимого не будет отображаться при редактировании статической страницы. Содержимое страницы будет определяться исключительно через плейсхолдеры и переменные.", + "Default layout": "Шаблон по умолчанию", + "Defines this layout as the default for new pages": "Определяет этот шаблон как шаблон по умолчанию для новых страниц", + "Subpage layout": "Шаблон подстраницы", + "The layout to use as the default for any new subpages": "Шаблон, используемый по умолчанию для всех новых подстраниц", + "Outputs a menu in a CMS layout.": "Выводит меню в CMS шаблоне.", + "Menu": "Меню", + "Specify a code of the menu the component should output.": "Укажите код меню, которое компонент должен вывести.", + "Outputs breadcrumbs for a static page.": "Выводит хлебные крошки для страницы.", + "Displays a list of child pages for the current page": "Отображает список дочерних страниц для текущей страницы", + "Static Pages": "Статические страницы", + "Menus": "Меню", + "Content": "Содержимое", + "Add": "Добавить", + "Refresh": "Обновить", + "Page": "Страница", + "Content block": "Блок содержимого", + "New page": "Новая страница", + "New page title": "Название новой страницы", + "New menu": "Новое меню", + "New content block": "Новый блок содержимого", + "Title": "Название", + "URL": "URL", + "File Name": "Имя файла", + "Layout": "Шаблон", + "Hidden": "Скрытый", + "Hide in navigation": "Спрятать в навигации", + "Description": "Описание", + "Name": "Имя", + "Code": "Код", + "The Title is required.": "Название обязательно.", + "The URL is required.": "Необходим URL.", + "The File Name is required.": "Имя файла обязательно.", + "The Name is required.": "Имя обязательно.", + "The Code is required.": "Поле Код обязательно.", + "Error loading page": "Ошибка загрузки страницы", + "Error loading menu": "Ошибка загрузки меню", + "Error loading content block": "Ошибка загрузки блока содержимого", + "Add item": "Добавить пункт", + "Add subitem": "Добавить подменю", + "New menu item": "Новый пункт", + "Untitled": "Без названия", + "Up": "Вверх", + "Down": "Вниз", + "Indent": "Увеличить отступ", + "Outdent": "Уменьшить отступ", + "Delete": "Удалить", + "No menu items yet. Use \"Add item\" in the toolbar.": "Пунктов меню пока нет. Используйте \"Добавить пункт\" на панели инструментов.", + "Select a menu item to edit, or add a new one.": "Выберите пункт меню для редактирования или добавьте новый.", + "Custom Fields": "Пользовательские поля", + "Search all references...": "Поиск по всем ссылкам...", + "Edit Menu Item": "Редактировать пункт меню", + "Move up": "Переместить вверх", + "Move down": "Переместить вниз", + "Apply": "Применить", + "Cancel": "Отмена", + "New Page": "Новая страница", + "New Menu": "Новое меню", + "New Content Block": "Новый блок содержимого", + "Fields": "Поля", + "Preview": "Предпросмотр", + "Add subpage": "Добавить подстраницу", + "You don't have permissions to manage :type documents.": "У вас нет прав на управление документами типа :type.", + "Content files cannot be saved in the static pages directory.": "Сохранение файлов содержимого в директорию статических страниц запрещено." +} diff --git a/lang/ru/lang.php b/lang/ru/lang.php deleted file mode 100644 index 5aaec692..00000000 --- a/lang/ru/lang.php +++ /dev/null @@ -1,100 +0,0 @@ - [ - 'name' => 'Страницы', - 'description' => 'Страницы и меню.', - ], - 'page' => [ - 'menu_label' => 'Страницы', - 'template_title' => '%s Страницы', - 'delete_confirmation' => 'Вы действительно хотите удалить выбранные страницы? Это также удалит имеющиеся подстраницы.', - 'no_records' => 'Страниц не найдено', - 'delete_confirm_single' => 'Вы действительно хотите удалить эту страницу? Это также удалит имеющиеся подстраницы.', - 'new' => 'Новая страница', - 'add_subpage' => 'Добавить подстраницу', - 'invalid_url' => 'Некорректный формат URL. URL должен начинаться с прямого слеша и может содержать цифры, латинские буквы и следующие символы: _-/.', - 'url_not_unique' => 'Это URL уже используется другой страницей.', - 'layout' => 'Шаблон', - 'layouts_not_found' => 'Шаблоны не найдены', - 'saved' => 'Страница была успешно сохранена.', - 'tab' => 'Страницы', - 'manage_pages' => 'Управление страницами', - 'manage_menus' => 'Управление меню', - 'access_snippets' => 'Доступ к сниппетами', - 'manage_content' => 'Управление содержимым', - ], - 'menu' => [ - 'menu_label' => 'Меню', - 'delete_confirmation' => 'Вы действительно хотите удалить выбранные пункты меню?', - 'no_records' => 'Меню не найдены', - 'new' => 'Новое меню', - 'new_name' => 'Новое меню', - 'new_code' => 'novoe-menyu', - 'delete_confirm_single' => 'Вы действительно хотите удалить это меню?', - 'saved' => 'Меню было успешно сохранено.', - 'name' => 'Имя', - 'code' => 'Код', - 'items' => 'Пункты меню', - 'add_subitem' => 'Добавить подменю', - 'code_required' => 'Поле Код обязательно', - 'invalid_code' => 'Некорректный формат Кода. Код может содержать цифры, латинские буквы и следующие символы: _-/', - ], - 'menuitem' => [ - 'title' => 'Название', - 'editor_title' => 'Редактировать пункт меню', - 'type' => 'Тип', - 'allow_nested_items' => 'Разрешить вложенные', - 'allow_nested_items_comment' => 'Вложенные пункты могут быть динамически сгенерированы статической страницей или другими типами элементов', - 'url' => 'URL', - 'reference' => 'Ссылка', - 'title_required' => 'Название обязательно', - 'unknown_type' => 'Неизвестный тип меню', - 'unnamed' => 'Безымянный пункт', - 'add_item' => 'Добавить пункт (i)', - 'new_item' => 'Новый пункт', - 'replace' => 'Заменять этот пункт его сгенерированными потомками', - 'replace_comment' => 'Отметьте для переноса генерируемых пунктов меню на один уровень с этим пунктом. Сам этот пункт будет скрыт.', - 'cms_page' => 'Страницы CMS', - 'cms_page_comment' => 'Выберите открываемую по клику страницу.', - 'reference_required' => 'Необходима ссылка для пункта меню.', - 'url_required' => 'Необходим URL', - 'cms_page_required' => 'Пожалуйста, выберите страницу CMS', - 'code' => 'Код', - 'code_comment' => 'Введите код пункта меню, если хотите иметь к нему доступ через API.', - ], - 'content' => [ - 'menu_label' => 'Содержимое', - 'cant_save_to_dir' => 'Сохранение файлов содержимого в директорию static-pages запрещено.', - ], - 'sidebar' => [ - 'add' => 'Добавить', - ], - 'object' => [ - 'invalid_type' => 'Неизвестный тип объекта', - 'not_found' => 'Запрашиваемый объект не найден.', - ], - 'editor' => [ - 'title' => 'Название', - 'new_title' => 'Название новой страницы', - 'content' => 'Содержимое', - 'url' => 'URL', - 'filename' => 'Имя Файла', - 'layout' => 'Шаблон', - 'description' => 'Описание', - 'preview' => 'Предпросмотр', - 'enter_fullscreen' => 'Войти в полноэкранный режим', - 'exit_fullscreen' => 'Выйти из полноэкранного режима', - 'hidden' => 'Скрытый', - 'hidden_comment' => 'Скрытые страницы доступны только вошедшим администраторам.', - 'navigation_hidden' => 'Спрятать в навигации', - 'navigation_hidden_comment' => 'Отметьте, чтобы скрыть эту страницу в генерируемых меню и хлебных крошках.', - ], - 'snippet' => [ - 'menu_label' => 'Сниппеты', - ], - 'component' => [ - 'static_page_description' => 'Выводит страницу в CMS шаблоне.', - 'static_menu_description' => 'Выводит меню в CMS шаблоне.', - 'static_menu_menu_code' => 'Укажите код меню, которое должно быть показано', - 'static_breadcrumbs_description' => 'Выводит хлебные крошки для страницы.', - ], -]; diff --git a/lang/sk.json b/lang/sk.json new file mode 100644 index 00000000..4c4f6a97 --- /dev/null +++ b/lang/sk.json @@ -0,0 +1,83 @@ +{ + "Pages": "Stránky", + "Pages & menus features.": "Funkcie pre správu stránok a menu.", + "Manage static pages": "Správa stránok", + "Manage static menus": "Správa menu", + "Manage static content": "Správa obsahu", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Neplatný formát URL adresy. URL by mala začínať symbolom lomítka a môže obsahovať číslice, latinské písmená a nasledujúce znaky: _-/.", + "This URL is already used by another page.": "Túto URL adresu už používa iná stránka.", + "Layouts not found": "Žiadne layouty neboli nájdené", + "The Code is required": "Pole kód je povinné", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Neplatný formát kódu. Kód môže obsahovať číslice, latinské písmená a nasledujúce znaky: _-", + "Static page": "Statická stránka", + "All static pages": "Všetky statické stránky", + "Static menu": "Statické menu", + "Static breadcrumbs": "Statická navigačná cesta", + "Child pages": "Podstránky", + "Outputs a static page in a CMS layout.": "Zobrazí obsah statickej stránky v CMS layoute.", + "Use page content field": "Použiť pole obsah stránky", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Ak nie je začiarknuté, sekcia obsahu sa nezobrazí pri úprave statickej stránky. Obsah stránky bude určený výhradne prostredníctvom zástupcov a premenných.", + "Default layout": "Predvolený layout", + "Defines this layout as the default for new pages": "Nastaví tento layout ako predvolený pre nové stránky", + "Subpage layout": "Layout podstránky", + "The layout to use as the default for any new subpages": "Layout ktorý sa má použiť ako predvolený pre všetky nové podstránky", + "Outputs a menu in a CMS layout.": "Zobrazí menu na stránke.", + "Menu": "Menu", + "Specify a code of the menu the component should output.": "Zadajte kód menu, ktoré má komponent zobraziť.", + "Outputs breadcrumbs for a static page.": "Zobrazí navigačnú cestu na stránke.", + "Displays a list of child pages for the current page": "Zobrazí zoznam podstránok pre aktuálnu stránku", + "Static Pages": "Statické stránky", + "Menus": "Menu", + "Content": "Obsah", + "Add": "Pridať", + "Refresh": "Obnoviť", + "Page": "Stránka", + "Content block": "Blok obsahu", + "New page": "Nová stránka", + "New page title": "Názov novej stránky", + "New menu": "Nové menu", + "New content block": "Nový blok obsahu", + "Title": "Názov", + "URL": "URL adresa", + "File Name": "Názov súboru", + "Layout": "Layout", + "Hidden": "Skrytá", + "Hide in navigation": "Skryť v menu", + "Description": "Popis", + "Name": "Názov", + "Code": "Kód", + "The Title is required.": "Názov je povinný.", + "The URL is required.": "Adresa URL je povinná.", + "The File Name is required.": "Názov súboru je povinný.", + "The Name is required.": "Názov je povinný.", + "The Code is required.": "Pole kód je povinné.", + "Error loading page": "Chyba pri načítaní stránky", + "Error loading menu": "Chyba pri načítaní menu", + "Error loading content block": "Chyba pri načítaní bloku obsahu", + "Add item": "Pridať položku", + "Add subitem": "Pridať vnorenú položku", + "New menu item": "Nová položka menu", + "Untitled": "Bez názvu", + "Up": "Hore", + "Down": "Dole", + "Indent": "Zväčšiť odsadenie", + "Outdent": "Zmenšiť odsadenie", + "Delete": "Odstrániť", + "No menu items yet. Use \"Add item\" in the toolbar.": "Zatiaľ žiadne položky menu. Použite \"Pridať položku\" na paneli nástrojov.", + "Select a menu item to edit, or add a new one.": "Vyberte položku menu na úpravu alebo pridajte novú.", + "Custom Fields": "Vlastné polia", + "Search all references...": "Prehľadať všetky odkazy...", + "Edit Menu Item": "Upraviť položku menu", + "Move up": "Presunúť hore", + "Move down": "Presunúť dole", + "Apply": "Použiť", + "Cancel": "Zrušiť", + "New Page": "Nová stránka", + "New Menu": "Nové menu", + "New Content Block": "Nový blok obsahu", + "Fields": "Polia", + "Preview": "Náhľad", + "Add subpage": "Pridať podstránku", + "You don't have permissions to manage :type documents.": "Nemáte oprávnenie na správu dokumentov typu :type.", + "Content files cannot be saved in the static pages directory.": "Ukladanie súborov s obsahom do adresára statických stránok nie je povolené." +} diff --git a/lang/sk/lang.php b/lang/sk/lang.php deleted file mode 100644 index e3977a4e..00000000 --- a/lang/sk/lang.php +++ /dev/null @@ -1,121 +0,0 @@ - [ - 'name' => 'Stránky', - 'description' => 'Funkcie pre správu stránok a menu.', - ], - 'page' => [ - 'menu_label' => 'Stránky', - 'template_title' => '%s Stránky', - 'delete_confirmation' => 'Naozaj chcete odstrániť vybrané stránky? Ak existujú nejaké podstránky, budú taktiež odstránené.', - 'no_records' => 'Neboli nájdené žiadne stránky', - 'delete_confirm_single' => 'Naozaj chcete odstrániť túto stránku? Ak existujú nejaké podstránky, budú taktiež odstránené.', - 'new' => 'Nová stránka', - 'add_subpage' => 'Pridať podstránku', - 'invalid_url' => 'Neplatný formát URL adresy. URL by mala začínať symbolom lomítka a môže obsahovať číslice, latinské písmená a nasledujúce znaky: _- /.', - 'url_not_unique' => 'Túto URL adresu už používa iná stránka.', - 'layout' => 'Layout', - 'layouts_not_found' => 'Žiadne layouty neboli nájdené', - 'saved' => 'Stránka bola úspešne uložená.', - 'tab' => 'Stránky', - 'manage_pages' => 'Správa stránok', - 'manage_menus' => 'Správa menu', - 'access_snippets' => 'Správa snippetov', - 'manage_content' => 'Správa obsahu', - ], - 'menu' => [ - 'menu_label' => 'Menu', - 'delete_confirmation' => 'Naozaj chcete odstrániť vybrané menu?', - 'no_records' => 'Neboli nájdené žiadne položky', - 'new' => 'Nové menu', - 'new_name' => 'Nové menu', - 'new_code' => 'nove-menu', - 'delete_confirm_single' => 'Naozaj chcete odstrániť toto menu?', - 'saved' => 'Menu bolo úspešne uložené', - 'name' => 'Názov', - 'code' => 'Kód', - 'items' => 'Položky menu', - 'add_subitem' => 'Pridať vnorenú položku', - 'code_required' => 'Pole kód je povinné.', - 'invalid_code' => 'Neplatný formát kódu. Kód môže obsahovať číslice, latinské písmená a nasledujúce znaky: _-', - ], - 'menuitem' => [ - 'title' => 'Názov', - 'editor_title' => 'Upraviť položku menu', - 'type' => 'Typ', - 'allow_nested_items' => 'Povoliť vnorené položky', - 'allow_nested_items_comment' => 'Vnorené položky môžu byť automaticky generované statickou stránkou alebo niektorými ďalšími typmi položiek', - 'url' => 'URL adresa', - 'reference' => 'Odkaz', - 'search_placeholder' => 'Prehľadať všetky odkazy...', - 'title_required' => 'Názov je povinný', - 'unknown_type' => 'Neznámy typ položky menu', - 'unnamed' => 'Nepomenovaná položka menu', - 'add_item' => 'Pridať Položku', - 'new_item' => 'Nová položka menu', - 'replace' => 'Nahradiť túto položku jej generovanými vnorenými položkami', - 'replace_comment' => 'Zaškrtnite toto pole pokiaľ si prajete vnorené položky menu posunúť na rovnakú úroveň akú má táto položka. Samotná položka zostane skrytá.', - 'cms_page' => 'CMS stránka', - 'cms_page_comment' => 'Vyberte stránku, ktorá sa má otvoriť po kliknutí na položku v menu.', - 'reference_required' => 'Odkaz na položku menu je povinný.', - 'url_required' => 'Adresa URL je povinná', - 'cms_page_required' => 'Prosím vyberte CMS stránku', - 'display_tab' => 'Zobrazenie', - 'hidden' => 'Skrytá', - 'hidden_comment' => 'Skryť túto položku menu pre celú webovú stránku.', - 'attributes_tab' => 'Vlastnosti', - 'code' => 'Kód', - 'code_comment' => 'Zadajte kód položky menu ak k nej chcete pristupovať prostredníctvom API.', - 'css_class' => 'CSS trieda', - 'css_class_comment' => 'Zadajte názov CSS triedy, ktorá sa ma aplikovať pre túto položku menu.', - 'external_link' => 'Externý odkaz', - 'external_link_comment' => 'Otvoriť odkaz tejto položky menu v novom okne.', - 'static_page' => 'Statická stránka', - 'all_static_pages' => 'Všetky statické stránky', - ], - 'content' => [ - 'menu_label' => 'Obsah', - 'cant_save_to_dir' => 'Ukladanie súborov s obsahom do adresára statických stránok nie je povolené.', - ], - 'sidebar' => [ - 'add' => 'Pridať', - ], - 'object' => [ - 'invalid_type' => 'Neznámy typ objektu', - 'not_found' => 'Požadovaný objekt nebol nájdený.', - ], - 'editor' => [ - 'title' => 'Názov', - 'new_title' => 'Názov novej stránky', - 'content' => 'Obsah', - 'url' => 'URL adresa', - 'filename' => 'Názov súboru', - 'layout' => 'Layout', - 'description' => 'Popis', - 'preview' => 'Náhľad', - 'enter_fullscreen' => 'Zapnúť režim celej obrazovky', - 'exit_fullscreen' => 'Vypnúť režim celej obrazovky', - 'hidden' => 'Skrytá', - 'hidden_comment' => 'Skryté stránky sú prístupné iba prihláseným používateľom.', - 'navigation_hidden' => 'Skryť v menu', - 'navigation_hidden_comment' => 'Začiarknutím tohto poľa skryjete túto stránku z automaticky generovaných menu a navigačnej cesty.', - ], - 'snippet' => [ - 'menu_label' => 'Snippety', - ], - 'component' => [ - 'static_page_name' => 'Statická stránka', - 'static_page_description' => 'Zobrazí obsah statickej stránky', - 'static_page_use_content_name' => 'Použiť pole obsah stránky', - 'static_page_use_content_description' => 'Ak nie je začiarknuté, sekcia obsahu sa nezobrazí pri úprave statickej stránky. Obsah stránky bude určený výhradne prostredníctvom zástupcov a premenných.', - 'static_page_default_name' => 'Predvolený layout', - 'static_page_default_description' => 'Nastaví tento layout ako predvolený pre nové stránky', - 'static_page_child_layout_name' => 'Layout podstránky', - 'static_page_child_layout_description' => 'Layout ktorý sa má použiť ako predvolený pre všetky nové podstránky', - 'static_menu_name' => 'Statické menu', - 'static_menu_description' => 'Zobrazí menu na stránke', - 'static_menu_code_name' => 'Menu', - 'static_menu_code_description' => 'Zadajte kód menu, ktoré má komponent zobraziť.', - 'static_breadcrumbs_name' => 'Statická navigačná cesta', - 'static_breadcrumbs_description' => 'Zobrazí navigačnú cestu na stránke.', - ], -]; diff --git a/lang/sl.json b/lang/sl.json new file mode 100644 index 00000000..c7bbb1a0 --- /dev/null +++ b/lang/sl.json @@ -0,0 +1,83 @@ +{ + "Pages": "Strani", + "Pages & menus features.": "Ustvarjanje strani in menijev.", + "Manage static pages": "Upravljanje statičnih strani", + "Manage static menus": "Upravljanje statičnih menijev", + "Manage static content": "Upravljanje statičnih vsebin", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Neveljavna oblika URL formata. URL se mora začeti z znakom za desno poševnico in lahko vsebuje številke, latinične črke in naslednje znake: _-/.", + "This URL is already used by another page.": "To URL povezavo uporablja že ena od drugih strani.", + "Layouts not found": "Ni najdenih postavitev", + "The Code is required": "Koda je obvezna", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Neveljaven format kode. Koda lahko vsebuje številke, latinične črke in naslednje znake: _-", + "Static page": "Statična stran", + "All static pages": "Vse statične strani", + "Static menu": "Statični meni", + "Static breadcrumbs": "Statične povezave", + "Child pages": "Podstrani", + "Outputs a static page in a CMS layout.": "Ustvari statično stran na CMS postavitvi.", + "Use page content field": "Uporabi polje z vsebino strani", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Če ni označeno, se razdelek z vsebino pri urejanju statične strani ne bo prikazal. Vsebina strani bo določena izključno prek vsebinskih okvirov in spremenljivk.", + "Default layout": "Privzeta postavitev", + "Defines this layout as the default for new pages": "To postavitev definira kot privzeto za nove strani", + "Subpage layout": "Postavitev podstrani", + "The layout to use as the default for any new subpages": "To postavitev definira kot privzeto za vse nove podstrani", + "Outputs a menu in a CMS layout.": "Ustvari meni na CMS postavitvi.", + "Menu": "Meni", + "Specify a code of the menu the component should output.": "Določite kodo menija, ki ga mora sestaviti komponenta.", + "Outputs breadcrumbs for a static page.": "Ustvari povezave za statično stran.", + "Displays a list of child pages for the current page": "Prikaže seznam podstrani za trenutno stran", + "Static Pages": "Statične strani", + "Menus": "Meniji", + "Content": "Vsebine", + "Add": "Dodaj", + "Refresh": "Osveži", + "Page": "Stran", + "Content block": "Vsebinski blok", + "New page": "Nova stran", + "New page title": "Nov naslov strani", + "New menu": "Nov meni", + "New content block": "Nov vsebinski blok", + "Title": "Naslov", + "URL": "URL", + "File Name": "Ime datoteke", + "Layout": "Postavitev", + "Hidden": "Skrita stran", + "Hide in navigation": "Skrita v navigaciji", + "Description": "Opis", + "Name": "Ime", + "Code": "Koda", + "The Title is required.": "Naslov je obvezen.", + "The URL is required.": "Povezava URL je obvezna.", + "The File Name is required.": "Ime datoteke je obvezno.", + "The Name is required.": "Ime je obvezno.", + "The Code is required.": "Koda je obvezna.", + "Error loading page": "Napaka pri nalaganju strani", + "Error loading menu": "Napaka pri nalaganju menija", + "Error loading content block": "Napaka pri nalaganju vsebinskega bloka", + "Add item": "Dodaj element", + "Add subitem": "Dodaj pod-element", + "New menu item": "Nov element menija", + "Untitled": "Brez naslova", + "Up": "Gor", + "Down": "Dol", + "Indent": "Povečaj zamik", + "Outdent": "Zmanjšaj zamik", + "Delete": "Izbriši", + "No menu items yet. Use \"Add item\" in the toolbar.": "Ni še elementov menija. Uporabite \"Dodaj element\" v orodni vrstici.", + "Select a menu item to edit, or add a new one.": "Izberite element menija za urejanje ali dodajte novega.", + "Custom Fields": "Polja po meri", + "Search all references...": "Išči po vseh referencah...", + "Edit Menu Item": "Uredi element menija", + "Move up": "Premakni gor", + "Move down": "Premakni dol", + "Apply": "Uporabi", + "Cancel": "Prekliči", + "New Page": "Nova stran", + "New Menu": "Nov meni", + "New Content Block": "Nov vsebinski blok", + "Fields": "Polja", + "Preview": "Predogled", + "Add subpage": "Dodaj podstran", + "You don't have permissions to manage :type documents.": "Nimate pooblastil za upravljanje :type dokumentov.", + "Content files cannot be saved in the static pages directory.": "Shranjevanje datotek z vsebino v mapo statičnih strani ni dovoljeno." +} diff --git a/lang/sl/lang.php b/lang/sl/lang.php deleted file mode 100644 index ed24298e..00000000 --- a/lang/sl/lang.php +++ /dev/null @@ -1,125 +0,0 @@ - [ - 'name' => 'Strani', - 'description' => 'Ustvarjanje strani in menijev.', - ], - 'page' => [ - 'menu_label' => 'Strani', - 'template_title' => '%s strani', - 'delete_confirmation' => 'Ali ste prepričani, da želite izbrisati izbrane strani? S tem boste izbrisali tudi njihove podstrani, če obstajajo.', - 'no_records' => 'Ni najdenih strani.', - 'delete_confirm_single' => 'Ali ste prepričani, da želite izbrisati to stran? S tem boste izbrisali tudi njene podstrani, če obstajajo.', - 'new' => 'Nova stran', - 'add_subpage' => 'Dodaj podstran', - 'invalid_url' => 'Neveljavna oblika URL formata. URL se mora začeti z znakom za desno poševnico in lahko vsebuje številke, latinične črke in naslednje znake: _-/.', - 'url_not_unique' => 'To URL povezavo uporablja že ena od drugih strani.', - 'layout' => 'Postavitev', - 'layouts_not_found' => 'Ni najdenih postavitev.', - 'saved' => 'Stran je bila uspešno shranjena.', - 'tab' => 'Strani', - 'manage_pages' => 'Upravljanje statičnih strani', - 'manage_menus' => 'Upravljanje statičnih menijev', - 'access_snippets' => 'Dostop do gradnikov', - 'manage_content' => 'Upravljanje statičnih vsebin', - ], - 'menu' => [ - 'menu_label' => 'Meniji', - 'delete_confirmation' => 'Ali ste prepričani, da želite izbrisati izbrane menije?', - 'no_records' => 'Ni najdenih menijev.', - 'new' => 'Nov meni', - 'new_name' => 'Nov meni', - 'new_code' => 'nov-meni', - 'delete_confirm_single' => 'Ali ste prepričani, da želite izbrisati ta meni?', - 'saved' => 'Meni je uspešno shranjen.', - 'name' => 'Ime', - 'code' => 'Koda', - 'items' => 'Elementi menija', - 'add_subitem' => 'Dodaj pod-element', - 'code_required' => 'Koda je obvezna.', - 'invalid_code' => 'Neveljaven format kode. Koda lahko vsebuje številke, latinične črke in naslednje znake: _-', - ], - 'menuitem' => [ - 'title' => 'Naslov', - 'editor_title' => 'Element menija', - 'type' => 'Vrsta', - 'allow_nested_items' => 'Dovoli gnezdene elemente', - 'allow_nested_items_comment' => 'Gnezdene elemente lahko dinamično ustvarijo statične strani in nekatere druge vrste elementov.', - 'url' => 'URL', - 'reference' => 'Referenca', - 'search_placeholder' => 'Išči po vseh referencah...', - 'title_required' => 'Naslov je obvezen', - 'unknown_type' => 'Neznana vrsta elementa menija.', - 'unnamed' => 'Neimenovan element menija.', - 'add_item' => 'Dodaj element', - 'new_item' => 'Nov element menija', - 'replace' => 'Zamenjaj ta element z njegovimi pod-elementi', - 'replace_comment' => 'Z uporabo tega kvadratka lahko potisnete ustvarjene elemente menija na njegov nivo, ob tem pa bo le-ta element postal skrit.', - 'cms_page' => 'CMS stran', - 'cms_page_comment' => 'Izberite stran, ki naj se odpre ob kliku na element menija.', - 'reference_required' => 'Referenca elementa menija je obvezna.', - 'url_required' => 'Povezava URL je obvezna.', - 'cms_page_required' => 'Prosimo, izberite CMS stran.', - 'display_tab' => 'Prikaz', - 'hidden' => 'Skrito', - 'hidden_comment' => 'Element menija na spletni strani naj ne bo prikazan.', - 'attributes_tab' => 'Atributi', - 'code' => 'Koda', - 'code_comment' => 'Vnesite kodo za element menija, če želite do njega omogočiti API dostop.', - 'css_class' => 'CSS razred', - 'css_class_comment' => 'Vnesite ime CSS razreda, če želite elementu omogočiti videz po meri.', - 'external_link' => 'Zunanja povezava', - 'external_link_comment' => 'Povezava za ta element menija naj se odpre v novem oknu.', - 'static_page' => 'Statična stran', - 'all_static_pages' => 'Vse statične strani', - ], - 'content' => [ - 'menu_label' => 'Vsebine', - 'saved' => 'Vsebina je bila uspešno shranjena.', - 'cant_save_to_dir' => 'Shranjevanje datotek z vsebino v mapo statičnih strani ni dovoljeno.', - ], - 'sidebar' => [ - 'add' => 'Dodaj', - ], - 'object' => [ - 'invalid_type' => 'Neznana vrsta objekta', - 'unauthorized_type' => 'Nimate pooblastil za upravljanje :type objektov.', - 'not_found' => 'Zahtevanega objekta ni mogoče najti.', - ], - 'editor' => [ - 'title' => 'Naslov', - 'new_title' => 'Nov naslov strani', - 'content' => 'Vsebina', - 'url' => 'URL', - 'filename' => 'Ime datoteke', - 'layout' => 'Postavitev', - 'description' => 'Opis', - 'preview' => 'Predogled', - 'enter_fullscreen' => 'Celozaslonski način', - 'exit_fullscreen' => 'Zapri celozaslonski način', - 'hidden' => 'Skrita stran', - 'hidden_comment' => 'Skrite strani so dostopne le prijavljenim administratorjem.', - 'navigation_hidden' => 'Skrita v navigaciji', - 'navigation_hidden_comment' => 'Skrite strani v navigaciji se v menijih in povezavah ne prikažejo.', - ], - 'snippet' => [ - 'menu_label' => 'Gradniki', - ], - 'component' => [ - 'static_page_name' => 'Statična stran', - 'static_page_description' => 'Ustvari statično stran na CMS postavitvi.', - 'static_page_use_content_name' => 'Uporabi polje z vsebino strani', - 'static_page_use_content_description' => 'Če ni označeno, se razdelek z vsebino pri urejanju statične strani ne bo prikazal. Vsebina strani bo določena izključno prek vsebinskih okvirov in spremenljivk.', - 'static_page_default_name' => 'Privzeta postavitev', - 'static_page_default_description' => 'To postavitev definira kot privzeto za nove strani.', - 'static_page_child_layout_name' => 'Postavitev podstrani', - 'static_page_child_layout_description' => 'To postavitev definira kot privzeto za vse nove podstrani.', - 'static_menu_name' => 'Statični meni', - 'static_menu_description' => 'Ustvari meni na CMS postavitvi.', - 'static_menu_code_name' => 'Meni', - 'static_menu_code_description' => 'Določite kodo menija, ki ga mora sestaviti komponenta.', - 'static_breadcrumbs_name' => 'Statične povezave', - 'static_breadcrumbs_description' => 'Ustvari povezave za statično stran.', - 'child_pages_name' => 'Podstrani', - 'child_pages_description' => 'Prikaže seznam podstrani za trenutno stran.', - ], -]; diff --git a/lang/sv.json b/lang/sv.json new file mode 100644 index 00000000..a445162b --- /dev/null +++ b/lang/sv.json @@ -0,0 +1,83 @@ +{ + "Pages": "Sidor", + "Pages & menus features.": "Sidor & menyer.", + "Manage static pages": "Hantera statiska sidor", + "Manage static menus": "Hantera statiska menyer", + "Manage static content": "Hantera statiskt innehåll", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Ogiltigt format på URL. URL:en ska börja med slash och kan innehålla siffror, latinska bokstäver och följande symboler: _-/.", + "This URL is already used by another page.": "Denna URL används redan av en annan sida.", + "Layouts not found": "Layouter kan inte hittas", + "The Code is required": "Koden är obligatorisk", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Ogiltigt kodformat. Koden kan innehålla siffror, latinska bokstäver och följande symboler: _-", + "Static page": "Statisk sida", + "All static pages": "Alla statiska sidor", + "Static menu": "Statisk meny", + "Static breadcrumbs": "Statiska sökvägar", + "Child pages": "Undersidor", + "Outputs a static page in a CMS layout.": "Visar en statisk sida i en CMS-layout.", + "Use page content field": "Använd sidans innehållsfält", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Om detta inte är ikryssat kommer innehållssektionen inte att visas när den statiska sidan redigeras. Sidans innehåll bestäms då enbart genom platshållare och variabler.", + "Default layout": "Standardlayout", + "Defines this layout as the default for new pages": "Anger denna layout som standard för nya sidor", + "Subpage layout": "Layout för undersidor", + "The layout to use as the default for any new subpages": "Layouten som används som standard för alla nya undersidor", + "Outputs a menu in a CMS layout.": "Visar en meny i en CMS-layout.", + "Menu": "Meny", + "Specify a code of the menu the component should output.": "Ange koden för menyn som komponenten ska visa.", + "Outputs breadcrumbs for a static page.": "Visar sökvägar för en statisk sida.", + "Displays a list of child pages for the current page": "Visar en lista med undersidor för den aktuella sidan", + "Static Pages": "Statiska sidor", + "Menus": "Menyer", + "Content": "Innehåll", + "Add": "Lägg till", + "Refresh": "Uppdatera", + "Page": "Sida", + "Content block": "Innehållsblock", + "New page": "Ny sida", + "New page title": "Ny sidrubrik", + "New menu": "Ny meny", + "New content block": "Nytt innehållsblock", + "Title": "Rubrik", + "URL": "URL", + "File Name": "Filnamn", + "Layout": "Layout", + "Hidden": "Gömd", + "Hide in navigation": "Göm i navigation", + "Description": "Beskrivning", + "Name": "Namn", + "Code": "Kod", + "The Title is required.": "Rubriken är obligatorisk.", + "The URL is required.": "URL:en är obligatorisk.", + "The File Name is required.": "Filnamnet är obligatoriskt.", + "The Name is required.": "Namnet är obligatoriskt.", + "The Code is required.": "Koden är obligatorisk.", + "Error loading page": "Fel vid inläsning av sidan", + "Error loading menu": "Fel vid inläsning av menyn", + "Error loading content block": "Fel vid inläsning av innehållsblocket", + "Add item": "Lägg till föremål", + "Add subitem": "Lägg till underföremål", + "New menu item": "Nytt menyföremål", + "Untitled": "Namnlös", + "Up": "Upp", + "Down": "Ner", + "Indent": "Öka indrag", + "Outdent": "Minska indrag", + "Delete": "Ta bort", + "No menu items yet. Use \"Add item\" in the toolbar.": "Inga menyföremål ännu. Använd \"Lägg till föremål\" i verktygsfältet.", + "Select a menu item to edit, or add a new one.": "Välj ett menyföremål att redigera, eller lägg till ett nytt.", + "Custom Fields": "Anpassade fält", + "Search all references...": "Sök bland alla referenser...", + "Edit Menu Item": "Redigera menyföremål", + "Move up": "Flytta upp", + "Move down": "Flytta ner", + "Apply": "Tillämpa", + "Cancel": "Avbryt", + "New Page": "Ny sida", + "New Menu": "Ny meny", + "New Content Block": "Nytt innehållsblock", + "Fields": "Fält", + "Preview": "Förhandsgranska", + "Add subpage": "Lägg till undersida", + "You don't have permissions to manage :type documents.": "Du har inte behörighet att hantera :type-dokument.", + "Content files cannot be saved in the static pages directory.": "Innehållsfiler kan inte sparas i mappen för statiska sidor." +} diff --git a/lang/sv/lang.php b/lang/sv/lang.php deleted file mode 100644 index 496e468d..00000000 --- a/lang/sv/lang.php +++ /dev/null @@ -1,94 +0,0 @@ - [ - 'name' => 'Sidor', - 'description' => 'Sidor & menyer.', - ], - 'page' => [ - 'menu_label' => 'Sidor', - 'template_title' => '%s Sidor', - 'delete_confirmation' => 'Vill du verkligen ta bort de valda sidorna? Detta kommer också ta bort undersidorna, ifall det finns några.', - 'no_records' => 'Inga sidor hittades', - 'delete_confirm_single' => 'Vill du verkligen ta bort den valda sidan? Detta kommer också ta bort sidans undersidor, ifall det finns några.', - 'new' => 'Ny sida', - 'add_subpage' => 'Lägg till undersida', - 'invalid_url' => 'Ogiltigt format på URL. URL:en ska börja med slash och kan innehålla siffror, latinska bokstäver och följande symboler: _-/', - 'url_not_unique' => 'Denna URL används redan av en annan sida.', - 'layout' => 'Layout', - 'layouts_not_found' => 'Layouter kan inte hittas', - 'saved' => 'Sidan har sparats.', - 'tab' => 'Sidor', - 'manage_pages' => 'Hantera statiska sidor', - 'manage_menus' => 'Hantera statiska menyer', - 'access_snippets' => 'Hantera stumpar', - 'manage_content' => 'Hantera statiskt innehåll', - ], - 'menu' => [ - 'menu_label' => 'Menyer', - 'delete_confirmation' => 'Vill du verkligen ta bort valda de menyerna?', - 'no_records' => 'Inga föremål kunde finnas', - 'new' => 'Ny meny', - 'new_name' => 'Ny meny', - 'new_code' => 'ny-meny', - 'delete_confirm_single' => 'Vill du verkligen ta bort denna menyn?', - 'saved' => 'Menyn har sparats.', - 'name' => 'Namn', - 'code' => 'Kod', - 'items' => 'Menyföremål', - 'add_subitem' => 'Lägg till underföremål', - 'code_required' => 'Koden är obligatorisk', - 'invalid_code' => 'Ogiltigt kodformat. Koden kan innehålla siffror, latinska bokstäver och följande symboler: _-', - ], - 'menuitem' => [ - 'title' => 'Rubrik', - 'editor_title' => 'Redigera menyföremål', - 'type' => 'Typ', - 'allow_nested_items' => 'Tillåt underliggande föremål', - 'allow_nested_items_comment' => 'Underliggande föremål kan skapas dynamiskt av en etatisk sida och några andra föremålstyper', - 'url' => 'URL', - 'reference' => 'Referens', - 'title_required' => 'Rubriken är obligatorisk', - 'unknown_type' => 'Okänd menyföremålstyp', - 'unnamed' => 'Namnlöst menyföremål', - 'add_item' => 'Lägg till föremål', - 'new_item' => 'Nytt menyföremål', - 'replace' => 'Ersätt detta föremål med dens skapade underföremål', - 'replace_comment' => 'Använd denna kryssruta för att föra skapade menyföremål till samma nivå som detta föremålet. Detta föremålet kommer att gömmas.', - 'cms_page' => 'CMS-sida', - 'cms_page_comment' => 'Välj en sida som ska öppnas när menyföremålet klickas.', - 'reference_required' => 'Menyföremålets referens är obligatorisk.', - 'url_required' => 'URL:en är obligatorisk', - 'cms_page_required' => 'Vänligen välj en CMS-sida', - 'code' => 'Kod', - 'code_comment' => 'Fyll i menyföremålets kod om du vill få tillgång till det i API:t.', - ], - 'content' => [ - 'menu_label' => 'Innehåll', - 'cant_save_to_dir' => 'Att sparar innehållsfiler till mappen för statiska sidor är inte tillåtet.', - ], - 'sidebar' => [ - 'add' => 'Lägg till', - ], - 'object' => [ - 'invalid_type' => 'Ogiltig objekttyp', - 'not_found' => 'Det begärda objektet kunde inte finnas.', - ], - 'editor' => [ - 'title' => 'Rubrik', - 'new_title' => 'Ny sidrubrik', - 'content' => 'Innehåll', - 'url' => 'URL', - 'filename' => 'Filnamn', - 'layout' => 'Layout', - 'description' => 'Beskrivning', - 'preview' => 'Förhandsgranska', - 'enter_fullscreen' => 'Gå in i fullskärmsläge', - 'exit_fullscreen' => 'Gå ut ur fullskärmsläge', - 'hidden' => 'Gömd', - 'hidden_comment' => 'Gömda sidor är bara tillgängliga för inloggande back-endanvändare.', - 'navigation_hidden' => 'Göm i navigation', - 'navigation_hidden_comment' => 'Fyll i denna rutan för att gömma sidan från automatiskt skapade menyer och sökvägar.', - ], - 'snippet' => [ - 'menu_label' => 'Stumpar', - ], -]; diff --git a/lang/tr.json b/lang/tr.json new file mode 100644 index 00000000..536c42ac --- /dev/null +++ b/lang/tr.json @@ -0,0 +1,83 @@ +{ + "Pages": "Sayfalar", + "Pages & menus features.": "Sayfalar & menüler modülü.", + "Manage static pages": "Sayfaları yönetebilsin", + "Manage static menus": "Menüleri yönetebilsin", + "Manage static content": "Sabit içerikleri yönetebilsin", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Geçersiz URL formatı. URL eğik çizgi sembolü ile başlamalıdır ve rakam, latin harfleri ve bu sembolleri: _-/. içerebilir.", + "This URL is already used by another page.": "Bu URL başka bir sayfa tarafından kullanılıyor.", + "Layouts not found": "Şablon bulunamadı", + "The Code is required": "Kod gerekli", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Geçersiz KOD formatı. Kod yalnızca rakam, Latin harfleri ve bu sembolleri: _- içerebilir.", + "Static page": "Sabit sayfa", + "All static pages": "Tüm sayfalar", + "Static menu": "Statik (sabit) menü", + "Static breadcrumbs": "Sabit breadcrumbs", + "Child pages": "Alt sayfalar", + "Outputs a static page in a CMS layout.": "CMS bölümüne sabit sayfa içeriği ekler.", + "Use page content field": "Sayfa içeriği alanını kullan", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Seçilmezse, statik sayfa düzenlenirken içerik bölümü görünmez. Sayfa içeriği yalnızca placeholderlar ve değişkenler aracılığıyla belirlenir.", + "Default layout": "Varsayılan şablon", + "Defines this layout as the default for new pages": "Bu şablonu yeni sayfalar için varsayılan olarak tanımlar", + "Subpage layout": "Alt sayfa şablonu", + "The layout to use as the default for any new subpages": "Yeni alt sayfalar için varsayılan olarak kullanılacak şablon", + "Outputs a menu in a CMS layout.": "CMS bölümüne sabit menü içeriği ekler.", + "Menu": "Menü", + "Specify a code of the menu the component should output.": "Bileşenin göstereceği menünün kodunu belirtin.", + "Outputs breadcrumbs for a static page.": "Sabit sayfaya breadcrumbs ekler.", + "Displays a list of child pages for the current page": "Geçerli sayfanın alt sayfalarının listesini görüntüler", + "Static Pages": "Statik Sayfalar", + "Menus": "Menüler", + "Content": "İçerik", + "Add": "Ekle", + "Refresh": "Yenile", + "Page": "Sayfa", + "Content block": "İçerik bloğu", + "New page": "Yeni sayfa", + "New page title": "Yeni sayfa başlığı", + "New menu": "Yeni menü", + "New content block": "Yeni içerik bloğu", + "Title": "Başlık", + "URL": "URL", + "File Name": "Dosya Adı", + "Layout": "Şablon", + "Hidden": "Gizli", + "Hide in navigation": "Menüde Gizle", + "Description": "Tanımlama", + "Name": "İsim", + "Code": "Kod", + "The Title is required.": "Başlık gerekli.", + "The URL is required.": "URL gereklidir.", + "The File Name is required.": "Dosya Adı gereklidir.", + "The Name is required.": "İsim gereklidir.", + "The Code is required.": "Kod gerekli.", + "Error loading page": "Sayfa yüklenirken hata oluştu", + "Error loading menu": "Menü yüklenirken hata oluştu", + "Error loading content block": "İçerik bloğu yüklenirken hata oluştu", + "Add item": "Öge Ekle", + "Add subitem": "Altöge ekle", + "New menu item": "Yeni menü ögesi", + "Untitled": "Başlıksız", + "Up": "Yukarı", + "Down": "Aşağı", + "Indent": "Girintiyi artır", + "Outdent": "Girintiyi azalt", + "Delete": "Sil", + "No menu items yet. Use \"Add item\" in the toolbar.": "Henüz menü ögesi yok. Araç çubuğundaki \"Öge Ekle\" seçeneğini kullanın.", + "Select a menu item to edit, or add a new one.": "Düzenlemek için bir menü ögesi seçin veya yeni bir tane ekleyin.", + "Custom Fields": "Özel Alanlar", + "Search all references...": "Referansları ara...", + "Edit Menu Item": "Menü Ögesini Düzenle", + "Move up": "Yukarı taşı", + "Move down": "Aşağı taşı", + "Apply": "Uygula", + "Cancel": "İptal", + "New Page": "Yeni Sayfa", + "New Menu": "Yeni Menü", + "New Content Block": "Yeni İçerik Bloğu", + "Fields": "Alanlar", + "Preview": "Önizleme", + "Add subpage": "Altsayfa ekle", + "You don't have permissions to manage :type documents.": ":type belgelerini yönetme izniniz yok.", + "Content files cannot be saved in the static pages directory.": "İçerik dosyaları statik sayfalar dizinine kaydedilemez." +} diff --git a/lang/tr/lang.php b/lang/tr/lang.php deleted file mode 100644 index f910697b..00000000 --- a/lang/tr/lang.php +++ /dev/null @@ -1,121 +0,0 @@ - [ - 'name' => 'Sayfalar', - 'description' => 'Sayfalar & menüler modülü.', - ], - 'page' => [ - 'menu_label' => 'Sayfalar', - 'template_title' => '%s Sayfalar', - 'delete_confirmation' => 'Seçili sayfaları silmek istiyor musunuz? Alt sayfalar da silinecektir.', - 'no_records' => 'Sayfa bulunamadı', - 'delete_confirm_single' => 'Bu sayfayı silmek istiyor musunuz? Alt sayfalar da silinecektir', - 'new' => 'Yeni sayfa', - 'add_subpage' => 'Altsayfa ekle', - 'invalid_url' => 'Geçersiz URL formatı. URL eğik çizgi sembolü ile başlamalıdır ve rakam, latin harfleri ve bu sembolleri: _-/. içerebilir.', - 'url_not_unique' => 'Bu URL başka bir sayfa tarafından kullanılıyor', - 'layout' => 'Şablon', - 'layouts_not_found' => 'Şablon bulunamadı', - 'saved' => 'Sayfa başarıyla kaydedildi.', - 'tab' => 'Sayfalar', - 'manage_pages' => 'Sayfaları yönetebilsin', - 'manage_menus' => 'Menüleri yönetebilsin', - 'access_snippets' => 'Snippetleri yönetebilsin', - 'manage_content' => 'Sabit içerikleri yönetebilsin', - ], - 'menu' => [ - 'menu_label' => 'Menüler', - 'delete_confirmation' => 'Seçili menüleri silmek istiyor musunuz?', - 'no_records' => 'Menü bulunamadı', - 'new' => 'Yeni Menü', - 'new_name' => 'Yeni menü', - 'new_code' => 'yeni-menu', - 'delete_confirm_single' => 'Bu menüyü silmek istiyor musunuz?', - 'saved' => 'Menü başarıyla kaydedildi.', - 'name' => 'İsim', - 'code' => 'Kod', - 'items' => 'Menü Ögeleri', - 'add_subitem' => 'Altöge ekle', - 'code_required' => 'Kod gerekli', - 'invalid_code' => 'Geçersiz KOD formatı. Kod yalnızca rakam, Latin harfleri ve bu sembolleri: _- içerebilir.', - ], - 'menuitem' => [ - 'title' => 'Başlık', - 'editor_title' => 'Menü Ögesini Düzenle', - 'type' => 'Tür', - 'allow_nested_items' => 'İçiçe ögelere izin ver', - 'allow_nested_items_comment' => 'İç içe öğeler statik sayfa ve bazı diğer öğe türlerine göre dinamik olarak üretilen olabilir', - 'url' => 'URL', - 'reference' => 'Referans', - 'search_placeholder' => 'Referansları ara...', - 'title_required' => 'Başlık gerekli', - 'unknown_type' => 'Geçersiz menü ögesi türü', - 'unnamed' => 'İsimsiz menü ögesi', - 'add_item' => 'Öge Ekle', - 'new_item' => 'Yeni menü ögesi', - 'replace' => 'Bu ögeyi oluşturulan çocuklarıyla değiştir', - 'replace_comment' => 'Use this checkbox to push generated menu items to the same level with this item. This item itself will be hidden.', - 'cms_page' => 'CMS Sayfası', - 'cms_page_comment' => 'Menü ögesine tıklandığında açılacak sayfayı seçin', - 'reference_required' => 'Menü ögesi referansı gereklidir.', - 'url_required' => 'URL gereklidir', - 'cms_page_required' => 'Lütfen bir CMS sayfası seçin', - 'display_tab' => 'Görünüm', - 'hidden' => 'Gizli', - 'hidden_comment' => 'Bu menüyü önyüzde gizle.', - 'attributes_tab' => 'Öznitellikler', - 'code' => 'Kod', - 'code_comment' => 'API ile giriş yapabilmek için menü ögesi kodunu girin.', - 'css_class' => 'CSS Class', - 'css_class_comment' => 'Bu menüye özel bir görünüm vermek için bir CSS sınıfı adı girin.', - 'external_link' => 'Dış link', - 'external_link_comment' => 'Bu menü için bağlantıları yeni sekmede aç.', - 'static_page' => 'Sayfa', - 'all_static_pages' => 'Tüm sayfalar', - ], - 'content' => [ - 'menu_label' => 'İçerik', - 'cant_save_to_dir' => 'Statik sayfalar dizinine içerik dosyalarını kaydetme izni verilmez.', - ], - 'sidebar' => [ - 'add' => 'Ekle', - ], - 'object' => [ - 'invalid_type' => 'Bilineyen nesne türü', - 'not_found' => 'İstenen nesne bulunamadı', - ], - 'editor' => [ - 'title' => 'Başlık', - 'new_title' => 'Yeni sayfa başlığı', - 'content' => 'İçerik', - 'url' => 'URL', - 'filename' => 'Dosya Adı', - 'layout' => 'Layout', - 'description' => 'Tanımlama', - 'preview' => 'Önizleme', - 'enter_fullscreen' => 'Tam Ekran moduna geç', - 'exit_fullscreen' => 'Tam Ekran modundan çık', - 'hidden' => 'Gizli', - 'hidden_comment' => 'Gizli sayfalar yalnızca yönetim paneline giriş yapmış kullanıcılar tarafından görüntülenebilir.', - 'navigation_hidden' => 'Menüde Gizle', - 'navigation_hidden_comment' => 'Otomatik olarak oluşturulan menüler ve kırıntıları gizlemek için bu kutuyu işaretleyin.', - ], - 'snippet' => [ - 'menu_label' => 'Snippetlar', - ], - 'component' => [ - 'static_page_name' => 'Sabit sayfa', - 'static_page_description' => 'CMS bölümüne sabit sayfa içeriği ekler.', - 'static_page_use_content_name' => 'Sayfa içeriği alanını kullan', - 'static_page_use_content_description' => 'Seçilmezse, statik sayfa düzenlenirken içerik bölümü görünmez. Sayfa içeriği yalnızca placeholderlar ve değişkenler aracılığıyla belirlenir.', - 'static_page_default_name' => 'Varsayılan şablon', - 'static_page_default_description' => 'Bu şablonu yeni sayfalar için varsayılan olarak tanımlar.', - 'static_page_child_layout_name' => 'Alt sayfa şablobu', - 'static_page_child_layout_description' => 'Yeni alt sayfalar için varsayılan olarak kullanılacak şablon', - 'static_menu_name' => 'Statik (sabit) menü', - 'static_menu_description' => 'CMS bölümüne sabit menü içeriği ekler.', - 'static_menu_code_name' => 'Menü', - 'static_menu_code_description' => 'Component in göstereceği menünün kodunu belirtin.', - 'static_breadcrumbs_name' => 'Sabit breadcrumbs', - 'static_breadcrumbs_description' => 'Sabit sayfaya breadcrumbs ekler.', - ], -]; diff --git a/lang/uk.json b/lang/uk.json new file mode 100644 index 00000000..f36c712d --- /dev/null +++ b/lang/uk.json @@ -0,0 +1,83 @@ +{ + "Pages": "Сторінки", + "Pages & menus features.": "Сторінки і меню.", + "Manage static pages": "Управління сторінками", + "Manage static menus": "Управління меню", + "Manage static content": "Управління змістом", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "Некоректний формат URL. URL повинен починатися з прямого слеша і може містити цифри, латинські літери і такі символи: _-/.", + "This URL is already used by another page.": "Цей URL вже використовується іншою сторінкою.", + "Layouts not found": "Шаблони не знайдені", + "The Code is required": "Поле Код обов'язкове", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "Некоректний формат Коду. Код може містити цифри, латинські літери і такі символи: _-", + "Static page": "Статична сторінка", + "All static pages": "Усі статичні сторінки", + "Static menu": "Статичне меню", + "Static breadcrumbs": "Статичні хлібні крихти", + "Child pages": "Дочірні сторінки", + "Outputs a static page in a CMS layout.": "Виводити сторінку в CMS шаблоні.", + "Use page content field": "Використовувати поле вмісту сторінки", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "Якщо не відмічено, розділ вмісту не з'являтиметься під час редагування статичної сторінки. Вміст сторінки визначатиметься лише через плейсхолдери та змінні.", + "Default layout": "Шаблон за замовчуванням", + "Defines this layout as the default for new pages": "Визначає цей шаблон як шаблон за замовчуванням для нових сторінок", + "Subpage layout": "Шаблон підсторінки", + "The layout to use as the default for any new subpages": "Шаблон, який використовуватиметься за замовчуванням для нових підсторінок", + "Outputs a menu in a CMS layout.": "Виводити меню в CMS шаблоні.", + "Menu": "Меню", + "Specify a code of the menu the component should output.": "Вкажіть код меню, яке повинно бути показано.", + "Outputs breadcrumbs for a static page.": "Виводити хлібні крихти для сторінки.", + "Displays a list of child pages for the current page": "Відображає список дочірніх сторінок для поточної сторінки", + "Static Pages": "Статичні сторінки", + "Menus": "Меню", + "Content": "Зміст", + "Add": "Додати", + "Refresh": "Оновити", + "Page": "Сторінка", + "Content block": "Блок вмісту", + "New page": "Нова сторінка", + "New page title": "Назва нової сторінки", + "New menu": "Нове меню", + "New content block": "Новий блок вмісту", + "Title": "Назва", + "URL": "URL", + "File Name": "Ім'я файлу", + "Layout": "Шаблон", + "Hidden": "Прихований", + "Hide in navigation": "Сховати в навігації", + "Description": "Опис", + "Name": "Ім'я", + "Code": "Код", + "The Title is required.": "Назва обов'язкова.", + "The URL is required.": "Необхідний URL.", + "The File Name is required.": "Ім'я файлу обов'язкове.", + "The Name is required.": "Ім'я обов'язкове.", + "The Code is required.": "Поле Код обов'язкове.", + "Error loading page": "Помилка завантаження сторінки", + "Error loading menu": "Помилка завантаження меню", + "Error loading content block": "Помилка завантаження блоку вмісту", + "Add item": "Додати пункт", + "Add subitem": "Додати підменю", + "New menu item": "Новий пункт", + "Untitled": "Без назви", + "Up": "Вгору", + "Down": "Вниз", + "Indent": "Збільшити відступ", + "Outdent": "Зменшити відступ", + "Delete": "Видалити", + "No menu items yet. Use \"Add item\" in the toolbar.": "Пунктів меню ще немає. Скористайтеся кнопкою \"Додати пункт\" на панелі інструментів.", + "Select a menu item to edit, or add a new one.": "Виберіть пункт меню для редагування або додайте новий.", + "Custom Fields": "Користувацькі поля", + "Search all references...": "Пошук за всіма посиланнями...", + "Edit Menu Item": "Редагувати пункт меню", + "Move up": "Перемістити вгору", + "Move down": "Перемістити вниз", + "Apply": "Застосувати", + "Cancel": "Скасувати", + "New Page": "Нова сторінка", + "New Menu": "Нове меню", + "New Content Block": "Новий блок вмісту", + "Fields": "Поля", + "Preview": "Попередній огляд", + "Add subpage": "Додати підсторінку", + "You don't have permissions to manage :type documents.": "У вас немає прав на керування документами типу :type.", + "Content files cannot be saved in the static pages directory.": "Файли вмісту не можна зберігати в директорії статичних сторінок." +} diff --git a/lang/uk/lang.php b/lang/uk/lang.php deleted file mode 100644 index 6d2f50c5..00000000 --- a/lang/uk/lang.php +++ /dev/null @@ -1,100 +0,0 @@ - [ - 'name' => 'Сторінки', - 'description' => 'Сторінки і меню.', - ], - 'page' => [ - 'menu_label' => 'Сторінки', - 'template_title' => '%s Сторінки', - 'delete_confirmation' => 'Ви дійсно хочете видалити вибрані сторінки? Це також видалить наявні підсторінки.', - 'no_records' => 'Сторінок не знайдено', - 'delete_confirm_single' => 'Ви дійсно хочете видалити цю сторінку? Це також видалить наявні підсторінки.', - 'new' => 'Нова сторінка', - 'add_subpage' => 'Додати підсторінку', - 'invalid_url' => 'Некоректний формат URL. URL повинен починатися з прямого слеша і може містити цифри, латинські літери і такі символи: _-/.', - 'url_not_unique' => 'Цей URL вже використовується іншою сторінкою.', - 'layout' => 'Шаблон', - 'layouts_not_found' => 'Шаблони не знайдені', - 'saved' => 'Сторінка була успішно збережена.', - 'tab' => 'Сторінки', - 'manage_pages' => 'Управління сторінками', - 'manage_menus' => 'Управління меню', - 'access_snippets' => 'Доступ до сніппетів', - 'manage_content' => 'Управління змістом', - ], - 'menu' => [ - 'menu_label' => 'Меню', - 'delete_confirmation' => 'Ви дійсно хочете видалити вибрані пункти меню?', - 'no_records' => 'Меню не знайдені', - 'new' => 'Нове меню', - 'new_name' => 'Новое меню', - 'new_code' => 'nove-menu', - 'delete_confirm_single' => 'Ви дійсно хочете видалити це меню?', - 'saved' => 'Меню було успішно збережено.', - 'name' => 'Ім`я', - 'code' => 'Код', - 'items' => 'Пункти меню', - 'add_subitem' => 'Додати підменю', - 'code_required' => 'Поле Код обов\'язкове', - 'invalid_code' => 'Некоректний формат Коду. Код може містити цифри, латинські літери і такі символи: _-/', - ], - 'menuitem' => [ - 'title' => 'Назва', - 'editor_title' => 'Редагувати пункт меню', - 'type' => 'Тип', - 'allow_nested_items' => 'Дозволити вкладені', - 'allow_nested_items_comment' => 'Вкладені пункти можуть бути динамічно згенеровані статичною сторінкою або іншими типами елементів', - 'url' => 'URL', - 'reference' => 'Посилання', - 'title_required' => 'Назва обов\'язково', - 'unknown_type' => 'Невідомий тип меню', - 'unnamed' => 'Безіменний пункт', - 'add_item' => 'Додати пункт ( i )', - 'new_item' => 'Новий пункт', - 'replace' => 'Замінювати цей пункт його згенеруванними нащадками', - 'replace_comment' => 'Відмітьте для перенесення згенерованних пунктів меню на один рівень з цим пунктом. Сам цей пункт буде приховано.', - 'cms_page' => 'Сторінки CMS', - 'cms_page_comment' => 'Оберіть відкриваючу при натисканні сторінку.', - 'reference_required' => 'Необхідне посилання для пункту меню.', - 'url_required' => 'Необхідний URL', - 'cms_page_required' => 'Будь ласка, виберіть сторінку CMS', - 'code' => 'Код', - 'code_comment' => 'Введіть код пункту меню, якщо хочете мати до нього доступ через API.', - ], - 'content' => [ - 'menu_label' => 'Зміст', - 'cant_save_to_dir' => 'Збереження файлів змісту в директорію static-pages заборонено.', - ], - 'sidebar' => [ - 'Add' => 'Додати', - ], - 'object' => [ - 'invalid_type' => 'Невідомий тип об\'єкту', - 'not_found' => 'Запитуваний об\'єкт не знайдено.', - ], - 'editor' => [ - 'title' => 'Назва', - 'new_title' => 'Назва нової сторінки', - 'content' => 'Зміст', - 'url' => 'URL', - 'filename' => 'Ім\'я файлу', - 'layout' => 'Шаблон', - 'description' => 'Опис', - 'preview' => 'Попередній огляд', - 'enter_fullscreen' => 'Увійти в повноекранний режим', - 'exit_fullscreen' => 'Вийти з повноекранного режиму', - 'hidden' => 'Прихований', - 'hidden_comment' => 'Приховані сторінки доступні тільки увійшовшим адміністраторам.', - 'navigation_hidden' => 'Сховати в навігації', - 'navigation_hidden_comment' => 'Відмітьте, щоб приховати цю сторінку в генеруючих меню і хлібних крихтах.', - ], - 'snippet' => [ - 'menu_label' => 'Сніппети', - ], - 'component' => [ - 'static_page_description' => 'Виводити сторінку в CMS шаблоні.', - 'static_menu_description' => 'Виводити меню в CMS шаблоні.', - 'static_menu_menu_code' => 'Вкажіть код меню, яке повинно бути показано', - 'static_breadcrumbs_description' => 'Виводити хлібні крихти для сторінки.', - ], -]; diff --git a/lang/zh-cn.json b/lang/zh-cn.json new file mode 100644 index 00000000..c7a7370c --- /dev/null +++ b/lang/zh-cn.json @@ -0,0 +1,83 @@ +{ + "Pages": "页面", + "Pages & menus features.": "页面和菜单功能拓展。", + "Manage static pages": "管理静态页面", + "Manage static menus": "管理静态菜单", + "Manage static content": "管理静态内容", + "Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.": "URL 格式无效,应以 '/' 开头,可以包含数字,字母和以下符号:_-/", + "This URL is already used by another page.": "此 URL 已被其他页面使用。", + "Layouts not found": "找不到布局文件", + "The Code is required": "编码是必需的", + "Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-": "编码格式无效,该编码可以包含数字,字母和以下符号:_-", + "Static page": "静态页", + "All static pages": "所有静态页", + "Static menu": "静态菜单", + "Static breadcrumbs": "静态面包屑导航", + "Child pages": "子页面", + "Outputs a static page in a CMS layout.": "在 CMS 布局中输出静态页。", + "Use page content field": "使用页面内容字段", + "If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.": "如果未选中,编辑静态页面时内容部分将不会出现。页面内容将仅通过占位符和变量来确定。", + "Default layout": "默认布局", + "Defines this layout as the default for new pages": "将此布局定义为新页面的默认设置", + "Subpage layout": "子页面布局", + "The layout to use as the default for any new subpages": "该布局用作任何新子页面的默认布局", + "Outputs a menu in a CMS layout.": "输出 CMS 布局中的菜单。", + "Menu": "菜单", + "Specify a code of the menu the component should output.": "指定组件应输出的菜单代码。", + "Outputs breadcrumbs for a static page.": "输出静态页面的面包屑导航。", + "Displays a list of child pages for the current page": "显示当前页面的子页面列表", + "Static Pages": "静态页面", + "Menus": "菜单", + "Content": "内容", + "Add": "插入", + "Refresh": "刷新", + "Page": "页面", + "Content block": "内容块", + "New page": "新建页面", + "New page title": "新页面标题", + "New menu": "新建菜单", + "New content block": "新建内容块", + "Title": "标题", + "URL": "URL", + "File Name": "文件名", + "Layout": "布局", + "Hidden": "隐藏", + "Hide in navigation": "在导航中隐藏", + "Description": "描述", + "Name": "名称", + "Code": "编码", + "The Title is required.": "标题是必需的。", + "The URL is required.": "URL 是必需的。", + "The File Name is required.": "文件名是必需的。", + "The Name is required.": "名称是必需的。", + "The Code is required.": "编码是必需的。", + "Error loading page": "加载页面时出错", + "Error loading menu": "加载菜单时出错", + "Error loading content block": "加载内容块时出错", + "Add item": "添加菜单项", + "Add subitem": "插入子项", + "New menu item": "新菜单项", + "Untitled": "未命名", + "Up": "向上", + "Down": "向下", + "Indent": "增加缩进", + "Outdent": "减少缩进", + "Delete": "删除", + "No menu items yet. Use \"Add item\" in the toolbar.": "尚无菜单项。请使用工具栏中的“添加菜单项”。", + "Select a menu item to edit, or add a new one.": "选择要编辑的菜单项,或添加一个新的。", + "Custom Fields": "自定义字段", + "Search all references...": "搜索所有参考...", + "Edit Menu Item": "编辑菜单项", + "Move up": "上移", + "Move down": "下移", + "Apply": "应用", + "Cancel": "取消", + "New Page": "新建页面", + "New Menu": "新建菜单", + "New Content Block": "新建内容块", + "Fields": "字段", + "Preview": "预览", + "Add subpage": "插入子页面", + "You don't have permissions to manage :type documents.": "您没有权限管理 :type 文档。", + "Content files cannot be saved in the static pages directory.": "内容文件不能保存在静态页面目录中。" +} diff --git a/lang/zh-cn/lang.php b/lang/zh-cn/lang.php deleted file mode 100644 index 753debe2..00000000 --- a/lang/zh-cn/lang.php +++ /dev/null @@ -1,113 +0,0 @@ - [ - 'name' => '页面', - 'description' => '页面和菜单功能拓展', - ], - 'page' => [ - 'menu_label' => '页面', - 'template_title' => '%s 页面', - 'delete_confirmation' => '你真的要删除所选页面吗? 如果有子页面,也将被删除。', - 'no_records' => '找不到页面', - 'delete_confirm_single' => '你真的要删除这个页面吗? 如果有子页面,也将被删除。', - 'new' => '新建', - 'add_subpage' => '插入子页面', - 'invalid_url' => 'URL 格式无效,应以 \'/\' 开头,可以包含数字,字母和以下符号:_-/', - 'url_not_unique' => 'URL 已存在', - 'layout' => '布局', - 'layouts_not_found' => '找不到布局文件', - 'saved' => '保存成功!', - 'tab' => '页面', - 'manage_pages' => '管理静态页面', - 'manage_menus' => '管理静态菜单', - 'access_snippets' => '代码片段', - 'manage_content' => '管理静态内容', - ], - 'menu' => [ - 'menu_label' => '菜单', - 'delete_confirmation' => '你真的要删除所选菜单吗?', - 'no_records' => '找不到菜单', - 'new' => '新建', - 'new_name' => '未命名的菜单', - 'new_code' => 'new-menu', - 'delete_confirm_single' => '你真的要删除这个菜单吗?', - 'saved' => '保存成功!', - 'name' => '名称', - 'code' => '编码', - 'items' => '菜单项', - 'add_subitem' => '插入子项', - 'code_required' => '编码是必要的!', - 'invalid_code' => '编码格式无效,该编码可以包含数字,字母和以下符号:_-', - ], - 'menuitem' => [ - 'title' => '标题', - 'editor_title' => '编辑菜单项', - 'type' => '类型', - 'allow_nested_items' => '允许嵌套', - 'allow_nested_items_comment' => '嵌套项目可以通过静态页面和其他一些项目类型动态生成', - 'url' => 'URL', - 'reference' => '参考', - 'search_placeholder' => '搜索所有参考...', - 'title_required' => '标题是必需的', - 'unknown_type' => '未知的菜单项类型', - 'unnamed' => '未命名的菜单项', - 'add_item' => '掺入菜单项I', - 'new_item' => '新菜单项', - 'replace' => '将此项目替换为生成的子项', - 'replace_comment' => '使用此复选框产生的菜单项推到与本项目同一层级。 该项目本身将被隐藏。', - 'cms_page' => 'CMS 页面', - 'cms_page_comment' => '当单击菜单项时,选择要打开的页面。', - 'reference_required' => '菜单项参考 是必需的。', - 'url_required' => 'URL 是必需的。', - 'cms_page_required' => '请选择 CMS 页面', - 'code' => '编码', - 'code_comment' => '如果要使用 API 访问菜单项代码,请输入。', - 'static_page' => '静态页面', - 'all_static_pages' => '所有静态页', - ], - 'content' => [ - 'menu_label' => '内容', - 'cant_save_to_dir' => '将内容文件保存到 \'static-pages\' 目录是不允许的。', - ], - 'sidebar' => [ - 'add' => '插入', - ], - 'object' => [ - 'invalid_type' => '未知的对象类型', - 'not_found' => '找不到请求的对象。', - ], - 'editor' => [ - 'title' => '标题', - 'new_title' => '未命名', - 'content' => '内容', - 'url' => 'URL', - 'filename' => '文件名', - 'layout' => '布局', - 'description' => '描述', - 'preview' => '预览', - 'enter_fullscreen' => '全屏编辑', - 'exit_fullscreen' => '退出全屏', - 'hidden' => '隐藏', - 'hidden_comment' => '隐藏的页面只能由登录的后台用户访问。', - 'navigation_hidden' => '在导航中隐藏', - 'navigation_hidden_comment' => '选中此框可将该页面在自动生成的菜单和面包屑导航中隐藏起来。', - ], - 'snippet' => [ - 'menu_label' => '代码片段', - ], - 'component' => [ - 'static_page_name' => '静态页', - 'static_page_description' => '在 CMS 布局中输出静态页。', - 'static_page_use_content_name' => '使用页面内容字段', - 'static_page_use_content_description' => '如果未选中,编辑静态页面时内容部分将不会出现。 页面内容将仅通过占位符和变量来确定。', - 'static_page_default_name' => '默认布局', - 'static_page_default_description' => '将此布局定义为新页面的默认设置', - 'static_page_child_layout_name' => '子页面布局', - 'static_page_child_layout_description' => '该布局用作任何新子页面的默认布局', - 'static_menu_name' => '静态菜单', - 'static_menu_description' => '输出 CMS 布局中的菜单。', - 'static_menu_code_name' => '菜单', - 'static_menu_code_description' => '指定组件应输出的菜单代码。', - 'static_breadcrumbs_name' => '静态面包屑导航', - 'static_breadcrumbs_description' => '输出静态页面的面包屑导航。', - ], -]; diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..1607cf46 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,32 @@ + + + + + ./tests + + + + + + + + + + + + + + + + + diff --git a/rainlab-pages.mix.js b/rainlab-pages.mix.js deleted file mode 100644 index 6d1227d4..00000000 --- a/rainlab-pages.mix.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - |-------------------------------------------------------------------------- - | Mix Asset Management - |-------------------------------------------------------------------------- - | - | Mix provides a clean, fluent API for defining some Webpack build steps - | for your theme assets. By default, we are compiling the CSS - | file for the application as well as bundling up all the JS files. - | - */ - -module.exports = (mix) => { - mix.less('plugins/rainlab/pages/assets/less/pages.less', 'plugins/rainlab/pages/assets/css/'); - mix.less('plugins/rainlab/pages/assets/less/treeview.less', 'plugins/rainlab/pages/assets/css/'); -} diff --git a/tests/ControllerTest.php b/tests/ControllerTest.php new file mode 100644 index 00000000..0351a48c --- /dev/null +++ b/tests/ControllerTest.php @@ -0,0 +1,41 @@ +initCmsPage('/about'); + + $this->assertNotNull($cmsPage); + $this->assertInstanceOf(\Cms\Classes\Page::class, $cmsPage); + $this->assertArrayHasKey('staticPage', $cmsPage->apiBag); + $this->assertEquals('About', $cmsPage->settings['title']); + $this->assertEquals('default', $cmsPage->settings['layout']); + } + + public function testInitCmsPageReturnsNullForUnknownUrl() + { + $this->assertNull(Controller::instance()->initCmsPage('/missing')); + } + + public function testGetPageContents() + { + $cmsPage = Controller::instance()->initCmsPage('/about'); + $contents = Controller::instance()->getPageContents($cmsPage); + + $this->assertStringContainsString('

    About content

    ', $contents); + } + + public function testParseSyntaxFieldsFallsBackOnInvalidContent() + { + $content = '{invalid syntax'; + $this->assertEquals($content, Controller::instance()->parseSyntaxFields($content)); + } +} diff --git a/tests/MenuItemTest.php b/tests/MenuItemTest.php new file mode 100644 index 00000000..667212b5 --- /dev/null +++ b/tests/MenuItemTest.php @@ -0,0 +1,69 @@ + 'Parent', + 'type' => 'static-page', + 'reference' => 'about', + 'viewBag' => ['cssClass' => 'top'], + 'items' => [ + ['title' => 'Child', 'type' => 'url', 'url' => '/child'], + ], + ], + ]); + + $this->assertCount(1, $items); + $this->assertEquals('Parent', $items[0]->title); + $this->assertEquals(['cssClass' => 'top'], $items[0]->viewBag); + $this->assertCount(1, $items[0]->items); + $this->assertEquals('Child', $items[0]->items[0]->title); + $this->assertEquals('/child', $items[0]->items[0]->url); + } + + public function testToArrayIncludesFillableProperties() + { + $item = new MenuItem; + $item->title = 'Example'; + $item->type = 'url'; + $item->url = '/example'; + + $result = $item->toArray(); + + $this->assertEquals('Example', $result['title']); + $this->assertEquals('url', $result['type']); + $this->assertEquals('/example', $result['url']); + $this->assertArrayHasKey('viewBag', $result); + $this->assertArrayNotHasKey('items', $result); + } + + public function testGetTypeOptionsIncludesRegisteredTypes() + { + $options = (new MenuItem)->getTypeOptions(); + + $this->assertArrayHasKey('url', $options); + $this->assertArrayHasKey('header', $options); + $this->assertArrayHasKey('static-page', $options); + $this->assertArrayHasKey('all-static-pages', $options); + } + + public function testGetTypeInfoForStaticPage() + { + $info = MenuItem::getTypeInfo('static-page'); + + $this->assertTrue($info['nesting']); + $this->assertTrue($info['dynamicItems']); + $this->assertArrayHasKey('index', $info['references']); + $this->assertArrayHasKey('about', $info['references']); + } +} diff --git a/tests/MenuTest.php b/tests/MenuTest.php new file mode 100644 index 00000000..d0dde885 --- /dev/null +++ b/tests/MenuTest.php @@ -0,0 +1,88 @@ +theme, 'main-menu.yaml'); + + $this->assertNotNull($menu); + $this->assertEquals('Main Menu', $menu->name); + $this->assertEquals('main-menu', $menu->code); + $this->assertCount(3, $menu->items); + $this->assertEquals('Home', $menu->items[0]->title); + $this->assertEquals('static-page', $menu->items[0]->type); + } + + public function testGenerateReferences() + { + $menu = Menu::load($this->theme, 'main-menu.yaml'); + $references = $menu->generateReferences($this->makeCmsPage()); + + $this->assertCount(3, $references); + + [$home, $about, $external] = $references; + + $this->assertEquals('Home', $home->title); + $this->assertEquals(url('/'), $home->url); + + $this->assertEquals('About', $about->title); + $this->assertEquals(url('/about'), $about->url); + $this->assertCount(1, $about->items, 'Nested item should include the subpage'); + $this->assertEquals('Team', $about->items[0]->title); + + $this->assertEquals('External', $external->title); + $this->assertEquals('https://example.com', $external->url); + } + + public function testGenerateReferencesWithReplaceExpandsGeneratedItems() + { + $menu = Menu::load($this->theme, 'all-pages.yaml'); + $references = $menu->generateReferences($this->makeCmsPage()); + + $titles = array_map(function($reference) { + return $reference->title; + }, $references); + + $this->assertContains('Home', $titles); + $this->assertContains('About', $titles); + $this->assertNotContains('All pages', $titles, 'Replaced item should not appear itself'); + $this->assertNotContains('Hidden', $titles, 'Pages hidden from navigation should be excluded'); + } + + public function testSetCodeRenamesFile() + { + $menu = new Menu; + $menu->code = 'footer'; + + $this->assertEquals('footer.yaml', $menu->fileName); + } + + public function testValidationRejectsInvalidCode() + { + $menu = Menu::inTheme($this->theme); + $menu->fill([ + 'name' => 'Bad Menu', + 'code' => 'bad code!', + ]); + + $this->expectException(\October\Rain\Halcyon\Exception\ModelException::class); + $menu->save(); + } + + /** + * makeCmsPage returns a host page for reference generation + */ + protected function makeCmsPage() + { + return CmsPage::inTheme($this->theme); + } +} diff --git a/tests/PageListTest.php b/tests/PageListTest.php new file mode 100644 index 00000000..821bfd26 --- /dev/null +++ b/tests/PageListTest.php @@ -0,0 +1,75 @@ +theme); + $pages = $pageList->listPages(); + + $fileNames = $pages->map(function($page) { + return $page->getBaseFileName(); + })->all(); + + sort($fileNames); + $this->assertEquals(['about', 'about-team', 'hidden-page', 'index', 'sidebar-page'], $fileNames); + } + + public function testGetPageTree() + { + $pageList = new PageList($this->theme); + $tree = $pageList->getPageTree(); + + $this->assertCount(4, $tree); + $this->assertEquals('index', $tree[0]->page->getBaseFileName()); + $this->assertEquals('about', $tree[1]->page->getBaseFileName()); + $this->assertCount(1, $tree[1]->subpages); + $this->assertEquals('about-team', $tree[1]->subpages[0]->page->getBaseFileName()); + } + + public function testGetPageParent() + { + $pageList = new PageList($this->theme); + + $child = Page::load($this->theme, 'about-team'); + $this->assertEquals('about', $pageList->getPageParent($child)); + + $root = Page::load($this->theme, 'about'); + $this->assertNull($pageList->getPageParent($root)); + } + + public function testGetPageSubTree() + { + $pageList = new PageList($this->theme); + + $page = Page::load($this->theme, 'about'); + $subTree = $pageList->getPageSubTree($page); + + $this->assertArrayHasKey('about-team', $subTree); + } + + public function testUpdateStructure() + { + $pageList = new PageList($this->theme); + + $pageList->updateStructure([ + 'index' => [], + 'about' => [ + 'about-team' => [], + 'sidebar-page' => [], + ], + 'hidden-page' => [], + ]); + + $page = Page::load($this->theme, 'sidebar-page'); + $this->assertEquals('about', $pageList->getPageParent($page)); + } +} diff --git a/tests/PageLocaleTest.php b/tests/PageLocaleTest.php new file mode 100644 index 00000000..50f18908 --- /dev/null +++ b/tests/PageLocaleTest.php @@ -0,0 +1,37 @@ +theme, 'about'); + $mirror = PageLocale::findLocale('fr', $page); + + $this->assertNotNull($mirror); + $this->assertEquals('À propos', $mirror->getViewBag()->property('title')); + $this->assertEquals('

    Contenu à propos

    ', trim($mirror->markup)); + } + + public function testFindLocaleReturnsNullWhenMissing() + { + $page = Page::load($this->theme, 'index'); + + $this->assertNull(PageLocale::findLocale('fr', $page)); + $this->assertNull(PageLocale::findLocale('de', $page)); + } + + public function testTranslatableUrlFallsBackToBaseUrl() + { + $page = Page::load($this->theme, 'about'); + + $this->assertEquals('/a-propos', array_get($page->attributes, 'viewBag.localeUrl.fr')); + } +} diff --git a/tests/PagesPluginTestCase.php b/tests/PagesPluginTestCase.php new file mode 100644 index 00000000..4620dead --- /dev/null +++ b/tests/PagesPluginTestCase.php @@ -0,0 +1,126 @@ +theme = Theme::load('test'); + $this->resetPluginCaches(); + $this->snapshotThemeFiles(); + } + + /** + * tearDown the test case + */ + public function tearDown(): void + { + $this->restoreThemeFiles(); + $this->resetPluginCaches(); + + parent::tearDown(); + } + + /** + * resetPluginCaches clears static caches held between requests + */ + protected function resetPluginCaches() + { + (new Router($this->theme))->clearCache(); + Page::clearMenuCache($this->theme); + \RainLab\Pages\Classes\Controller::forgetInstance(); + + $this->resetStaticProperty(Page::class, 'menuTreeCache', null); + $this->resetStaticProperty(PageList::class, 'configCache', false); + } + + /** + * resetStaticProperty forces a static property back to its initial value + */ + protected function resetStaticProperty(string $class, string $property, $value) + { + $reflection = new ReflectionProperty($class, $property); + $reflection->setAccessible(true); + $reflection->setValue(null, $value); + } + + /** + * snapshotThemeFiles records the fixture theme contents + */ + protected function snapshotThemeFiles() + { + $this->themeSnapshot = []; + + foreach (File::allFiles($this->theme->getPath()) as $file) { + $this->themeSnapshot[$file->getPathname()] = file_get_contents($file->getPathname()); + } + } + + /** + * restoreThemeFiles reverts the fixture theme to its recorded state + */ + protected function restoreThemeFiles() + { + if (!$this->themeSnapshot) { + return; + } + + foreach (File::allFiles($this->theme->getPath()) as $file) { + if (!array_key_exists($file->getPathname(), $this->themeSnapshot)) { + $this->deleteWithRetry($file->getPathname()); + } + } + + foreach ($this->themeSnapshot as $path => $contents) { + if (!File::exists($path) || file_get_contents($path) !== $contents) { + File::put($path, $contents); + } + } + } + + /** + * deleteWithRetry deletes a file, retrying while external processes hold a lock + */ + protected function deleteWithRetry(string $path) + { + for ($attempt = 0; $attempt < 5; $attempt++) { + File::delete($path); + clearstatcache(true, $path); + + if (!File::exists($path)) { + return; + } + + usleep(100000); + } + } +} diff --git a/tests/RouterTest.php b/tests/RouterTest.php new file mode 100644 index 00000000..a0149a7e --- /dev/null +++ b/tests/RouterTest.php @@ -0,0 +1,42 @@ +theme); + + $page = $router->findByUrl('/'); + $this->assertNotNull($page); + $this->assertEquals('index', $page->getBaseFileName()); + + $page = $router->findByUrl('/about'); + $this->assertEquals('about', $page->getBaseFileName()); + + $page = $router->findByUrl('/about/team'); + $this->assertEquals('about-team', $page->getBaseFileName()); + } + + public function testFindByUrlIsCaseInsensitive() + { + $router = new Router($this->theme); + + $page = $router->findByUrl('/About'); + $this->assertNotNull($page); + $this->assertEquals('about', $page->getBaseFileName()); + } + + public function testFindByUrlReturnsNullForUnknownUrl() + { + $router = new Router($this->theme); + + $this->assertNull($router->findByUrl('/missing')); + } +} diff --git a/tests/StaticPageTest.php b/tests/StaticPageTest.php new file mode 100644 index 00000000..a41f7a43 --- /dev/null +++ b/tests/StaticPageTest.php @@ -0,0 +1,218 @@ +theme, 'about'); + + $this->assertNotNull($page); + $this->assertEquals('About', $page->getViewBag()->property('title')); + $this->assertEquals('/about', $page->getViewBag()->property('url')); + $this->assertEquals('

    About content

    ', trim($page->markup)); + } + + public function testUrlHelper() + { + $this->assertEquals(url('/about'), Page::url('about')); + $this->assertNull(Page::url('missing-page')); + $this->assertNull(Page::url('')); + } + + public function testGetParentAndChildren() + { + $page = Page::load($this->theme, 'about'); + $children = $page->getChildren(); + + $this->assertCount(1, $children); + $this->assertEquals('about-team', $children[0]->getBaseFileName()); + + $child = Page::load($this->theme, 'about-team'); + $parent = $child->getParent(); + + $this->assertNotNull($parent); + $this->assertEquals('about', $parent->getBaseFileName()); + } + + public function testGetLayoutOptions() + { + $page = Page::inTheme($this->theme); + $options = $page->getLayoutOptions(); + + $this->assertArrayHasKey('default', $options); + $this->assertArrayHasKey('sidebar', $options); + $this->assertArrayNotHasKey('plain', $options); + $this->assertEquals('Default layout', $options['default']); + } + + public function testListLayoutPlaceholders() + { + $page = Page::load($this->theme, 'sidebar-page'); + $placeholders = $page->listLayoutPlaceholders(); + + $this->assertArrayHasKey('sidebar', $placeholders); + $this->assertEquals('Sidebar', $placeholders['sidebar']['title']); + $this->assertEquals('html', $placeholders['sidebar']['type']); + + $this->assertArrayHasKey('notes', $placeholders); + $this->assertEquals('text', $placeholders['notes']['type']); + } + + public function testGetPlaceholdersAttribute() + { + $page = Page::load($this->theme, 'sidebar-page'); + $placeholders = $page->placeholders; + + $this->assertArrayHasKey('sidebar', $placeholders); + $this->assertEquals('

    Sidebar placeholder content

    ', trim($placeholders['sidebar'])); + } + + public function testSetPlaceholdersRendersPutBlocks() + { + $page = Page::load($this->theme, 'sidebar-page'); + + $page->placeholders = [ + 'sidebar' => '

    Updated

    ', + 'unknown' => '

    Not defined by the layout

    ', + ]; + + $this->assertStringContainsString('{% put sidebar %}', $page->code); + $this->assertStringContainsString('

    Updated

    ', $page->code); + $this->assertStringNotContainsString('unknown', $page->code); + } + + public function testCreatePageAppendsToMeta() + { + $page = Page::inTheme($this->theme); + $page->fill([ + 'settings' => [ + 'viewBag' => [ + 'title' => 'New Page', + 'url' => '/new-page', + 'layout' => 'default', + ], + ], + 'markup' => '

    New page content

    ', + ]); + $page->save(); + + $this->assertEquals('new-page.htm', $page->fileName); + $this->assertFileExists($this->theme->getPath().'/content/static-pages/new-page.htm'); + + $pageList = new PageList($this->theme); + $tree = $pageList->getPageTree(true); + $names = array_map(function($node) { + return $node->page->getBaseFileName(); + }, $tree); + + $this->assertContains('new-page', $names); + } + + public function testDeletePageRemovesChildrenAndMeta() + { + $page = Page::load($this->theme, 'about'); + $deleted = $page->delete(); + + sort($deleted); + $this->assertEquals(['about', 'about-team'], $deleted); + $this->assertFileDoesNotExist($this->theme->getPath().'/content/static-pages/about.htm'); + $this->assertFileDoesNotExist($this->theme->getPath().'/content/static-pages/about-team.htm'); + $this->assertFileDoesNotExist($this->theme->getPath().'/content/static-pages-fr/about.htm'); + } + + public function testValidationRequiresTitleAndUrl() + { + $page = Page::inTheme($this->theme); + $page->fill([ + 'settings' => [ + 'viewBag' => [ + 'title' => '', + 'url' => '/valid-url', + ], + ], + ]); + + $this->expectException(\October\Rain\Halcyon\Exception\ModelException::class); + $page->save(); + } + + public function testValidationRejectsMalformedUrl() + { + $page = Page::inTheme($this->theme); + $page->fill([ + 'settings' => [ + 'viewBag' => [ + 'title' => 'Bad URL', + 'url' => 'no-leading-slash', + ], + ], + ]); + + $this->expectException(\October\Rain\Halcyon\Exception\ModelException::class); + $page->save(); + } + + public function testValidationRejectsDuplicateUrl() + { + $page = Page::inTheme($this->theme); + $page->fill([ + 'settings' => [ + 'viewBag' => [ + 'title' => 'Duplicate', + 'url' => '/about', + ], + ], + ]); + + $this->expectException(\October\Rain\Halcyon\Exception\ModelException::class); + $page->save(); + } + + public function testResolveMenuItemForStaticPage() + { + $item = new \RainLab\Pages\Classes\MenuItem; + $item->type = 'static-page'; + $item->reference = 'about'; + $item->nesting = true; + + $result = Page::resolveMenuItem($item, url('/'), $this->theme); + + $this->assertEquals(url('/about'), $result['url']); + $this->assertFalse($result['isActive']); + $this->assertCount(1, $result['items']); + $this->assertEquals('Team', $result['items'][0]['title']); + } + + public function testResolveMenuItemForAllPagesSkipsHiddenNavigation() + { + $item = new \RainLab\Pages\Classes\MenuItem; + $item->type = 'all-static-pages'; + + $result = Page::resolveMenuItem($item, url('/'), $this->theme); + $titles = array_column($result['items'], 'title'); + + $this->assertContains('Home', $titles); + $this->assertContains('About', $titles); + $this->assertNotContains('Hidden', $titles); + } + + public function testGetMenuTypeInfo() + { + $info = Page::getMenuTypeInfo('static-page'); + + $this->assertTrue($info['nesting']); + $this->assertTrue($info['dynamicItems']); + $this->assertArrayHasKey('about', $info['references']); + + $info = Page::getMenuTypeInfo('all-static-pages'); + $this->assertTrue($info['dynamicItems']); + } +} diff --git a/tests/fixtures/themes/test/content/static-pages-fr/about.htm b/tests/fixtures/themes/test/content/static-pages-fr/about.htm new file mode 100644 index 00000000..5c8a3a38 --- /dev/null +++ b/tests/fixtures/themes/test/content/static-pages-fr/about.htm @@ -0,0 +1,5 @@ +## +[viewBag] +title = "À propos" +== +

    Contenu à propos

    diff --git a/tests/fixtures/themes/test/content/static-pages/about-team.htm b/tests/fixtures/themes/test/content/static-pages/about-team.htm new file mode 100644 index 00000000..465022e6 --- /dev/null +++ b/tests/fixtures/themes/test/content/static-pages/about-team.htm @@ -0,0 +1,9 @@ +## +[viewBag] +title = "Team" +url = "/about/team" +layout = "default" +is_hidden = 0 +navigation_hidden = 0 +== +

    Team content

    diff --git a/tests/fixtures/themes/test/content/static-pages/about.htm b/tests/fixtures/themes/test/content/static-pages/about.htm new file mode 100644 index 00000000..834912f9 --- /dev/null +++ b/tests/fixtures/themes/test/content/static-pages/about.htm @@ -0,0 +1,10 @@ +## +[viewBag] +title = "About" +url = "/about" +layout = "default" +is_hidden = 0 +navigation_hidden = 0 +localeUrl[fr] = "/a-propos" +== +

    About content

    diff --git a/tests/fixtures/themes/test/content/static-pages/hidden-page.htm b/tests/fixtures/themes/test/content/static-pages/hidden-page.htm new file mode 100644 index 00000000..db89aa25 --- /dev/null +++ b/tests/fixtures/themes/test/content/static-pages/hidden-page.htm @@ -0,0 +1,9 @@ +## +[viewBag] +title = "Hidden" +url = "/hidden" +layout = "default" +is_hidden = 0 +navigation_hidden = 1 +== +

    Hidden from navigation

    diff --git a/tests/fixtures/themes/test/content/static-pages/index.htm b/tests/fixtures/themes/test/content/static-pages/index.htm new file mode 100644 index 00000000..49f6c14b --- /dev/null +++ b/tests/fixtures/themes/test/content/static-pages/index.htm @@ -0,0 +1,9 @@ +## +[viewBag] +title = "Home" +url = "/" +layout = "default" +is_hidden = 0 +navigation_hidden = 0 +== +

    Home content

    diff --git a/tests/fixtures/themes/test/content/static-pages/sidebar-page.htm b/tests/fixtures/themes/test/content/static-pages/sidebar-page.htm new file mode 100644 index 00000000..10447681 --- /dev/null +++ b/tests/fixtures/themes/test/content/static-pages/sidebar-page.htm @@ -0,0 +1,13 @@ +## +[viewBag] +title = "Sidebar Page" +url = "/sidebar-page" +layout = "sidebar" +is_hidden = 0 +navigation_hidden = 0 +== +{% put sidebar %} +

    Sidebar placeholder content

    +{% endput %} +== +

    Sidebar page content

    diff --git a/tests/fixtures/themes/test/content/welcome.htm b/tests/fixtures/themes/test/content/welcome.htm new file mode 100644 index 00000000..2de60c79 --- /dev/null +++ b/tests/fixtures/themes/test/content/welcome.htm @@ -0,0 +1 @@ +

    Welcome content block

    diff --git a/tests/fixtures/themes/test/layouts/default.htm b/tests/fixtures/themes/test/layouts/default.htm new file mode 100644 index 00000000..90ff1172 --- /dev/null +++ b/tests/fixtures/themes/test/layouts/default.htm @@ -0,0 +1,10 @@ +description = "Default layout" + +[staticPage] +default = 1 +== + + + {% page %} + + diff --git a/tests/fixtures/themes/test/layouts/plain.htm b/tests/fixtures/themes/test/layouts/plain.htm new file mode 100644 index 00000000..11c93d2b --- /dev/null +++ b/tests/fixtures/themes/test/layouts/plain.htm @@ -0,0 +1,5 @@ +description = "Plain layout without the staticPage component" +== + + {% page %} + diff --git a/tests/fixtures/themes/test/layouts/sidebar.htm b/tests/fixtures/themes/test/layouts/sidebar.htm new file mode 100644 index 00000000..51a5cc2e --- /dev/null +++ b/tests/fixtures/themes/test/layouts/sidebar.htm @@ -0,0 +1,11 @@ +description = "Sidebar layout" + +[staticPage] +== + + +
    {% page %}
    + +
    {% placeholder notes type="text" title="Notes" %}
    + + diff --git a/tests/fixtures/themes/test/meta/menus/all-pages.yaml b/tests/fixtures/themes/test/meta/menus/all-pages.yaml new file mode 100644 index 00000000..840c64a4 --- /dev/null +++ b/tests/fixtures/themes/test/meta/menus/all-pages.yaml @@ -0,0 +1,12 @@ +name: 'All Pages' +items: + - + title: 'All pages' + nesting: '0' + type: all-static-pages + url: '' + code: all + reference: '' + cmsPage: '' + replace: '1' + viewBag: { } diff --git a/tests/fixtures/themes/test/meta/menus/main-menu.yaml b/tests/fixtures/themes/test/meta/menus/main-menu.yaml new file mode 100644 index 00000000..73ce1988 --- /dev/null +++ b/tests/fixtures/themes/test/meta/menus/main-menu.yaml @@ -0,0 +1,36 @@ +name: 'Main Menu' +items: + - + title: Home + nesting: '0' + type: static-page + url: '' + code: home + reference: index + cmsPage: '' + replace: '0' + viewBag: + isHidden: '0' + locale: + fr: + title: Accueil + - + title: About + nesting: '1' + type: static-page + url: '' + code: about + reference: about + cmsPage: '' + replace: '0' + viewBag: { } + - + title: External + nesting: '0' + type: url + url: 'https://example.com' + code: external + reference: '' + cmsPage: '' + replace: '0' + viewBag: { } diff --git a/tests/fixtures/themes/test/meta/static-pages.yaml b/tests/fixtures/themes/test/meta/static-pages.yaml new file mode 100644 index 00000000..711e1fad --- /dev/null +++ b/tests/fixtures/themes/test/meta/static-pages.yaml @@ -0,0 +1,6 @@ +static-pages: + index: { } + about: + about-team: { } + hidden-page: { } + sidebar-page: { } diff --git a/tests/fixtures/themes/test/theme.yaml b/tests/fixtures/themes/test/theme.yaml new file mode 100644 index 00000000..fee698de --- /dev/null +++ b/tests/fixtures/themes/test/theme.yaml @@ -0,0 +1,2 @@ +name: Test +description: Test fixture theme for the Pages plugin diff --git a/updates/version.yaml b/updates/version.yaml index 736e5da5..bbf3c5d0 100644 --- a/updates/version.yaml +++ b/updates/version.yaml @@ -89,3 +89,4 @@ v2.2.3: Fixes placeholder tab missing from the Static Page editor on Twig 3.12+ v2.2.4: Fixes tab rendering in Firefox v2.2.5: Fixes performance of tree control UI v2.2.6: Compatibility fixes for October v4.3 +v3.0.0: Modernized backend editor rebuilt on the Vue Editor module diff --git a/vuecomponents/ContentEditor.php b/vuecomponents/ContentEditor.php new file mode 100644 index 00000000..cbc7c74f --- /dev/null +++ b/vuecomponents/ContentEditor.php @@ -0,0 +1,21 @@ + this.$refs.editor.layout()); + } + if (this.$refs.markdownEditor) { + this.$nextTick(() => this.$refs.markdownEditor.refresh()); + } + }, + + documentCreatedOrLoaded: function() { + this.buildMarkupModel(); + } + }, + watch: { + isRicheditorDocument: function(value) { + if (!value) { + this.toolbarExtensionPoint = []; + } + }, + isMarkdownDocument: function(value) { + if (!value) { + this.toolbarExtensionPoint = []; + } + } + } +}; diff --git a/vuecomponents/contenteditor/partials/_contenteditor.php b/vuecomponents/contenteditor/partials/_contenteditor.php new file mode 100644 index 00000000..99c5fe57 --- /dev/null +++ b/vuecomponents/contenteditor/partials/_contenteditor.php @@ -0,0 +1,70 @@ + + + + + + + diff --git a/vuecomponents/menueditor/assets/js/menueditor.js b/vuecomponents/menueditor/assets/js/menueditor.js new file mode 100644 index 00000000..80f236e3 --- /dev/null +++ b/vuecomponents/menueditor/assets/js/menueditor.js @@ -0,0 +1,715 @@ +import { DocumentComponentBase } from '../../../../../../../modules/editor/assets/js/editor.extension.documentcomponent.base.js'; + +// Each open menu document renders its own item form island; a per-instance +// counter keeps the container id and form widget alias (which prefixes every +// field id) unique, so checkbox labels always target their own tab's inputs. +let menuItemFormUid = 0; + +export default { + extends: DocumentComponentBase, + data: function() { + const uid = ++menuItemFormUid; + + return { + documentSettingsPopupTitle: this.trans('Menu') || 'Menu', + documentTitleProperty: 'name', + menuItemFormContainerId: 'pagesMenuItemForm' + uid, + menuItemFormAlias: 'menuItemForm' + uid, + items: [], + selectedItem: null, + itemFormLoaded: false, + modalVisible: false, + modalLoading: false, + newItemTitle: this.trans('New menu item'), + nextItemId: 1, + dragItemId: null, + dropTargetId: null, + dropMode: null + }; + }, + computed: { + flatItems: function() { + // Flatten the nested tree into a list with depth for indented rendering. + const result = []; + const walk = (list, depth, parent) => { + list.forEach((item) => { + result.push({ item: item, depth: depth, parent: parent, siblings: list }); + if (item._children && item._children.length) { + walk(item._children, depth + 1, item); + } + }); + }; + walk(this.items, 0, null); + return result; + }, + + toolbarElements: function() { + return [ + { + type: 'button', + icon: 'icon-save-cloud', + label: this.trans('backend::lang.form.save'), + hotkey: 'ctrl+s, cmd+s', + tooltip: this.trans('backend::lang.form.save'), + command: 'save' + }, + { + type: 'button', + icon: 'icon-settings', + label: this.trans('editor::lang.common.settings'), + command: 'settings', + hidden: !this.hasSettingsForm + }, + { + type: 'button', + icon: 'icon-plus', + label: this.trans('Add item'), + command: 'add-item' + }, + { + type: 'separator' + }, + { + type: 'button', + icon: 'icon-delete', + disabled: this.isNewDocument, + command: 'delete', + hotkey: 'shift+option+d', + tooltip: this.trans('backend::lang.form.delete') + }, + { + type: 'button', + icon: this.documentHeaderCollapsed ? 'icon-angle-down' : 'icon-angle-up', + command: 'document:toggleToolbar', + fixedRight: true, + tooltip: this.trans('editor::lang.common.toggle_document_header') + } + ]; + } + }, + watch: { + // The header subtitle edits the top-level code; saving and the settings + // popup read settings.code - keep the two in sync both ways. + 'documentData.code': function(value) { + if (this.documentData && this.documentData.settings && this.documentData.settings.code !== value) { + this.documentData.settings.code = value; + } + }, + 'documentData.settings.code': function(value) { + if (this.documentData && this.documentData.code !== value) { + this.documentData.code = value; + } + } + }, + methods: { + getRootProperties: function() { + return ['code', 'items']; + }, + + getMainUiDocumentProperties: function() { + return ['name', 'code', 'items']; + }, + + getSaveDocumentData: function(inspectorDocumentData) { + const rootProperties = this.getRootProperties(); + const documentData = inspectorDocumentData ? inspectorDocumentData : this.documentData; + const data = $.oc.vueUtils.getCleanObject(documentData); + const result = { settings: {} }; + const ignoredProperties = ['items']; + + Object.keys(data).forEach((property) => { + if (property === 'settings' || ignoredProperties.indexOf(property) !== -1) { + return; + } + if (rootProperties.indexOf(property) !== -1) { + result[property] = data[property]; + } + else { + result.settings[property] = data[property]; + } + }); + + if (typeof data.settings === 'object' && data.settings !== null) { + Object.keys(data.settings).forEach((property) => { + if (rootProperties.indexOf(property) === -1 && result.settings[property] === undefined) { + result.settings[property] = data.settings[property]; + } + }); + } + + result.items = this.serializeItems(this.items); + + return result; + }, + + syncItemsToDocument: function() { + if (this.documentData) { + this.documentData.items = this.serializeItems(this.items); + } + }, + + serializeItems: function(items) { + return items.map((item) => { + const copy = Object.assign({}, item); + delete copy._children; + delete copy._selected; + delete copy._id; + delete copy.typeLabel; + if (item._children && item._children.length) { + copy.items = this.serializeItems(item._children); + } + else { + delete copy.items; + } + return copy; + }); + }, + + inflateItems: function(rawItems) { + return (rawItems || []).map((raw) => { + const item = Object.assign({}, raw); + item._id = this.nextItemId++; + item._children = this.inflateItems(raw.items || []); + item._selected = false; + delete item.items; + return item; + }); + }, + + // Subtitle shown under a menu item row (e.g. "Static page"). + itemSubtitle: function(item) { + return item.typeLabel || item.type || ''; + }, + + newBlankItem: function() { + return { + _id: this.nextItemId++, + title: this.newItemTitle, + type: 'url', + typeLabel: 'URL', + url: '/', + code: '', + reference: '', + cmsPage: '', + nesting: false, + replace: false, + viewBag: {}, + _children: [], + _selected: false + }; + }, + + addItem: function() { + const item = this.newBlankItem(); + this.items.push(item); + this.syncItemsToDocument(); + this.editItem(item); + }, + + // Creates a new child item under the given row and opens it for editing. + addSubItem: function(entry) { + const item = this.newBlankItem(); + if (!entry.item._children) { + entry.item._children = []; + } + entry.item._children.push(item); + this.syncItemsToDocument(); + this.editItem(item); + }, + + deleteItem: function(item, list) { + const arr = list || this.items; + const idx = arr.indexOf(item); + if (idx !== -1) { + arr.splice(idx, 1); + if (this.selectedItem === item) { + this.selectedItem = null; + } + this.syncItemsToDocument(); + } + }, + + // Delete the item open in the modal, wherever it lives in the tree, then close. + deleteSelectedItem: function() { + const target = this.selectedItem; + if (!target) { + return; + } + const removeFrom = (list) => { + const idx = list.indexOf(target); + if (idx !== -1) { + list.splice(idx, 1); + return true; + } + return list.some((item) => item._children && removeFrom(item._children)); + }; + removeFrom(this.items); + this.closeModal(); + this.syncItemsToDocument(); + }, + + moveItemUp: function(entry) { + const arr = entry.siblings; + const idx = arr.indexOf(entry.item); + if (idx > 0) { + arr.splice(idx, 1); + arr.splice(idx - 1, 0, entry.item); + this.syncItemsToDocument(); + } + }, + + moveItemDown: function(entry) { + const arr = entry.siblings; + const idx = arr.indexOf(entry.item); + if (idx !== -1 && idx < arr.length - 1) { + arr.splice(idx, 1); + arr.splice(idx + 1, 0, entry.item); + this.syncItemsToDocument(); + } + }, + + indentItem: function(entry) { + const arr = entry.siblings; + const idx = arr.indexOf(entry.item); + if (idx > 0) { + arr.splice(idx, 1); + arr[idx - 1]._children.push(entry.item); + this.syncItemsToDocument(); + } + }, + + outdentItem: function(entry) { + if (!entry.parent) { + return; + } + const grand = this.findParentContext(entry.parent); + const arr = entry.siblings; + const idx = arr.indexOf(entry.item); + if (idx !== -1) { + arr.splice(idx, 1); + const parentIdx = grand.list.indexOf(entry.parent); + grand.list.splice(parentIdx + 1, 0, entry.item); + this.syncItemsToDocument(); + } + }, + + findParentContext: function(target) { + let found = { list: this.items }; + const walk = (list) => { + list.forEach((item) => { + if (item === target) { + return; + } + if (item._children && item._children.indexOf(target) !== -1) { + found = { list: item._children, parent: item }; + } + if (item._children) { + walk(item._children); + } + }); + }; + walk(this.items); + return found; + }, + + // --- Drag and drop reordering + nesting --------------------------- + // Mirrors the sidebar page tree: dropping on the top/bottom edge of a row reorders + // it before/after that row; dropping on the middle nests it inside that row. + + onDragStart: function(entry, ev) { + this.dragItemId = entry.item._id; + if (ev.dataTransfer) { + ev.dataTransfer.effectAllowed = 'move'; + ev.dataTransfer.setData('text/plain', String(entry.item._id)); + } + }, + + onDragOver: function(entry, ev) { + if (this.dragItemId === null || this.dragItemId === entry.item._id) { + return; + } + + // Cannot drop a node into its own descendant. + const dragged = this.findEntryById(this.dragItemId); + if (dragged && this.isDescendant(dragged.item, entry.item)) { + return; + } + + ev.preventDefault(); + + // Which third of the row is the cursor over? top = before, middle = inside, + // bottom = after. + const rect = ev.currentTarget.getBoundingClientRect(); + const offset = ev.clientY - rect.top; + const third = rect.height / 3; + + let mode; + if (offset < third) { + mode = 'before'; + } + else if (offset > third * 2) { + mode = 'after'; + } + else { + mode = 'inside'; + } + + this.dropTargetId = entry.item._id; + this.dropMode = mode; + }, + + onDrop: function(entry) { + const draggedId = this.dragItemId; + const mode = this.dropMode; + this.dropTargetId = null; + this.dropMode = null; + this.dragItemId = null; + + if (draggedId === null || draggedId === entry.item._id) { + return; + } + + const dragged = this.findEntryById(draggedId); + if (!dragged || this.isDescendant(dragged.item, entry.item)) { + return; + } + + // Detach the dragged item from its current parent list. + const fromArr = dragged.siblings; + fromArr.splice(fromArr.indexOf(dragged.item), 1); + + if (mode === 'inside') { + // Nest as the first child of the target. + if (!entry.item._children) { + entry.item._children = []; + } + entry.item._children.unshift(dragged.item); + } + else { + // Reorder within the target's sibling list, before or after it. + const toArr = entry.siblings; + let targetIdx = toArr.indexOf(entry.item); + if (mode === 'after') { + targetIdx += 1; + } + toArr.splice(targetIdx, 0, dragged.item); + } + + this.syncItemsToDocument(); + }, + + onDragEnd: function() { + this.dragItemId = null; + this.dropTargetId = null; + this.dropMode = null; + }, + + findEntryById: function(id) { + return this.flatItems.find((e) => e.item._id === id) || null; + }, + + isDescendant: function(ancestor, node) { + let found = false; + const walk = (list) => { + list.forEach((child) => { + if (child === node) { + found = true; + } + if (child._children) { + walk(child._children); + } + }); + }; + walk(ancestor._children || []); + return found; + }, + + // --- Edit Menu Item modal ----------------------------------------- + + editItem: function(item) { + if (this.selectedItem) { + this.selectedItem._selected = false; + } + this.selectedItem = item; + item._selected = true; + this.modalVisible = true; + this.modalLoading = true; + + this.$nextTick(() => { + this.loadItemForm(item); + }); + }, + + closeModal: function() { + this.modalVisible = false; + + // The row highlight only marks the item being edited - clear it when + // the modal closes. + if (this.selectedItem) { + this.selectedItem._selected = false; + this.selectedItem = null; + } + }, + + applyAndClose: function() { + this.applyItemForm(); + this.closeModal(); + }, + + loadItemForm: function(item) { + const container = this.$refs.menuItemForm; + if (!container) { + this.modalLoading = false; + return; + } + + // The form is hidden behind a loading indicator until it is fully + // rendered, populated and the type field visibility applied - otherwise + // the previous item's content flashes and the fields jump around. + oc.request(container, 'onLoadMenuItemForm', { + data: { + bindMenuItemForm: 1, + containerId: this.menuItemFormContainerId, + formAlias: this.menuItemFormAlias + } + }).then(() => { + this.itemFormLoaded = true; + this.populateItemForm(item); + this.bindTypeChange(); + this.bindReferenceSearch(); + return this.refreshReferenceOptions(item.reference, item.cmsPage); + }).then( + () => { this.modalLoading = false; }, + () => { this.modalLoading = false; } + ); + }, + + // Wire the Type dropdown so switching type reloads the reference/cmsPage options. + bindTypeChange: function() { + const form = this.$refs.menuItemForm; + if (!form) { + return; + } + const typeInput = form.querySelector('[name="menuItem[type]"]'); + if (typeInput && !typeInput._pagesTypeBound) { + typeInput._pagesTypeBound = true; + typeInput.addEventListener('change', () => { + this.refreshReferenceOptions('', ''); + }); + } + }, + + // Wire the "Search all references" field so picking a result (value format + // "type::reference" from MenuItemSearch) populates the Type + Reference fields. + bindReferenceSearch: function() { + const form = this.$refs.menuItemForm; + if (!form) { + return; + } + const searchInput = form.querySelector('[name="referenceSearch"]'); + if (searchInput && !searchInput._pagesSearchBound) { + searchInput._pagesSearchBound = true; + searchInput.addEventListener('change', () => { + const value = searchInput.value || ''; + const pos = value.indexOf('::'); + if (pos === -1) { + return; + } + + const type = value.substring(0, pos); + const reference = value.substring(pos + 2); + + const typeInput = form.querySelector('[name="menuItem[type]"]'); + if (typeInput && typeInput.value !== type) { + typeInput.value = type; + // Update the select2 display without firing regular change + // handlers, the type cascade would clear the selection made here. + if (window.jQuery) { + window.jQuery(typeInput).trigger('change.select2'); + } + } + + this.refreshReferenceOptions(reference, ''); + }); + } + }, + + // Fetch type info for the current type, (re)populate the reference + cmsPage + // dropdowns, and restrict field visibility to what the type supports (url only + // for the url type, reference/cmsPage/nesting/replace only when advertised). + refreshReferenceOptions: function(selectedReference, selectedCmsPage) { + const form = this.$refs.menuItemForm; + if (!form) { + return Promise.resolve(); + } + const typeInput = form.querySelector('[name="menuItem[type]"]'); + const type = typeInput ? typeInput.value : ''; + if (!type) { + return Promise.resolve(); + } + + return oc.request(form, 'onGetMenuItemTypeInfo', { + data: { type: type } + }).then((data) => { + const info = (data && data.menuItemTypeInfo) || {}; + this.fillSelect( + form.querySelector('[name="menuItem[reference]"]'), + this.flattenReferences(info.references || {}), + selectedReference + ); + this.fillSelect( + form.querySelector('[name="menuItem[cmsPage]"]'), + info.cmsPages || {}, + selectedCmsPage + ); + + this.applyTypeFieldVisibility(type, info); + }); + }, + + // Show only the fields relevant to the selected type. + applyTypeFieldVisibility: function(type, info) { + const form = this.$refs.menuItemForm; + if (!form) { + return; + } + + const toggleGroup = (name, visible) => { + const group = form.querySelector('[data-field-name="' + name + '"]'); + if (group) { + group.style.display = visible ? '' : 'none'; + } + }; + + toggleGroup('url', type === 'url'); + toggleGroup('reference', !!info.references); + toggleGroup('cmsPage', !!info.cmsPages); + toggleGroup('nesting', !!info.nesting); + toggleGroup('replace', !!info.dynamicItems); + }, + + // Flatten the (possibly nested) references structure into a flat {key: label} map. + flattenReferences: function(references) { + const flat = {}; + const walk = (map, prefix) => { + Object.keys(map).forEach((key) => { + const entry = map[key]; + const title = (entry && typeof entry === 'object') ? entry.title : entry; + flat[key] = (prefix ? prefix + ' / ' : '') + (title || key); + if (entry && typeof entry === 'object' && entry.items) { + walk(entry.items, flat[key]); + } + }); + }; + walk(references || {}, ''); + return flat; + }, + + fillSelect: function(select, options, selectedValue) { + if (!select) { + return; + } + select.innerHTML = ''; + Object.keys(options).forEach((value) => { + const opt = document.createElement('option'); + opt.value = value; + opt.textContent = options[value]; + if (value === selectedValue) { + opt.selected = true; + } + select.appendChild(opt); + }); + select.dispatchEvent(new Event('change', { bubbles: true })); + }, + + populateItemForm: function(item) { + const form = this.$refs.menuItemForm; + if (!form) { + return; + } + + // Checkboxes must be matched explicitly - October renders a hidden + // "0" input with the same name before each checkbox, which a plain + // name selector would match instead. + const setField = (name, value) => { + const checkbox = form.querySelector('input[type="checkbox"][name="' + name + '"]'); + if (checkbox) { + checkbox.checked = !!value && value !== '0'; + checkbox.dispatchEvent(new Event('change', { bubbles: true })); + return; + } + + const input = form.querySelector('[name="' + name + '"]'); + if (input) { + input.value = (value === null || value === undefined) ? '' : value; + input.dispatchEvent(new Event('change', { bubbles: true })); + } + }; + + Object.keys(item).forEach((key) => { + if (key.charAt(0) === '_' || key === 'typeLabel') { + return; + } + + // View bag values map to nested field names; the locale key holds + // per-locale translations, not form fields. + if (key === 'viewBag' && item.viewBag && typeof item.viewBag === 'object') { + Object.keys(item.viewBag).forEach((vbKey) => { + if (vbKey === 'locale') { + return; + } + setField('menuItem[viewBag][' + vbKey + ']', item.viewBag[vbKey]); + }); + return; + } + + setField('menuItem[' + key + ']', item[key]); + }); + }, + + applyItemForm: function() { + const form = this.$refs.menuItemForm; + if (!form || !this.selectedItem) { + return; + } + + const formData = new FormData(form); + const viewBag = {}; + for (const [name, value] of formData.entries()) { + const vbMatch = name.match(/^menuItem\[viewBag\]\[([^\]]+)\]$/); + if (vbMatch) { + viewBag[vbMatch[1]] = value; + continue; + } + const m = name.match(/^menuItem\[([^\]]+)\]$/); + if (m) { + this.selectedItem[m[1]] = value; + } + } + // Merge over the existing view bag - keys not represented in the form + // (e.g. the nested locale translations) must survive the round trip. + this.selectedItem.viewBag = Object.assign({}, this.selectedItem.viewBag, viewBag); + + // Refresh the row subtitle from the (possibly changed) type. + const typeInput = form.querySelector('[name="menuItem[type]"]'); + if (typeInput) { + const opt = typeInput.options ? typeInput.options[typeInput.selectedIndex] : null; + this.selectedItem.typeLabel = opt ? opt.text : this.selectedItem.type; + } + + this.syncItemsToDocument(); + }, + + documentCreatedOrLoaded: function() { + this.items = this.inflateItems(this.documentData.items || []); + }, + + onToolbarCommand: function(command, isHotkey) { + if (command === 'add-item') { + this.addItem(); + return; + } + this.handleBasicDocumentCommands(command, isHotkey); + } + } +}; diff --git a/vuecomponents/menueditor/partials/_menueditor.php b/vuecomponents/menueditor/partials/_menueditor.php new file mode 100644 index 00000000..3c1d27c0 --- /dev/null +++ b/vuecomponents/menueditor/partials/_menueditor.php @@ -0,0 +1,111 @@ + + + + + + + diff --git a/vuecomponents/staticpageeditor/assets/js/staticpageeditor.js b/vuecomponents/staticpageeditor/assets/js/staticpageeditor.js new file mode 100644 index 00000000..55d09dc2 --- /dev/null +++ b/vuecomponents/staticpageeditor/assets/js/staticpageeditor.js @@ -0,0 +1,634 @@ +import { DocumentComponentBase } from '../../../../../../../modules/editor/assets/js/editor.extension.documentcomponent.base.js'; +import EditorModelDefinition from '../../../../../../../modules/backend/vuecomponents/monacoeditor/assets/js/modeldefinition.js'; + +// Each open page document renders its own syntax-field islands; a per-instance +// counter keeps the container ids and form widget alias (which prefixes every +// field id) unique so labels always target their own tab's inputs. +let syntaxIslandUid = 0; + +export default { + extends: DocumentComponentBase, + data: function() { + const uid = ++syntaxIslandUid; + + return { + syntaxFormUid: uid, + documentSettingsPopupTitle: this.trans('Static page') || 'Static Page', + previewUrl: null, + hasContentField: true, + // Layout-driven UI state lives outside documentData: the settings popup + // deep-clones documentData back over itself on apply, which would wipe + // any values refreshed by the save that runs before the apply. + placeholderInfo: {}, + syntaxFieldGroups: [], + // Last auto-generated URL for a new subpage. While the URL still matches + // this value, typing the title keeps rebuilding it as parentUrl + slug; + // a manual URL edit (or saving) stops the preset. Null when inactive. + autoUrlValue: null, + // Monaco backs only the "code" surfaces (text-type placeholders). + codeEditorModelDefinitions: [], + codeModels: {}, + modelsReady: false, + // Tabs: the main content surface, one per layout placeholder, and one per + // layout syntax-field group. + activeSurfaceKey: 'markup', + loadedSyntaxGroups: {}, + loadingSyntaxGroups: {}, + // Each rich surface's richeditor connector owns its own toolbar-button array, + // keyed by surface key. The document toolbar renders the active surface's array + // (activeToolbarExtension) so switching tabs just swaps which array is shown - + // the connector never has to re-emit its buttons. + surfaceToolbars: {} + }; + }, + computed: { + // Every editable content region as a tab. + // - mode 'rich' = WYSIWYG richeditor (page content, html placeholders) + // - mode 'code' = Monaco (text-type placeholders) + // - mode 'syntax' = server-rendered Form-widget island (layout syntax fields) + contentSurfaces: function() { + const surfaces = []; + + // The layout's staticPage component can disable the content field (useContent). + if (this.hasContentField) { + surfaces.push({ key: 'markup', title: this.trans('Content') || 'Content', mode: 'rich', holder: 'root' }); + } + + const info = this.placeholderInfo || {}; + Object.keys(info).forEach((code) => { + const meta = info[code] || {}; + surfaces.push({ + key: code, + title: meta.title || code, + mode: meta.type === 'text' ? 'code' : 'rich', + holder: 'placeholder' + }); + }); + + const groups = this.syntaxFieldGroups || []; + groups.forEach((group) => { + surfaces.push({ + key: group.key, + title: group.title, + mode: 'syntax', + tab: group.title, + containerId: 'pagesSyntax' + this.syntaxFormUid + '_' + group.key.replace(/[^a-z0-9]/gi, '') + }); + }); + + return surfaces; + }, + + hasTabs: function() { + return this.contentSurfaces.length > 1; + }, + + surfaceTabs: function() { + return this.contentSurfaces.map((surface) => ({ + key: surface.key, + label: surface.title + })); + }, + + syntaxSurfaces: function() { + return this.contentSurfaces.filter((s) => s.mode === 'syntax'); + }, + + activeSurface: function() { + return this.contentSurfaces.find((s) => s.key === this.activeSurfaceKey) + || this.contentSurfaces[0]; + }, + + showCodeEditor: function() { + return this.modelsReady + && this.activeSurface + && this.activeSurface.mode === 'code'; + }, + + // The richeditor toolbar buttons for the active surface (empty for non-rich). + activeToolbarExtension: function() { + const surface = this.activeSurface; + if (surface && surface.mode === 'rich') { + return this.surfaceToolbars[surface.key] || []; + } + return []; + }, + + toolbarElements: function() { + return [].concat([ + { + type: 'button', + icon: 'icon-save-cloud', + label: this.trans('backend::lang.form.save'), + hotkey: 'ctrl+s, cmd+s', + tooltip: this.trans('backend::lang.form.save'), + command: 'save' + }, + { + type: 'button', + target: '_blank', + href: this.previewUrl, + disabled: this.previewUrl === null || this.isNewDocument, + icon: 'icon-location-target', + label: this.trans('Preview') || 'Preview', + tooltip: this.trans('Preview') || 'Preview', + command: 'preview' + }, + { + type: 'button', + icon: 'icon-settings', + label: this.trans('editor::lang.common.settings'), + command: 'settings', + hidden: !this.hasSettingsForm + }, + this.activeToolbarExtension, + { + type: 'separator' + }, + { + type: 'button', + icon: 'icon-delete', + disabled: this.isNewDocument, + command: 'delete', + hotkey: 'shift+option+d', + tooltip: this.trans('backend::lang.form.delete') + }, + { + type: 'button', + icon: this.documentHeaderCollapsed ? 'icon-angle-down' : 'icon-angle-up', + command: 'document:toggleToolbar', + fixedRight: true, + tooltip: this.trans('editor::lang.common.toggle_document_header') + } + ]); + } + }, + watch: { + // The header subtitle edits the top-level url; saving and the settings + // popup read settings.url - keep the two in sync both ways. + 'documentData.url': function(value) { + if (this.documentData && this.documentData.settings && this.documentData.settings.url !== value) { + this.documentData.settings.url = value; + } + }, + 'documentData.settings.url': function(value) { + if (this.documentData && this.documentData.url !== value) { + this.documentData.url = value; + } + }, + + // For new subpages, typing the title keeps building the URL as + // parentUrl + slug until the URL is edited by hand. + 'documentData.title': function(value) { + if (this.autoUrlValue === null || !this.isNewDocument) { + return; + } + + // A URL differing from the last generated value means it was edited by hand + if (this.documentData.url !== this.autoUrlValue) { + this.autoUrlValue = null; + return; + } + + const prefix = String(this.documentMetadata.parentUrl || '').replace(/\/+$/, ''); + const slug = oc.InputPresetEngine.formatValue( + { inputPresetType: 'url', inputPresetRemoveWords: true }, + String(value || '') + ); + + this.autoUrlValue = (slug === '/' || slug === '') ? prefix + '/' : prefix + slug; + this.documentData.url = this.autoUrlValue; + }, + + // The settings popup deep-clones its snapshot back over documentData on + // apply, replacing the placeholders object - rebind the code models so + // Monaco keeps writing into the live object. + 'documentData.placeholders': function(newValue, oldValue) { + if (newValue === oldValue || !this.modelsReady) { + return; + } + + this.modelsReady = false; + this.ensurePlaceholderKeys(); + this.buildCodeModels(); + this.$nextTick(() => { + this.modelsReady = true; + }); + } + }, + methods: { + getRootProperties: function() { + return ['fileName', 'markup', 'placeholders']; + }, + + getMainUiDocumentProperties: function() { + return ['title', 'url', 'markup', 'placeholders']; + }, + + surfaceValue: function(surface) { + if (surface.holder === 'root') { + return this.documentData.markup; + } + return this.documentData.placeholders[surface.key]; + }, + + setSurfaceValue: function(surface, value) { + if (surface.holder === 'root') { + this.documentData.markup = value; + } + else { + this.documentData.placeholders[surface.key] = value; + } + }, + + onSurfaceTabSelected: function(key) { + this.selectSurface(key); + }, + + selectSurface: function(key) { + this.activeSurfaceKey = key; + + this.$nextTick(() => { + const surface = this.activeSurface; + if (!surface) { + return; + } + + if (surface.mode === 'code' && this.$refs.editor) { + const def = this.codeModels[surface.key]; + if (def) { + this.$refs.editor.updateValue(def, this.surfaceValue(surface)); + this.$refs.editor.layout(); + } + } + else if (surface.mode === 'rich') { + this.refreshRichSurface(surface.key); + } + else if (surface.mode === 'syntax' && !this.loadedSyntaxGroups[surface.key]) { + this.loadSyntaxGroup(surface); + } + }); + }, + + // Refresh a rich surface's connector once it becomes active. The connector runs + // updateSize()/extendToolbar() on mount, but on first load Froala's wrapper may not + // be ready yet (so the resizer stays non-responsive until the tab is toggled). Retry + // until the Froala wrapper exists, then run both: + // - updateSize(): recomputes the resizable width/centering (responsive resize) + // - extendToolbar(): repopulates the toolbar buttons + refreshRichSurface: function(key, attempt) { + attempt = attempt || 0; + + if (this.activeSurfaceKey !== key || attempt > 20) { + return; + } + + const connector = this.$refs['richEditor_' + key]; + const inst = Array.isArray(connector) ? connector[0] : connector; + + // Wait for the connector and its Froala wrapper to exist before measuring. + const wrapperReady = inst && inst.$el && inst.$el.querySelector('.fr-wrapper'); + if (!wrapperReady) { + setTimeout(() => this.refreshRichSurface(key, attempt + 1), 50); + return; + } + + if (typeof inst.updateSize === 'function') { + inst.updateSize(); + } + if (typeof inst.extendToolbar === 'function') { + inst.extendToolbar(); + } + }, + + buildCodeModels: function() { + if (!this.documentData.placeholders || typeof this.documentData.placeholders !== 'object') { + this.documentData.placeholders = {}; + } + + const defs = []; + const models = {}; + + this.contentSurfaces.forEach((surface) => { + if (surface.mode !== 'code') { + return; + } + + if (surface.holder === 'placeholder' && this.documentData.placeholders[surface.key] === undefined) { + this.documentData.placeholders[surface.key] = ''; + } + + const holderObject = surface.holder === 'root' + ? this.documentData + : this.documentData.placeholders; + const holderProperty = surface.holder === 'root' ? 'markup' : surface.key; + + const def = new EditorModelDefinition( + 'plaintext', + surface.title, + holderObject, + holderProperty, + 'backend-icon-background monaco-document html' + ); + + defs.push(def); + models[surface.key] = def; + }); + + this.codeEditorModelDefinitions = defs; + this.codeModels = models; + }, + + ensurePlaceholderKeys: function() { + const info = this.placeholderInfo || {}; + if (!this.documentData.placeholders || typeof this.documentData.placeholders !== 'object') { + this.documentData.placeholders = {}; + } + Object.keys(info).forEach((code) => { + if (this.documentData.placeholders[code] === undefined) { + this.documentData.placeholders[code] = ''; + } + }); + }, + + getSaveDocumentData: function(inspectorDocumentData) { + const rootProperties = this.getRootProperties(); + const documentData = inspectorDocumentData ? inspectorDocumentData : this.documentData; + + const data = $.oc.vueUtils.getCleanObject(documentData); + const result = { settings: {} }; + + const ignoredProperties = ['placeholderInfo', 'syntaxFieldGroups']; + + Object.keys(data).forEach((property) => { + if (property === 'settings' || ignoredProperties.indexOf(property) !== -1) { + return; + } + + if (rootProperties.indexOf(property) !== -1) { + result[property] = data[property]; + } + else { + result.settings[property] = data[property]; + } + }); + + if (typeof data.settings === 'object' && data.settings !== null) { + Object.keys(data.settings).forEach((property) => { + if (rootProperties.indexOf(property) === -1 && result.settings[property] === undefined) { + result.settings[property] = data.settings[property]; + } + }); + } + + // Merge values from every loaded syntax-field island into settings (viewBag). + const syntaxData = this.collectSyntaxFieldData(); + Object.keys(syntaxData).forEach((key) => { + result.settings[key] = syntaxData[key]; + }); + + return result; + }, + + collectSyntaxFieldData: function() { + const result = {}; + + this.syntaxSurfaces.forEach((surface) => { + const form = this.$refs['form_' + surface.containerId]; + const el = Array.isArray(form) ? form[0] : form; + if (!this.loadedSyntaxGroups[surface.key] || !el) { + return; + } + + const formData = new FormData(el); + for (const [name, value] of formData.entries()) { + const match = name.match(/^syntaxFields\[viewBag\]\[([^\]]+)\](.*)$/); + if (!match) { + continue; + } + + const path = [match[1]]; + const bracketRe = /\[([^\]]*)\]/g; + let m; + while ((m = bracketRe.exec(match[2])) !== null) { + path.push(m[1]); + } + + this.assignNested(result, path, value); + } + }); + + return result; + }, + + assignNested: function(target, path, value) { + let node = target; + for (let i = 0; i < path.length - 1; i++) { + const key = path[i]; + if (node[key] === undefined || typeof node[key] !== 'object') { + node[key] = {}; + } + node = node[key]; + } + node[path[path.length - 1]] = value; + }, + + loadSyntaxGroup: function(surface) { + const form = this.$refs['form_' + surface.containerId]; + const el = Array.isArray(form) ? form[0] : form; + if (!el || this.loadingSyntaxGroups[surface.key]) { + return; + } + + this.loadingSyntaxGroups[surface.key] = true; + + // Load the group's Form-widget island. Core's MutationObserver auto-initializes + // the injected controls (repeater, mediafinder, richeditor, ...). + oc.request(el, 'onLoadSyntaxFields', { + data: { + path: this.documentMetadata.path, + tab: surface.tab, + containerId: surface.containerId, + formAlias: 'pagesSyntaxForm' + this.syntaxFormUid + } + }).then( + () => { + this.loadedSyntaxGroups[surface.key] = true; + this.loadingSyntaxGroups[surface.key] = false; + }, + () => { + this.loadingSyntaxGroups[surface.key] = false; + } + ); + }, + + onToolbarCommand: function(command, isHotkey, ev) { + this.handleBasicDocumentCommands(command, isHotkey); + + const surface = this.activeSurface; + if (surface && surface.mode === 'rich') { + const connector = this.$refs['richEditor_' + this.activeSurfaceKey]; + const inst = Array.isArray(connector) ? connector[0] : connector; + if (inst && inst.internalEventBus) { + inst.internalEventBus.emit('toolbarcmd', { command: command, ev: ev }); + } + } + }, + + documentLoaded: function(data) { + this.previewUrl = (data && data.previewUrl) || null; + if (data && data.hasContentField !== undefined) { + this.hasContentField = data.hasContentField !== false; + } + this.ensureActiveSurfaceExists(); + + this.$nextTick(() => { + if (this.$refs.editor) { + Object.keys(this.codeModels).forEach((key) => { + const def = this.codeModels[key]; + const surface = this.contentSurfaces.find((s) => s.key === key); + if (surface) { + this.$refs.editor.updateValue(def, this.surfaceValue(surface)); + } + }); + } + + // Ensure the initially-active rich surface's toolbar is populated. + if (this.activeSurface && this.activeSurface.mode === 'rich') { + this.refreshRichSurface(this.activeSurfaceKey); + } + }); + }, + + documentCreatedOrLoaded: function() { + this.placeholderInfo = (this.documentData && this.documentData.placeholderInfo) || {}; + this.syntaxFieldGroups = (this.documentData && this.documentData.syntaxFieldGroups) || []; + + this.autoUrlValue = ( + this.documentMetadata && + this.documentMetadata.isNewDocument && + this.documentMetadata.parentUrl + ) ? this.documentData.url : null; + + this.ensurePlaceholderKeys(); + this.buildCodeModels(); + this.loadedSyntaxGroups = {}; + this.loadingSyntaxGroups = {}; + + // Pre-create a stable toolbar array for every rich surface before the editor + // panel renders, so each connector binds to (and mutates) its own live array. + const toolbars = {}; + this.contentSurfaces.forEach((surface) => { + if (surface.mode === 'rich') { + toolbars[surface.key] = []; + } + }); + this.surfaceToolbars = toolbars; + + this.activeSurfaceKey = 'markup'; + this.ensureActiveSurfaceExists(); + this.modelsReady = true; + }, + + ensureActiveSurfaceExists: function() { + if (!this.contentSurfaces.find((s) => s.key === this.activeSurfaceKey)) { + this.activeSurfaceKey = this.contentSurfaces.length ? this.contentSurfaces[0].key : 'markup'; + } + }, + + documentSaved: function(data) { + if (!data) { + return; + } + + if (data.previewUrl !== undefined) { + this.previewUrl = data.previewUrl; + } + + // A layout change alters the placeholder tabs, syntax-field groups and the + // content field visibility - rebuild the surfaces when they changed. + const infoChanged = + (data.placeholderInfo !== undefined && + JSON.stringify(data.placeholderInfo) !== JSON.stringify(this.placeholderInfo || {})) || + (data.syntaxFieldGroups !== undefined && + JSON.stringify(data.syntaxFieldGroups) !== JSON.stringify(this.syntaxFieldGroups || [])) || + (data.hasContentField !== undefined && (data.hasContentField !== false) !== this.hasContentField); + + if (!infoChanged) { + return; + } + + if (data.placeholderInfo !== undefined) { + this.placeholderInfo = data.placeholderInfo; + } + if (data.syntaxFieldGroups !== undefined) { + this.syntaxFieldGroups = data.syntaxFieldGroups; + } + if (data.hasContentField !== undefined) { + this.hasContentField = data.hasContentField !== false; + } + + this.rebuildContentSurfaces(); + }, + + // Rebuilds the tabs, code models and toolbars after the layout-driven + // surface set changed. + rebuildContentSurfaces: function() { + this.modelsReady = false; + this.ensurePlaceholderKeys(); + this.buildCodeModels(); + this.loadedSyntaxGroups = {}; + this.loadingSyntaxGroups = {}; + + const toolbars = {}; + this.contentSurfaces.forEach((surface) => { + if (surface.mode === 'rich') { + toolbars[surface.key] = this.surfaceToolbars[surface.key] || []; + } + }); + this.surfaceToolbars = toolbars; + + this.ensureActiveSurfaceExists(); + + this.$nextTick(() => { + this.modelsReady = true; + + this.$nextTick(() => { + const surface = this.activeSurface; + if (surface && surface.mode === 'syntax' && !this.loadedSyntaxGroups[surface.key]) { + this.loadSyntaxGroup(surface); + } + else if (surface && surface.mode === 'rich') { + this.refreshRichSurface(surface.key); + } + }); + }); + }, + + // Keep the active rich surface's resizable width in sync on window resize. The + // connector's own resize handler is unreliable here (its listener loses the + // component `this`), so drive updateSize ourselves. Debounced. + onWindowResize: function() { + if (this.resizeDebounce) { + clearTimeout(this.resizeDebounce); + } + this.resizeDebounce = setTimeout(() => { + if (this.activeSurface && this.activeSurface.mode === 'rich') { + this.refreshRichSurface(this.activeSurfaceKey); + } + }, 10); + } + }, + mounted: function() { + this.boundWindowResize = this.onWindowResize.bind(this); + window.addEventListener('resize', this.boundWindowResize); + }, + beforeUnmount: function() { + if (this.boundWindowResize) { + window.removeEventListener('resize', this.boundWindowResize); + } + if (this.resizeDebounce) { + clearTimeout(this.resizeDebounce); + } + } +}; diff --git a/vuecomponents/staticpageeditor/partials/_staticpageeditor.php b/vuecomponents/staticpageeditor/partials/_staticpageeditor.php new file mode 100644 index 00000000..3a307b2c --- /dev/null +++ b/vuecomponents/staticpageeditor/partials/_staticpageeditor.php @@ -0,0 +1,110 @@ + + + + + + + diff --git a/widgets/MenuList.php b/widgets/MenuList.php deleted file mode 100644 index a9a43d10..00000000 --- a/widgets/MenuList.php +++ /dev/null @@ -1,127 +0,0 @@ -alias = $alias; - $this->theme = Theme::getEditTheme(); - $this->dataIdPrefix = 'page-'.$this->theme->getDirName(); - - parent::__construct($controller, []); - $this->bindToController(); - } - - /** - * Renders the widget. - * @return string - */ - public function render() - { - return $this->makePartial('body', [ - 'data' => $this->getData() - ]); - } - - // - // Event handlers - // - - public function onUpdate() - { - $this->extendSelection(); - - return $this->updateList(); - } - - public function onSearch() - { - $this->setSearchTerm(Input::get('search')); - $this->extendSelection(); - - return $this->updateList(); - } - - // - // Methods for the internal use - // - - protected function getData() - { - $menus = Menu::listInTheme($this->theme, true); - - $searchTerm = Str::lower($this->getSearchTerm()); - - if (strlen($searchTerm)) { - $words = explode(' ', $searchTerm); - $filteredMenus = []; - - foreach ($menus as $menu) { - if ($this->textMatchesSearch($words, $menu->name.' '.$menu->fileName)) { - $filteredMenus[] = $menu; - } - } - - $menus = $filteredMenus; - } - - if ($sortMenusBy = Config::get('rainlab.pages::menus_sort_by', false)) { - return $menus->sortBy($sortMenusBy); - } - - return $menus; - } - - protected function updateList() - { - $vars = ['items' => $this->getData()]; - return ['#'.$this->getId('menu-list') => $this->makePartial('items', $vars)]; - } - - protected function getThemeSessionKey($prefix) - { - return $prefix . $this->theme->getDirName(); - } - - protected function getSession($key = null, $default = null) - { - $key = strlen($key) ? $this->getThemeSessionKey($key) : $key; - - return parent::getSession($key, $default); - } - - protected function putSession($key, $value) - { - return parent::putSession($this->getThemeSessionKey($key), $value); - } -} diff --git a/widgets/PageList.php b/widgets/PageList.php deleted file mode 100644 index 2ae2c415..00000000 --- a/widgets/PageList.php +++ /dev/null @@ -1,165 +0,0 @@ -alias = $alias; - $this->theme = Theme::getEditTheme(); - $this->dataIdPrefix = 'page-'.$this->theme->getDirName(); - - parent::__construct($controller, []); - $this->bindToController(); - } - - /** - * Renders the widget. - * @return string - */ - public function render() - { - return $this->makePartial('body', [ - 'data' => $this->getData() - ]); - } - - /* - * Event handlers - */ - - public function onReorder() - { - $structure = json_decode(Input::get('structure'), true); - if (!$structure) { - throw new SystemException('Invalid structure data posted.'); - } - - $pageList = new StaticPageList($this->theme); - $pageList->updateStructure($structure); - } - - public function onUpdate() - { - $this->extendSelection(); - - return $this->updateList(); - } - - public function onSearch() - { - $this->setSearchTerm(Input::get('search')); - $this->extendSelection(); - - return $this->updateList(); - } - - /* - * Methods for internal use - */ - - protected function getData() - { - $pageList = new StaticPageList($this->theme); - $pages = $pageList->getPageTree(true); - - $searchTerm = Str::lower($this->getSearchTerm()); - - if (strlen($searchTerm)) { - $words = explode(' ', $searchTerm); - - $iterator = function($pages) use (&$iterator, $words) { - $result = []; - - foreach ($pages as $page) { - if ($this->textMatchesSearch($words, $this->subtreeToText($page))) { - $result[] = (object) [ - 'page' => $page->page, - 'subpages' => $iterator($page->subpages) - ]; - } - } - - return $result; - }; - - $pages = $iterator($pages); - } - - return $pages; - } - - protected function getThemeSessionKey($prefix) - { - return $prefix.$this->theme->getDirName(); - } - - protected function updateList() - { - return ['#'.$this->getId('page-list') => $this->makePartial('items', ['items' => $this->getData()])]; - } - - protected function subtreeToText($page) - { - $result = $this->pageToText($page->page); - - $iterator = function($pages) use (&$iterator, &$result) { - foreach ($pages as $page) { - $result .= ' '.$this->pageToText($page->page); - $iterator($page->subpages); - } - }; - - $iterator($page->subpages); - - return $result; - } - - protected function pageToText($page) - { - $viewBag = $page->getViewBag(); - - return $page->getViewBag()->property('title').' '.$page->getViewBag()->property('url'); - } - - protected function getSession($key = null, $default = null) - { - $key = strlen($key) ? $this->getThemeSessionKey($key) : $key; - - return parent::getSession($key, $default); - } - - protected function putSession($key, $value) - { - return parent::putSession($this->getThemeSessionKey($key), $value); - } -} diff --git a/widgets/TemplateList.php b/widgets/TemplateList.php deleted file mode 100644 index 3e53ca39..00000000 --- a/widgets/TemplateList.php +++ /dev/null @@ -1,415 +0,0 @@ -'URL'] - */ - public $descriptionProperties = []; - - /** - * @var string object property to use as a description. - */ - public $descriptionProperty; - - /** - * @var string Message to display when there are no records in the list. - */ - public $noRecordsMessage = 'rainlab.pages::lang.template.no_list_records'; - - /** - * @var string Message to display when the Delete button is clicked. - */ - public $deleteConfirmation = 'rainlab.pages::lang.template.delete_confirm'; - - /** - * @var string Specifies the item type. - */ - public $itemType; - - /** - * @var string Extra CSS class name to apply to the control. - */ - public $controlClass; - - /** - * @var array A list of file name patterns to suppress / hide. - */ - public $ignoreDirectories = []; - - /** - * @var array Defines sorting properties. - * The sorting feature is disabled if there are no sorting properties defined. - */ - public $sortingProperties = []; - - /* - * Public methods - */ - - public function __construct($controller, $alias, callable $dataSource) - { - $this->alias = $alias; - $this->dataSource = $dataSource; - $this->theme = Theme::getEditTheme(); - $this->selectionInputName = 'template'; - $this->collapseSessionKey = $this->getThemeSessionKey('groups'); - - parent::__construct($controller, []); - - if (!Request::isXmlHttpRequest()) { - $this->resetSelection(); - } - - $configFile = 'config_' . snake_case($alias) .'.yaml'; - $config = $this->makeConfig($configFile); - - foreach ($config as $field => $value) { - if (property_exists($this, $field)) { - $this->$field = $value; - } - } - - $this->bindToController(); - } - - /** - * Renders the widget. - * @return string - */ - public function render() - { - $toolbarClass = Str::contains($this->controlClass, 'hero') ? 'separator' : null; - - $this->vars['toolbarClass'] = $toolbarClass; - - return $this->makePartial('body', [ - 'data' => $this->getData() - ]); - } - - /* - * Event handlers - */ - - public function onSearch() - { - $this->setSearchTerm(Input::get('search')); - $this->extendSelection(); - - return $this->updateList(); - } - - public function onUpdate() - { - $this->extendSelection(); - - return $this->updateList(); - } - - public function onApplySorting() - { - $this->setSortingProperty(Input::get('sortProperty')); - - $result = $this->updateList(); - $result['#'.$this->getId('sorting-options')] = $this->makePartial('sorting-options'); - - return $result; - } - - // - // Methods for the internal use - // - - protected function getData() - { - /* - * Load the data - */ - $items = call_user_func($this->dataSource); - - if ($items instanceof \October\Rain\Support\Collection) { - $items = $items->all(); - } - - $items = $this->removeIgnoredDirectories($items); - - $items = array_map([$this, 'normalizeItem'], $items); - - $this->sortItems($items); - - /* - * Apply the search - */ - $filteredItems = []; - $searchTerm = Str::lower($this->getSearchTerm()); - - if (strlen($searchTerm)) { - /* - * Exact - */ - foreach ($items as $index => $item) { - if ($this->itemContainsWord($searchTerm, $item, true)) { - $filteredItems[] = $item; - unset($items[$index]); - } - } - - /* - * Fuzzy - */ - $words = explode(' ', $searchTerm); - foreach ($items as $item) { - if ($this->itemMatchesSearch($words, $item)) { - $filteredItems[] = $item; - } - } - } - else { - $filteredItems = $items; - } - - /* - * Group the items - */ - $result = []; - $foundGroups = []; - foreach ($filteredItems as $itemData) { - $pos = strpos($itemData->fileName, '/'); - - if ($pos !== false) { - $group = substr($itemData->fileName, 0, $pos); - if (!array_key_exists($group, $foundGroups)) { - $newGroup = (object)[ - 'title' => $group, - 'items' => [] - ]; - - $foundGroups[$group] = $newGroup; - } - - $foundGroups[$group]->items[] = $itemData; - } - else { - $result[] = $itemData; - } - } - - // Sort folders by name regardless of the - // selected sorting options. - ksort($foundGroups); - - foreach ($foundGroups as $group) { - $result[] = $group; - } - - return $result; - } - - protected function sortItems(&$items) - { - $sortingProperty = $this->getSortingProperty(); - - usort($items, function ($a, $b) use ($sortingProperty) { - return strcmp($a->$sortingProperty, $b->$sortingProperty); - }); - } - - protected function removeIgnoredDirectories($items) - { - if (!$this->ignoreDirectories) { - return $items; - } - - $ignoreCache = []; - - $items = array_filter($items, function ($item) use (&$ignoreCache) { - $fileName = $item->getBaseFileName(); - $dirName = dirname($fileName); - - if (isset($ignoreCache[$dirName])) { - return false; - } - - foreach ($this->ignoreDirectories as $ignoreDir) { - if (File::fileNameMatch($dirName, $ignoreDir)) { - $ignoreCache[$dirName] = true; - return false; - } - } - - return true; - }); - - return $items; - } - - protected function normalizeItem($item) - { - $description = null; - if ($descriptionProperty = $this->descriptionProperty) { - $description = $item->$descriptionProperty; - } - - $descriptions = []; - foreach ($this->descriptionProperties as $property => $title) { - if ($item->$property) { - $descriptions[$title] = $item->$property; - } - } - - $result = [ - 'title' => $this->getItemTitle($item), - 'fileName' => $item->getFileName(), - 'description' => $description, - 'descriptions' => $descriptions, - 'dragValue' => $this->getItemDragValue($item) - ]; - - foreach ($this->sortingProperties as $property => $name) { - $result[$property] = $item->$property; - } - - return (object) $result; - } - - protected function getItemDragValue($item) - { - if ($item instanceof \Cms\Classes\Partial) { - return "{% partial '".$item->getBaseFileName()."' %}"; - } - - if ($item instanceof \Cms\Classes\Content) { - return "{% content '".$item->getBaseFileName()."' %}"; - } - - if ($item instanceof \Cms\Classes\Page) { - return "{{ '".$item->getBaseFileName()."'|page }}"; - } - - return ''; - } - - protected function getItemTitle($item) - { - $titleProperty = $this->titleProperty; - - if ($titleProperty) { - return $item->$titleProperty ?: basename($item->getFileName()); - } - - return basename($item->getFileName()); - } - - protected function setSearchTerm($term) - { - $this->searchTerm = trim($term); - $this->putSession('search', $this->searchTerm); - } - - protected function getSearchTerm() - { - return $this->searchTerm !== false ? $this->searchTerm : $this->getSession('search'); - } - - protected function updateList() - { - return [ - '#'.$this->getId('template-list') => $this->makePartial('items', ['items' => $this->getData()]) - ]; - } - - protected function itemMatchesSearch($words, $item) - { - foreach ($words as $word) { - $word = trim($word); - if (!strlen($word)) { - continue; - } - - if (!$this->itemContainsWord($word, $item)) { - return false; - } - } - - return true; - } - - protected function itemContainsWord($word, $item, $exact = false) - { - $operator = $exact ? 'is' : 'contains'; - - if (strlen($item->title) && Str::$operator(Str::lower($item->title), $word)) { - return true; - } - - if (Str::$operator(Str::lower($item->fileName), $word)) { - return true; - } - - if (Str::$operator(Str::lower($item->description), $word) && strlen($item->description)) { - return true; - } - - foreach ($item->descriptions as $value) { - if (Str::$operator(Str::lower($value), $word) && strlen($value)) { - return true; - } - } - - return false; - } - - protected function getThemeSessionKey($prefix) - { - return $prefix.$this->theme->getDirName(); - } - - protected function getSortingProperty() - { - $property = $this->getSession($this->getThemeSessionKey('sorting_property'), self::SORTING_FILENAME); - - if (!array_key_exists($property, $this->sortingProperties)) { - return self::SORTING_FILENAME; - } - - return $property; - } - - protected function setSortingProperty($property) - { - $this->putSession($this->getThemeSessionKey('sorting_property'), $property); - } -} diff --git a/widgets/menulist/partials/_body.htm b/widgets/menulist/partials/_body.htm deleted file mode 100644 index da0e6963..00000000 --- a/widgets/menulist/partials/_body.htm +++ /dev/null @@ -1,10 +0,0 @@ - -makePartial('toolbar') ?> - -
    -
    -
    - makePartial('menus', ['data' => $data]) ?> -
    -
    -
    \ No newline at end of file diff --git a/widgets/menulist/partials/_items.htm b/widgets/menulist/partials/_items.htm deleted file mode 100644 index 94696c3f..00000000 --- a/widgets/menulist/partials/_items.htm +++ /dev/null @@ -1,39 +0,0 @@ - - - -

    noRecordsMessage)) ?>

    - - - - - diff --git a/widgets/menulist/partials/_menus.htm b/widgets/menulist/partials/_menus.htm deleted file mode 100644 index c5f03acd..00000000 --- a/widgets/menulist/partials/_menus.htm +++ /dev/null @@ -1,10 +0,0 @@ -
    -
    - -
    -
    \ No newline at end of file diff --git a/widgets/menulist/partials/_toolbar.htm b/widgets/menulist/partials/_toolbar.htm deleted file mode 100644 index 9332ec55..00000000 --- a/widgets/menulist/partials/_toolbar.htm +++ /dev/null @@ -1,37 +0,0 @@ -
    -
    - - -
    -
    - - -
    -
    - - -
    - " - data-track-input - data-load-indicator - data-load-indicator-opaque - data-request-success="$('getId('delete-button') ?>').trigger('oc.triggerOn.update')" - data-request="getEventHandler('onSearch') ?>" - /> -
    - -
    -
    \ No newline at end of file diff --git a/widgets/pagelist/partials/_body.htm b/widgets/pagelist/partials/_body.htm deleted file mode 100644 index cf5e883f..00000000 --- a/widgets/pagelist/partials/_body.htm +++ /dev/null @@ -1,10 +0,0 @@ - -makePartial('toolbar') ?> - -
    -
    -
    - makePartial('pages', ['data' => $data]) ?> -
    -
    -
    \ No newline at end of file diff --git a/widgets/pagelist/partials/_items.htm b/widgets/pagelist/partials/_items.htm deleted file mode 100644 index 61b07919..00000000 --- a/widgets/pagelist/partials/_items.htm +++ /dev/null @@ -1,9 +0,0 @@ - -
      - makePartial('treebranch', ['items' => $items]) ?> -
    - -

    noRecordsMessage) ?>

    - - - \ No newline at end of file diff --git a/widgets/pagelist/partials/_pages.htm b/widgets/pagelist/partials/_pages.htm deleted file mode 100644 index 4748886c..00000000 --- a/widgets/pagelist/partials/_pages.htm +++ /dev/null @@ -1,12 +0,0 @@ -
    -
    -
    - makePartial('items', ['items' => $data]) ?> -
    -
    -
    \ No newline at end of file diff --git a/widgets/pagelist/partials/_toolbar.htm b/widgets/pagelist/partials/_toolbar.htm deleted file mode 100644 index 460f516e..00000000 --- a/widgets/pagelist/partials/_toolbar.htm +++ /dev/null @@ -1,37 +0,0 @@ -
    -
    - - -
    -
    - - -
    -
    - - -
    - " - data-track-input - data-load-indicator - data-load-indicator-opaque - data-request-success="$('getId('delete-button') ?>').trigger('oc.triggerOn.update')" - data-request="getEventHandler('onSearch') ?>" - /> -
    - -
    -
    \ No newline at end of file diff --git a/widgets/pagelist/partials/_treebranch.htm b/widgets/pagelist/partials/_treebranch.htm deleted file mode 100644 index f8f09930..00000000 --- a/widgets/pagelist/partials/_treebranch.htm +++ /dev/null @@ -1,69 +0,0 @@ - - page->getBaseFileName(); - $groupStatus = $this->getCollapseStatus($fileName, false); - $dataId = $this->dataIdPrefix.'-'.$fileName; - $searchMode = strlen($this->getSearchTerm()) > 0; - $cbId = 'cb'.md5($fileName); - - $pageTitle = $pageObj->page->getViewBag()->property('title'); - $pageUrl = $pageObj->page->getViewBag()->property('url'); - - if (class_exists('\RainLab\Translate\Behaviors\TranslatableModel')) { - $locale = \RainLab\Translate\Classes\Translator::instance()->getLocale(); - $pageTitle = isset($pageObj->page->viewBag['title']) ? $pageObj->page->viewBag['title'] : $pageTitle; - $pageUrl = isset($pageObj->page->getViewBag()->property('localeUrl')[$locale]) ? $pageObj->page->getViewBag()->property('localeUrl')[$locale] : $pageUrl; - } - - ?> -
  • data-no-drag-mode - data-id="" - > -
    - Expand - - - - - - -
    - isItemSelected($fileName) ? 'checked' : null ?> - data-request="getEventHandler('onSelect') ?>" - value="1"> - -
    - - - - title="Dragging is disabled when the Search is active">Drag - -
    - -
      - subpages): ?> - makePartial('treebranch', ['items' => $subpages]) ?> - -
    -
  • - diff --git a/widgets/templatelist/partials/_body.htm b/widgets/templatelist/partials/_body.htm deleted file mode 100644 index a4273cb7..00000000 --- a/widgets/templatelist/partials/_body.htm +++ /dev/null @@ -1,8 +0,0 @@ -makePartial('toolbar') ?> -
    -
    -
    - makePartial('templates', ['data' => $data]) ?> -
    -
    -
    diff --git a/widgets/templatelist/partials/_items.htm b/widgets/templatelist/partials/_items.htm deleted file mode 100644 index d330ca32..00000000 --- a/widgets/templatelist/partials/_items.htm +++ /dev/null @@ -1,59 +0,0 @@ - - - -

    noRecordsMessage)) ?>

    - - - - - diff --git a/widgets/templatelist/partials/_sorting-options.htm b/widgets/templatelist/partials/_sorting-options.htm deleted file mode 100644 index 4aacc073..00000000 --- a/widgets/templatelist/partials/_sorting-options.htm +++ /dev/null @@ -1,7 +0,0 @@ -sortingProperties as $propertyName=>$propertyTitle): ?> -
  • getSortingProperty() == $propertyName): ?>class="active"> - - - -
  • - diff --git a/widgets/templatelist/partials/_templates.htm b/widgets/templatelist/partials/_templates.htm deleted file mode 100644 index e1f0b860..00000000 --- a/widgets/templatelist/partials/_templates.htm +++ /dev/null @@ -1,11 +0,0 @@ -
    -
    -
    - makePartial('items', ['items' => $data]) ?> -
    -
    -
    diff --git a/widgets/templatelist/partials/_toolbar.htm b/widgets/templatelist/partials/_toolbar.htm deleted file mode 100644 index c3ed60f8..00000000 --- a/widgets/templatelist/partials/_toolbar.htm +++ /dev/null @@ -1,51 +0,0 @@ -
    -
    - - -
    -
    - - - sortingProperties): ?> - - - - -
    -
    - - -
    - -
    - -
    -