Added Tabs to edit page
This commit is contained in:
parent
bba62180fe
commit
a14df54cd9
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "4lt/novaconium",
|
"name": "4lt/novaconium",
|
||||||
"description": "A high-performance PHP framework built from the past.",
|
"description": "A high-performance PHP framework built from the past.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"authors": [
|
"authors": [
|
||||||
@ -11,19 +11,18 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Novaconium\\\\": "src/"
|
"Novaconium\\": "src/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"twig/twig": "*",
|
"twig/twig": "*",
|
||||||
"nickyeoman/php-validation-class": "^5.0"
|
"nickyeoman/php-validation-class": "^5.0"
|
||||||
},
|
},
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"extra": {
|
"extra": {
|
||||||
"versioning": {
|
"versioning": {
|
||||||
"strategy": "semantic-versioning"
|
"strategy": "semantic-versioning"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
// Create an admin user (POST)
|
||||||
|
|
||||||
use Nickyeoman\Validation;
|
use Nickyeoman\Validation;
|
||||||
|
|
||||||
$validate = new Validation\Validate();
|
$validate = new Validation\Validate();
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
$data = array_merge($data, [
|
$data = array_merge($data, [
|
||||||
'title' => 'Novaconium Edit Page',
|
'title' => 'Novaconium Edit Page',
|
||||||
'pageclass' => 'novaconium'
|
'pageclass' => 'novaconium',
|
||||||
|
'editor' => 'ace'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Check if logged in
|
// Check if logged in
|
||||||
@ -18,25 +19,35 @@ $pageid = $router->parameters['id'] ?? null;
|
|||||||
if (!empty($pageid)) {
|
if (!empty($pageid)) {
|
||||||
// Existing page: fetch from database
|
// Existing page: fetch from database
|
||||||
$query = <<<EOSQL
|
$query = <<<EOSQL
|
||||||
SELECT
|
WITH all_tags AS (
|
||||||
id,
|
SELECT GROUP_CONCAT(DISTINCT name ORDER BY name SEPARATOR ',') AS tags_list
|
||||||
title,
|
FROM tags
|
||||||
heading,
|
)
|
||||||
description,
|
SELECT
|
||||||
keywords,
|
p.id,
|
||||||
author,
|
p.title,
|
||||||
slug,
|
p.heading,
|
||||||
path,
|
p.description,
|
||||||
intro,
|
p.keywords,
|
||||||
body,
|
p.author,
|
||||||
notes,
|
p.slug,
|
||||||
draft,
|
p.path,
|
||||||
changefreq,
|
p.intro,
|
||||||
priority,
|
p.body,
|
||||||
created,
|
p.notes,
|
||||||
updated
|
p.draft,
|
||||||
FROM pages
|
p.changefreq,
|
||||||
WHERE id = ?
|
p.priority,
|
||||||
|
p.created,
|
||||||
|
p.updated,
|
||||||
|
COALESCE(GROUP_CONCAT(DISTINCT t.name ORDER BY t.name SEPARATOR ','), '') AS page_tags,
|
||||||
|
at.tags_list AS existing_tags
|
||||||
|
FROM pages p
|
||||||
|
LEFT JOIN page_tags pt ON p.id = pt.page_id
|
||||||
|
LEFT JOIN tags t ON pt.tag_id = t.id
|
||||||
|
CROSS JOIN all_tags at -- Zero-cost join for scalar
|
||||||
|
WHERE p.id = ?
|
||||||
|
GROUP BY p.id;
|
||||||
EOSQL;
|
EOSQL;
|
||||||
|
|
||||||
$data['rows'] = $db->getRow($query, [$pageid]);
|
$data['rows'] = $db->getRow($query, [$pageid]);
|
||||||
@ -70,4 +81,4 @@ if (empty($pageid)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render the edit page view
|
// Render the edit page view
|
||||||
view('@novacore/editpage', $data);
|
view('@novacore/editpage/index', $data);
|
||||||
|
|||||||
@ -151,4 +151,51 @@ if ($row['total'] < 1) {
|
|||||||
makeitso();
|
makeitso();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check Tags Table
|
||||||
|
$query = <<<EOSQL
|
||||||
|
SELECT TABLE_NAME
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND TABLE_NAME = 'tags';
|
||||||
|
EOSQL;
|
||||||
|
$result = $db->query($query);
|
||||||
|
if ($result->num_rows === 0) {
|
||||||
|
$query = <<<EOSQL
|
||||||
|
CREATE TABLE IF NOT EXISTS `tags` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(100) NOT NULL UNIQUE,
|
||||||
|
`created` datetime NOT NULL,
|
||||||
|
`updated` datetime DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;
|
||||||
|
EOSQL;
|
||||||
|
$db->query($query);
|
||||||
|
$log->info('Tags Table Created');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Page Tags Junction Table (after tags table)
|
||||||
|
$query = <<<EOSQL
|
||||||
|
SELECT TABLE_NAME
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND TABLE_NAME = 'page_tags';
|
||||||
|
EOSQL;
|
||||||
|
$result = $db->query($query);
|
||||||
|
if ($result->num_rows === 0) {
|
||||||
|
$query = <<<EOSQL
|
||||||
|
CREATE TABLE IF NOT EXISTS `page_tags` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`page_id` int(11) NOT NULL,
|
||||||
|
`tag_id` int(11) NOT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `page_id` (`page_id`),
|
||||||
|
KEY `tag_id` (`tag_id`),
|
||||||
|
FOREIGN KEY (`page_id`) REFERENCES `pages` (`id`) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;
|
||||||
|
EOSQL;
|
||||||
|
$db->query($query);
|
||||||
|
$log->info('Page Tags Junction Table Created');
|
||||||
|
}
|
||||||
|
|
||||||
view('@novacore/init', $data);
|
view('@novacore/init', $data);
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use Nickyeoman\Validation;
|
use Nickyeoman\Validation;
|
||||||
$v = new Nickyeoman\Validation\Validate();
|
$v = new Nickyeoman\Validation\Validate();
|
||||||
|
use Novaconium\Services\TagManager; // Autoloads automatically
|
||||||
|
|
||||||
$url_error = '/novaconium/page/edit/' . $post->get('id'); // fallback for errors
|
$url_error = '/novaconium/page/edit/' . $post->get('id'); // fallback for errors
|
||||||
|
|
||||||
@ -34,6 +35,7 @@ $notes = $_POST['notes'] ?? '';
|
|||||||
$draft = !empty($post->get('draft')) ? 1 : 0;
|
$draft = !empty($post->get('draft')) ? 1 : 0;
|
||||||
$changefreq = $_POST['changefreq'] ?? 'monthly';
|
$changefreq = $_POST['changefreq'] ?? 'monthly';
|
||||||
$priority = $_POST['priority'] ?? 0.0;
|
$priority = $_POST['priority'] ?? 0.0;
|
||||||
|
$tags_json = $_POST['tags_json'] ?? '[]';
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (empty($title) || empty($slug) || empty($body)) {
|
if (empty($title) || empty($slug) || empty($body)) {
|
||||||
@ -64,6 +66,19 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($id) && !$newpage) {
|
if (!empty($id) && !$newpage) {
|
||||||
|
|
||||||
|
/** Work in Progress
|
||||||
|
// Delete old tag links for this page (cleanup)
|
||||||
|
$deleteQuery = <<<EOSQL
|
||||||
|
DELETE FROM page_tags WHERE page_id = ?
|
||||||
|
EOSQL;
|
||||||
|
$db->query($deleteQuery, [$id]);
|
||||||
|
|
||||||
|
$tagManager = new TagManager();
|
||||||
|
|
||||||
|
dd($tags_json);
|
||||||
|
**/
|
||||||
|
|
||||||
// Update existing page
|
// Update existing page
|
||||||
$query = "UPDATE `pages` SET
|
$query = "UPDATE `pages` SET
|
||||||
`title` = ?, `heading` = ?, `description` = ?, `keywords` = ?, `author` = ?,
|
`title` = ?, `heading` = ?, `description` = ?, `keywords` = ?, `author` = ?,
|
||||||
|
|||||||
29
sass/framework/_ace.sass
Normal file
29
sass/framework/_ace.sass
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
@use '../abstracts' as *
|
||||||
|
@use 'sass:color' // For color.adjust()—non-deprecated color tweaks
|
||||||
|
@use '../base' as *
|
||||||
|
|
||||||
|
// ACE Editor
|
||||||
|
.editor-container
|
||||||
|
width: 100%
|
||||||
|
min-height: 400px // ~10 rows
|
||||||
|
|
||||||
|
.ace-editor
|
||||||
|
height: 400px // Scrollable
|
||||||
|
border: 1px solid $accent-light
|
||||||
|
font-family: $mono-font // Your monospace
|
||||||
|
|
||||||
|
// Ace internals (dark theme tweaks)
|
||||||
|
.ace_gutter
|
||||||
|
background: $bg-darker !important
|
||||||
|
color: $text-muted !important
|
||||||
|
border-right: 1px solid $accent-light !important
|
||||||
|
|
||||||
|
.ace_scroller
|
||||||
|
background: $bg-dark !important
|
||||||
|
|
||||||
|
.ace_text-layer .ace_print-margin
|
||||||
|
background: transparent !important
|
||||||
|
|
||||||
|
// Selection
|
||||||
|
.ace_marker-layer .ace_selection
|
||||||
|
background: color.adjust($accent-light, $alpha: -0.3) !important // Green tint
|
||||||
15
sass/framework/_edit_page.sass
Normal file
15
sass/framework/_edit_page.sass
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
@use '../abstracts' as *
|
||||||
|
@use '../base' as *
|
||||||
|
@use 'sass:color' // Already there for adjusts
|
||||||
|
|
||||||
|
#edit-page-title
|
||||||
|
margin-top: 2rem
|
||||||
|
|
||||||
|
#title
|
||||||
|
width: 100%
|
||||||
|
font-size: 1.9rem
|
||||||
|
padding: 20px
|
||||||
|
|
||||||
|
&>div
|
||||||
|
font-size: 0.8rem
|
||||||
|
margin: 8px 0 0 20px
|
||||||
48
sass/framework/_tabs.sass
Normal file
48
sass/framework/_tabs.sass
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// framework/_tabs.sass
|
||||||
|
|
||||||
|
@use '../abstracts' as *
|
||||||
|
@use 'sass:color' // For color.adjust()—non-deprecated color tweaks
|
||||||
|
|
||||||
|
// Simplified tab styling: Square borders, no rounds
|
||||||
|
.tab-container
|
||||||
|
margin: space('lg') auto
|
||||||
|
background: $bg-dark
|
||||||
|
|
||||||
|
.tab-nav
|
||||||
|
display: flex
|
||||||
|
background-color: $bg-darker
|
||||||
|
border-bottom: 1px solid $border-light
|
||||||
|
|
||||||
|
.tab-button
|
||||||
|
padding: space('sm') space('md')
|
||||||
|
background: $bg-darker
|
||||||
|
border: 1px solid $border-light
|
||||||
|
border-bottom: none
|
||||||
|
cursor: pointer
|
||||||
|
font-size: 14px
|
||||||
|
min-width: 120px // Makes them more square/equal
|
||||||
|
text-align: center
|
||||||
|
color: $text-light
|
||||||
|
font-family: $mono-font // From vars
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background-color: color.adjust($bg-darker, $lightness: 5%) // Modern, non-deprecated lighten equivalent
|
||||||
|
|
||||||
|
+ .tab-button
|
||||||
|
border-left: none // Shared borders
|
||||||
|
|
||||||
|
&.active
|
||||||
|
background-color: $bg-dark
|
||||||
|
border-bottom: 2px solid $accent-light
|
||||||
|
color: $accent-light
|
||||||
|
font-weight: bold
|
||||||
|
|
||||||
|
.tab-content
|
||||||
|
display: none
|
||||||
|
padding: space('lg')
|
||||||
|
background-color: $bg-dark
|
||||||
|
color: $text-light
|
||||||
|
border: none
|
||||||
|
|
||||||
|
&.active
|
||||||
|
display: block
|
||||||
108
sass/framework/_tags.sass
Normal file
108
sass/framework/_tags.sass
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
@use '../abstracts' as *
|
||||||
|
@use 'sass:color' // For color.adjust()—non-deprecated color tweaks
|
||||||
|
@use '../base' as *
|
||||||
|
|
||||||
|
// Tags Dropdown
|
||||||
|
.tags-dropdown
|
||||||
|
position: absolute
|
||||||
|
top: 100%
|
||||||
|
left: 0
|
||||||
|
right: 0
|
||||||
|
max-height: 200px
|
||||||
|
overflow-y: auto
|
||||||
|
background: $bg-darker
|
||||||
|
border: 1px solid $border-light
|
||||||
|
border-top: none
|
||||||
|
border-radius: 0 0 4px 4px
|
||||||
|
list-style: none
|
||||||
|
margin: 0
|
||||||
|
padding: 0
|
||||||
|
z-index: 10
|
||||||
|
opacity: 0
|
||||||
|
visibility: hidden
|
||||||
|
transition: opacity 0.2s
|
||||||
|
|
||||||
|
&[aria-expanded="true"]
|
||||||
|
opacity: 1
|
||||||
|
visibility: visible
|
||||||
|
|
||||||
|
li
|
||||||
|
padding: space('xs') space('sm')
|
||||||
|
cursor: pointer
|
||||||
|
color: $text-light
|
||||||
|
font-size: 14px
|
||||||
|
border-bottom: 1px solid transparent
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.selected
|
||||||
|
background: color.adjust($accent-light, $alpha: -0.1)
|
||||||
|
color: $accent-light
|
||||||
|
|
||||||
|
&:last-child
|
||||||
|
border-bottom: none
|
||||||
|
|
||||||
|
.tags-input
|
||||||
|
position: relative // For absolute dropdown
|
||||||
|
|
||||||
|
// Tags Input
|
||||||
|
.tags-input
|
||||||
|
display: flex
|
||||||
|
flex-wrap: wrap
|
||||||
|
gap: space('xs')
|
||||||
|
align-items: center
|
||||||
|
border: 1px solid $border-light
|
||||||
|
padding: space('xs')
|
||||||
|
background: $bg-darker
|
||||||
|
min-height: 40px
|
||||||
|
grid-column: 2 // Spans input area in .form-row grid
|
||||||
|
|
||||||
|
input
|
||||||
|
border: none
|
||||||
|
background: transparent
|
||||||
|
color: $text-light
|
||||||
|
font-family: $mono-font
|
||||||
|
font-size: 14px
|
||||||
|
flex: 1
|
||||||
|
min-width: 100px
|
||||||
|
outline: none
|
||||||
|
|
||||||
|
&::placeholder
|
||||||
|
color: $text-muted
|
||||||
|
|
||||||
|
.tag-chip
|
||||||
|
display: inline-flex
|
||||||
|
align-items: center
|
||||||
|
background: color.adjust($accent-light, $alpha: -0.2)
|
||||||
|
color: $accent-light
|
||||||
|
padding: space('xs') space('sm')
|
||||||
|
border-radius: 4px
|
||||||
|
font-size: 12px
|
||||||
|
max-width: 150px
|
||||||
|
overflow: hidden
|
||||||
|
text-overflow: ellipsis
|
||||||
|
white-space: nowrap
|
||||||
|
|
||||||
|
.tag-remove
|
||||||
|
background: none
|
||||||
|
border: none
|
||||||
|
color: inherit
|
||||||
|
font-size: 16px
|
||||||
|
margin-left: space('xs')
|
||||||
|
cursor: pointer
|
||||||
|
padding: 0
|
||||||
|
line-height: 1
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
opacity: 0.7
|
||||||
|
|
||||||
|
// Tags Autocomplete (native datalist styling)
|
||||||
|
.tags-input input[list]
|
||||||
|
// Existing input styles apply...
|
||||||
|
|
||||||
|
#tag-suggestions
|
||||||
|
// Native datalist not directly stylable, but options can be influenced via input focus
|
||||||
|
// For custom polyfill, add JS-generated <ul>, but native is fine for now
|
||||||
|
|
||||||
|
// Browser-specific tweaks (Chrome/Firefox)
|
||||||
|
.tags-input input[list]::-webkit-calendar-picker-indicator
|
||||||
|
display: none // Hide arrow if interfering
|
||||||
47
sass/framework/_tooltip.sass
Normal file
47
sass/framework/_tooltip.sass
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
@use '../abstracts' as *
|
||||||
|
@use '../base' as *
|
||||||
|
@use 'sass:color' // Already there for adjusts
|
||||||
|
|
||||||
|
// Tooltip styling (mouse-follow version)
|
||||||
|
.tooltip
|
||||||
|
position: relative
|
||||||
|
display: inline-block
|
||||||
|
cursor: help
|
||||||
|
font-size: 16px
|
||||||
|
color: $accent-light // Green accent for ?
|
||||||
|
margin-left: space('xs') // 0.25rem
|
||||||
|
top: 2px
|
||||||
|
|
||||||
|
.tooltiptext
|
||||||
|
visibility: hidden
|
||||||
|
width: 200px
|
||||||
|
background-color: $bg-darker // Darker bg
|
||||||
|
color: $text-light // White text
|
||||||
|
text-align: left // Changed to left-align for better readability
|
||||||
|
border-radius: 0 // Square
|
||||||
|
padding: space('sm') // 0.5rem
|
||||||
|
position: fixed // Fixed to viewport, updated by JS
|
||||||
|
z-index: z('dropdown') // High z-index
|
||||||
|
opacity: 0
|
||||||
|
transition: opacity 0.3s
|
||||||
|
font-size: 14px
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2) // Subtle shadow
|
||||||
|
pointer-events: none // Prevents mouse interference with tooltip
|
||||||
|
|
||||||
|
&::after
|
||||||
|
content: ""
|
||||||
|
position: absolute
|
||||||
|
top: -5px // Arrow above tooltip (points up to mouse)
|
||||||
|
left: 10px // Slight indent from left edge
|
||||||
|
border-width: 5px
|
||||||
|
border-style: solid
|
||||||
|
border-color: $bg-darker transparent transparent transparent // Upward arrow
|
||||||
|
|
||||||
|
&:hover .tooltiptext
|
||||||
|
visibility: visible
|
||||||
|
opacity: 1
|
||||||
|
|
||||||
|
// Accessibility: Hidden tooltip text for screen readers
|
||||||
|
.tooltip-desc
|
||||||
|
position: absolute
|
||||||
|
left: -9999px
|
||||||
@ -2,4 +2,9 @@
|
|||||||
@forward 'ui';
|
@forward 'ui';
|
||||||
@forward 'forms';
|
@forward 'forms';
|
||||||
@forward 'login_form';
|
@forward 'login_form';
|
||||||
@forward 'logo';
|
@forward 'logo';
|
||||||
|
@forward 'tabs';
|
||||||
|
@forward 'edit_page';
|
||||||
|
@forward 'tooltip';
|
||||||
|
@forward 'ace';
|
||||||
|
@forward 'tags';
|
||||||
File diff suppressed because one or more lines are too long
@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
class Logger {
|
class Logger {
|
||||||
protected string $logFile;
|
protected string $logFile;
|
||||||
protected int $logLevelThreshold;
|
protected int $logLevelThreshold;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use
|
* Use
|
||||||
* $redirect->url('/login');
|
* $redirect->url('/login');
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
namespace Novaconium\Services;
|
||||||
class Auth
|
class Auth
|
||||||
{
|
{
|
||||||
|
|
||||||
14
src/Services/TagManager.php
Normal file
14
src/Services/TagManager.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
namespace Novaconium\Services;
|
||||||
|
/**
|
||||||
|
* TagManager Class
|
||||||
|
* Handles tag preparation, insertion, and linking for pages.
|
||||||
|
* Cleans up controller by encapsulating tag logic.
|
||||||
|
*/
|
||||||
|
class TagManager
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
echo "class access";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
define('FRAMEWORKPATH', BASEPATH . '/vendor/4lt/novaconium');
|
|
||||||
|
|
||||||
require_once(BASEPATH . '/vendor/autoload.php');
|
require_once(BASEPATH . '/vendor/autoload.php');
|
||||||
|
define('FRAMEWORKPATH', BASEPATH . '/vendor/4lt/novaconium');
|
||||||
|
|
||||||
//Check if config file exists
|
//Check if config file exists
|
||||||
if (file_exists(BASEPATH . '/App/config.php')) {
|
if (file_exists(BASEPATH . '/App/config.php')) {
|
||||||
@ -48,7 +47,7 @@ if (!empty($config['database']['host'])) {
|
|||||||
// Sanatize POST Data
|
// Sanatize POST Data
|
||||||
if (!empty($_POST)) {
|
if (!empty($_POST)) {
|
||||||
require_once(FRAMEWORKPATH . '/src/Post.php');
|
require_once(FRAMEWORKPATH . '/src/Post.php');
|
||||||
$post = new POST($_POST);
|
$post = new Post($_POST);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a Redirect
|
// Start a Redirect
|
||||||
|
|||||||
@ -3,4 +3,9 @@
|
|||||||
right before the /body
|
right before the /body
|
||||||
like javascript
|
like javascript
|
||||||
or analytics
|
or analytics
|
||||||
-->
|
-->
|
||||||
|
{% include '@novaconium/javascript/page-edit.html.twig' %}
|
||||||
|
|
||||||
|
{% if editor == 'ace' %}
|
||||||
|
{% include '@novaconium/javascript/ace.html.twig' %}
|
||||||
|
{% endif %}
|
||||||
|
|||||||
@ -37,4 +37,14 @@
|
|||||||
<link href="{{ fonts | default('https://fonts.googleapis.com/css2?family=VT323:wght@400&family=Fira+Code:wght@400;500&display=swap&family=Material+Icons:wght@400;500&display=swap') }}" rel="stylesheet">
|
<link href="{{ fonts | default('https://fonts.googleapis.com/css2?family=VT323:wght@400&family=Fira+Code:wght@400;500&display=swap&family=Material+Icons:wght@400;500&display=swap') }}" rel="stylesheet">
|
||||||
|
|
||||||
{# STYLESHEET #}
|
{# STYLESHEET #}
|
||||||
<link rel="stylesheet" href="/css/novaconium.css">
|
<link rel="stylesheet" href="/css/novaconium.css">
|
||||||
|
|
||||||
|
{% if editor == 'ace' %}
|
||||||
|
<!-- ACE Editor -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ace.min.css">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ace.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/mode-html.min.js"></script> {# HTML syntax #}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/theme-tomorrow_night.min.js"></script> {# Dark theme #}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ext-language_tools.min.js"></script> {# Autocomplete #}
|
||||||
|
<!-- END ACE Editor -->
|
||||||
|
{% endif %}
|
||||||
40
twig/javascript/ace.html.twig
Normal file
40
twig/javascript/ace.html.twig
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<!-- Ace Editor -->
|
||||||
|
<script>
|
||||||
|
// Ace Editor Init (cleaned: removed duplicate setUseWorker, added null checks)
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const bodyTextarea = document.getElementById('body');
|
||||||
|
const bodyEditorEl = document.getElementById('body-editor');
|
||||||
|
|
||||||
|
if (bodyTextarea && bodyEditorEl && typeof ace !== 'undefined') { // Check ace loaded
|
||||||
|
try {
|
||||||
|
const editor = ace.edit(bodyEditorEl);
|
||||||
|
editor.session.setValue(bodyTextarea.value || ''); // Load initial HTML
|
||||||
|
editor.session.setMode('ace/mode/html'); // HTML syntax highlighting
|
||||||
|
editor.setTheme('ace/theme/tomorrow_night'); // Dark theme (black bg, green accents)
|
||||||
|
editor.setOptions({
|
||||||
|
fontSize: 14,
|
||||||
|
showPrintMargin: false,
|
||||||
|
wrap: true, // Line wrapping
|
||||||
|
useWorker: false // Disable worker for linting if not needed (faster)
|
||||||
|
});
|
||||||
|
editor.session.setUseWorker(false); // No JS linting in HTML mode
|
||||||
|
|
||||||
|
// Enable basic autocomplete
|
||||||
|
editor.setOptions({ enableBasicAutocompletion: true });
|
||||||
|
|
||||||
|
// Sync back to textarea on change
|
||||||
|
editor.session.on('change', function() {
|
||||||
|
bodyTextarea.value = editor.getValue(); // Full HTML string
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Ace loaded! Initial value:', editor.getValue().substring(0, 50) + '...'); // Debug
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ace init failed:', error); // Graceful error
|
||||||
|
bodyEditorEl.style.display = 'none'; // Hide div, show plain textarea if needed
|
||||||
|
bodyTextarea.style.display = 'block';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('Ace elements or lib missing'); // Fallback to plain textarea
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
192
twig/javascript/page-edit.html.twig
Normal file
192
twig/javascript/page-edit.html.twig
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
<script>
|
||||||
|
// Tab switching JS (unchanged)
|
||||||
|
function switchTab(tabId, button) {
|
||||||
|
const contents = document.querySelectorAll('.tab-content');
|
||||||
|
contents.forEach(content => content.classList.remove('active'));
|
||||||
|
|
||||||
|
const buttons = document.querySelectorAll('.tab-button');
|
||||||
|
buttons.forEach(b => b.classList.remove('active'));
|
||||||
|
|
||||||
|
document.getElementById(tabId).classList.add('active');
|
||||||
|
button.classList.add('active');
|
||||||
|
|
||||||
|
if (tabId === 'content6') {
|
||||||
|
setTimeout(initTags, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags Init (with custom dropdown autocomplete)
|
||||||
|
let tagsListeners = [];
|
||||||
|
function initTags() {
|
||||||
|
const tagsInput = document.getElementById('tags-input');
|
||||||
|
const tagsField = document.getElementById('tags');
|
||||||
|
const hiddenTags = document.getElementById('tags_json');
|
||||||
|
const dropdown = document.getElementById('tags-dropdown');
|
||||||
|
|
||||||
|
if (!tagsInput || !tagsField || !hiddenTags || !dropdown) {
|
||||||
|
console.warn('Tags elements missing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean old listeners
|
||||||
|
tagsListeners.forEach(ls => ls());
|
||||||
|
tagsListeners = [];
|
||||||
|
|
||||||
|
let tags = [];
|
||||||
|
let existingTags = [];
|
||||||
|
try {
|
||||||
|
tags = JSON.parse(tagsInput.dataset.tags || '[]');
|
||||||
|
existingTags = JSON.parse(tagsInput.dataset.existingTags || '[]');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('JSON error:', e);
|
||||||
|
tags = [];
|
||||||
|
existingTags = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedIndex = -1; // For keyboard nav
|
||||||
|
|
||||||
|
// Render chips (unchanged)
|
||||||
|
function renderTags() {
|
||||||
|
tagsInput.innerHTML = '';
|
||||||
|
tags.forEach((tag, index) => {
|
||||||
|
const chip = document.createElement('span');
|
||||||
|
chip.className = 'tag-chip';
|
||||||
|
chip.innerHTML = `${tag} <button type="button" class="tag-remove">×</button>`;
|
||||||
|
chip.querySelector('.tag-remove').onclick = () => {
|
||||||
|
tags.splice(index, 1);
|
||||||
|
renderTags();
|
||||||
|
hiddenTags.value = JSON.stringify(tags);
|
||||||
|
};
|
||||||
|
tagsInput.appendChild(chip);
|
||||||
|
});
|
||||||
|
tagsInput.appendChild(tagsField);
|
||||||
|
tagsInput.appendChild(dropdown); // Re-add dropdown
|
||||||
|
hiddenTags.value = JSON.stringify(tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter and render dropdown
|
||||||
|
function updateDropdown() {
|
||||||
|
const value = tagsField.value.toLowerCase().trim();
|
||||||
|
dropdown.innerHTML = '';
|
||||||
|
dropdown.setAttribute('aria-expanded', value ? 'true' : 'false');
|
||||||
|
selectedIndex = -1;
|
||||||
|
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
const matches = existingTags.filter(tag =>
|
||||||
|
tag.toLowerCase().startsWith(value) && !tags.includes(tag.toLowerCase())
|
||||||
|
).slice(0, 10); // Top 10 matches, exclude existing
|
||||||
|
|
||||||
|
matches.forEach((tag, index) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = tag;
|
||||||
|
li.setAttribute('role', 'option');
|
||||||
|
li.onclick = () => selectTag(tag);
|
||||||
|
li.onmouseover = () => { selectedIndex = index; updateHighlight(); };
|
||||||
|
dropdown.appendChild(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matches.length) dropdown.parentElement.classList.add('has-dropdown');
|
||||||
|
else dropdown.parentElement.classList.remove('has-dropdown');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight selected in dropdown
|
||||||
|
function updateHighlight() {
|
||||||
|
dropdown.querySelectorAll('li').forEach((li, index) => {
|
||||||
|
li.classList.toggle('selected', index === selectedIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select tag from dropdown
|
||||||
|
function selectTag(tag) {
|
||||||
|
addTag(tag, true); // Add as chip, refocus
|
||||||
|
tagsField.value = ''; // Clear input
|
||||||
|
dropdown.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tag (unchanged)
|
||||||
|
function addTag(inputValue, refocus = false) {
|
||||||
|
const tag = inputValue.trim().toLowerCase().replace(/[^\w-]/g, '');
|
||||||
|
if (tag && tag.length > 0 && tag.length <= 50 && !tags.includes(tag)) {
|
||||||
|
tags.push(tag);
|
||||||
|
renderTags();
|
||||||
|
if (refocus) tagsField.focus();
|
||||||
|
}
|
||||||
|
tagsField.value = '';
|
||||||
|
updateDropdown(); // Hide dropdown after add
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events: Enter/comma/TAB adds tag (no form submit)
|
||||||
|
const keydownListener = (e) => {
|
||||||
|
console.log('Keydown:', e.key, 'Value:', tagsField.value, 'Selected:', selectedIndex);
|
||||||
|
if (dropdown.getAttribute('aria-expanded') === 'true') {
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
selectedIndex = Math.min(selectedIndex + 1, dropdown.querySelectorAll('li').length - 1);
|
||||||
|
updateHighlight();
|
||||||
|
dropdown.querySelector(`li:nth-child(${selectedIndex + 1})`).scrollIntoView({ block: 'nearest' });
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
selectedIndex = Math.max(selectedIndex - 1, 0);
|
||||||
|
updateHighlight();
|
||||||
|
dropdown.querySelector(`li:nth-child(${selectedIndex + 1})`).scrollIntoView({ block: 'nearest' });
|
||||||
|
} else if ((e.key === 'Enter' || e.key === 'Tab') && selectedIndex >= 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
const selected = dropdown.querySelector(`li:nth-child(${selectedIndex + 1})`).textContent;
|
||||||
|
selectTag(selected); // Adds chip, refocuses
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
tagsField.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for no dropdown or non-selected Tab/Enter
|
||||||
|
if (!dropdown.getAttribute('aria-expanded') === 'true' || selectedIndex < 0) {
|
||||||
|
if (e.key === 'Enter' || e.key === ',') {
|
||||||
|
e.preventDefault();
|
||||||
|
addTag(tagsField.value, true);
|
||||||
|
} else if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (tagsField.value.trim()) addTag(tagsField.value, true); // Add typed value, refocus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tagsField.addEventListener('keydown', keydownListener);
|
||||||
|
tagsListeners.push(() => tagsField.removeEventListener('keydown', keydownListener));
|
||||||
|
|
||||||
|
const inputListener = () => updateDropdown();
|
||||||
|
tagsField.addEventListener('input', inputListener);
|
||||||
|
tagsListeners.push(() => tagsField.removeEventListener('input', inputListener));
|
||||||
|
|
||||||
|
const blurListener = () => {
|
||||||
|
setTimeout(() => dropdown.setAttribute('aria-expanded', 'false'), 150); // Hide after blur
|
||||||
|
if (tagsField.value.trim()) addTag(tagsField.value);
|
||||||
|
};
|
||||||
|
tagsField.addEventListener('blur', blurListener);
|
||||||
|
tagsListeners.push(() => tagsField.removeEventListener('blur', blurListener));
|
||||||
|
|
||||||
|
// Initial render
|
||||||
|
renderTags();
|
||||||
|
console.log('Tags ready with autocomplete, loaded:', tags.length, 'tags');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init on load + observe (unchanged)
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const tagsTab = document.getElementById('content6');
|
||||||
|
if (tagsTab && tagsTab.classList.contains('active')) {
|
||||||
|
initTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
||||||
|
const target = mutation.target;
|
||||||
|
if (target.id === 'content6' && target.classList.contains('active')) {
|
||||||
|
initTags();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
observer.observe(document.body, { subtree: true, attributes: true });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -1,92 +0,0 @@
|
|||||||
{% extends '@novaconium/master.html.twig' %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h2>Edit Page - {{ title }}</h2>
|
|
||||||
|
|
||||||
<form method="post" action="/novaconium/savePage" id="edit-page-form-novaconium">
|
|
||||||
|
|
||||||
<input type="hidden" name="id" value="{{ rows.id }}">
|
|
||||||
<input type="hidden" name="token" value="{{ token }}">
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="title">Title:</label>
|
|
||||||
<input type="text" id="title" name="title" value="{{ rows.title }}" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="slug">
|
|
||||||
Slug: (<a href="/page/{{ rows.slug }}" target="_new">/page/{{ rows.slug }}</a>)
|
|
||||||
</label>
|
|
||||||
<input type="text" id="slug" name="slug" value="{{ rows.slug }}" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group fullwidth">
|
|
||||||
<label for="body">Body:</label>
|
|
||||||
<textarea id="body" name="body" rows="10">{{ rows.body }}</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Metadata</h2>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="description">Description:</label>
|
|
||||||
<input type="text" id="description" name="description" value="{{ rows.description }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="keywords">Keywords:</label>
|
|
||||||
<input type="text" id="keywords" name="keywords" value="{{ rows.keywords }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="author">Author:</label>
|
|
||||||
<input type="text" id="author" name="author" value="{{ rows.author }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>CMS Info</h2>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="path">Path:</label>
|
|
||||||
<input type="text" id="path" name="path" value="{{ rows.path }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="changefreq">Change Frequency:</label>
|
|
||||||
<select id="changefreq" name="changefreq">
|
|
||||||
{% set freqs = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'] %}
|
|
||||||
{% for freq in freqs %}
|
|
||||||
<option value="{{ freq }}" {% if rows.changefreq == freq %}selected{% endif %}>
|
|
||||||
{{ freq|capitalize }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="priority">Priority (0.0 - 1.0):</label>
|
|
||||||
<input type="number" id="priority" name="priority" value="{{ rows.priority }}" step="0.1" min="0" max="1">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group fullwidth">
|
|
||||||
<label for="notes">Notes:</label>
|
|
||||||
<textarea id="notes" name="notes" rows="5">{{ rows.notes }}</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Page Data</h2>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="heading">Heading:</label>
|
|
||||||
<input type="text" id="heading" name="heading" value="{{ rows.heading }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group fullwidth">
|
|
||||||
<label for="intro">Intro:</label>
|
|
||||||
<textarea id="intro" name="intro" rows="5">{{ rows.intro }}</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p><strong>Created:</strong> {{ rows.created|date("Y-m-d H:i:s") }}</p>
|
|
||||||
<p><strong>Last Updated:</strong> {{ rows.updated|date("Y-m-d H:i:s") }}</p>
|
|
||||||
|
|
||||||
<button type="submit">Save Changes</button>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
62
views/editpage/index.html.twig
Normal file
62
views/editpage/index.html.twig
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{% extends '@novaconium/master.html.twig' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Edit Page - {{ title }}</h2>
|
||||||
|
|
||||||
|
<form method="post" action="/novaconium/savePage" id="edit-page-form-novaconium">
|
||||||
|
|
||||||
|
<input type="hidden" name="id" value="{{ rows.id }}">
|
||||||
|
<input type="hidden" name="token" value="{{ token }}">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="edit-page-title" class="form-group">
|
||||||
|
<label for="title">
|
||||||
|
Title
|
||||||
|
<span class="tooltip">?<span class="tooltiptext">This is the title for the cms, it's the default in twig, heading and metadata can be overwritten.</span></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input type="text" id="title" name="title" value="{{ rows.title }}" required>
|
||||||
|
<div id="edit-page-dates">
|
||||||
|
<strong>Last Updated:</strong> {{ rows.updated|date("Y-m-d H:i:s") }},
|
||||||
|
<strong>Created:</strong> {{ rows.created|date("Y-m-d H:i:s") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-container">
|
||||||
|
|
||||||
|
<!-- Tab Navigation -->
|
||||||
|
<nav class="tab-nav">
|
||||||
|
<button type="button" class="tab-button active" onclick="switchTab('content1')">Basic Info</button>
|
||||||
|
<button type="button" class="tab-button" onclick="switchTab('content2')">SEO & Meta</button>
|
||||||
|
<button type="button" class="tab-button" onclick="switchTab('content3')">Sitemap</button>
|
||||||
|
<button type="button" class="tab-button" onclick="switchTab('content4')">Page Tweaks</button>
|
||||||
|
<button type="button" class="tab-button" onclick="switchTab('content5')">Page Notes</button>
|
||||||
|
<button type="button" class="tab-button" onclick="switchTab('content6', this)">Tags</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="content1" class="tab-content active">
|
||||||
|
{% include '@novacore/editpage/tab-main.html.twig' %}
|
||||||
|
</div>
|
||||||
|
<div id="content2" class="tab-content">
|
||||||
|
{% include '@novacore/editpage/tab-metadata.html.twig' %}
|
||||||
|
</div>
|
||||||
|
<div id="content3" class="tab-content">
|
||||||
|
{% include '@novacore/editpage/tab-other.html.twig' %}
|
||||||
|
</div>
|
||||||
|
<div id="content4" class="tab-content">
|
||||||
|
{% include '@novacore/editpage/tab-tweaks.html.twig' %}
|
||||||
|
</div>
|
||||||
|
<div id="content5" class="tab-content">
|
||||||
|
{% include '@novacore/editpage/tab-notes.html.twig' %}
|
||||||
|
</div>
|
||||||
|
<div id="content6" class="tab-content">
|
||||||
|
{% include '@novacore/editpage/tab-tags.html.twig' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
16
views/editpage/tab-main.html.twig
Normal file
16
views/editpage/tab-main.html.twig
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<div class="form-group">
|
||||||
|
<label for="slug">
|
||||||
|
Slug: (<a href="/page/{{ rows.slug }}" target="_new">/page/{{ rows.slug }}</a>)
|
||||||
|
<span class="tooltip">?<span class="tooltiptext">Slug is a human readable but uri friendly name for the page.</span></span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="slug" name="slug" value="{{ rows.slug }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ace Editor -->
|
||||||
|
<div class="form-group fullwidth">
|
||||||
|
<label for="body">Body:</label>
|
||||||
|
<div class="editor-container">
|
||||||
|
<textarea id="body" name="body" rows="10" style="display: none;">{{ rows.body|default('')|e('html') }}</textarea>
|
||||||
|
<div id="body-editor" class="ace-editor"></div> {# Ace mounts here #}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
17
views/editpage/tab-metadata.html.twig
Normal file
17
views/editpage/tab-metadata.html.twig
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
<h2>Metadata</h2>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Description:</label>
|
||||||
|
<input type="text" id="description" name="description" value="{{ rows.description }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="keywords">Keywords:</label>
|
||||||
|
<input type="text" id="keywords" name="keywords" value="{{ rows.keywords }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="author">Author:</label>
|
||||||
|
<input type="text" id="author" name="author" value="{{ rows.author }}">
|
||||||
|
</div>
|
||||||
4
views/editpage/tab-notes.html.twig
Normal file
4
views/editpage/tab-notes.html.twig
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<div class="form-group fullwidth">
|
||||||
|
<label for="notes">Notes:</label>
|
||||||
|
<textarea id="notes" name="notes" rows="5">{{ rows.notes }}</textarea>
|
||||||
|
</div>
|
||||||
25
views/editpage/tab-other.html.twig
Normal file
25
views/editpage/tab-other.html.twig
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<h2>Sitemap</h2>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="path">Path:</label>
|
||||||
|
<input type="text" id="path" name="path" value="{{ rows.path }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="changefreq">Change Frequency:</label>
|
||||||
|
<select id="changefreq" name="changefreq">
|
||||||
|
{% set freqs = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'] %}
|
||||||
|
{% for freq in freqs %}
|
||||||
|
<option value="{{ freq }}" {% if rows.changefreq == freq %}selected{% endif %}>
|
||||||
|
{{ freq|capitalize }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="priority">Priority (0.0 - 1.0):</label>
|
||||||
|
<input type="number" id="priority" name="priority" value="{{ rows.priority }}" step="0.1" min="0" max="1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
12
views/editpage/tab-tags.html.twig
Normal file
12
views/editpage/tab-tags.html.twig
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<h1>Tags</h1>
|
||||||
|
<label for="tags">
|
||||||
|
Tags:
|
||||||
|
<span class="tooltip">?<span class="tooltiptext">Comma-separated keywords for categorizing this page (e.g., blog, tutorial). Click chips to remove; type to see suggestions.</span><span class="tooltip-desc" id="desc-tags">Comma-separated keywords for categorizing this page (e.g., blog, tutorial). Click chips to remove; type to see suggestions.</span></span>
|
||||||
|
</label>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="tags-input" id="tags-input" data-tags="{{ rows.page_tags|default('')|split(',')|filter('trim')|default([])|json_encode|e('html_attr') }}" data-existing-tags="{{ rows.existing_tags|default('')|split(',')|filter('trim')|default([])|json_encode|e('html_attr') }}">
|
||||||
|
<input type="text" id="tags" name="tags" placeholder="e.g., seo, cms, php" aria-describedby="desc-tags">
|
||||||
|
<ul id="tags-dropdown" class="tags-dropdown" role="listbox" aria-expanded="false"></ul> {# Custom dropdown #}
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="tags_json" name="tags_json" value="{{ rows.page_tags|default('')|split(',')|filter('trim')|default([])|json_encode|e('html_attr') }}">
|
||||||
|
</div>
|
||||||
10
views/editpage/tab-tweaks.html.twig
Normal file
10
views/editpage/tab-tweaks.html.twig
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<h2>Page Data</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="heading">Heading:</label>
|
||||||
|
<input type="text" id="heading" name="heading" value="{{ rows.heading }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group fullwidth">
|
||||||
|
<label for="intro">Intro:</label>
|
||||||
|
<textarea id="intro" name="intro" rows="5">{{ rows.intro }}</textarea>
|
||||||
|
</div>
|
||||||
Loading…
x
Reference in New Issue
Block a user