Component

Module

Enhancement For Page Module

Add Icon & Cover Image
Set status (on/off) for External Link to open new tab.

Directory

File Folder Link
edit.php \\SYNAS\Allan\DOCUMENTATION\Component\Enhancement For Page Module\rckuc\wolf\app\views\page
PageController.php \\SYNAS\Allan\DOCUMENTATION\Component\Enhancement For Page Module\rckuc\wolf\app\controllers
style.css \\SYNAS\Allan\DOCUMENTATION\Component\Enhancement For Page Module\rckuc\wolf\admin\themes\black_and_white
backend.js \\SYNAS\Allan\DOCUMENTATION\Component\Enhancement For Page Module\rckuc\wolf\admin\javascripts
menu.css & default.css \\SYNAS\Allan\DOCUMENTATION\Component\Enhancement For Page Module\rckuc\public\themes\rckuc\css
rckuc.js \\SYNAS\Allan\DOCUMENTATION\Component\Enhancement For Page Module\rckuc\public\themes\rckuc\js

 

Step 1

Update: views -> page -> edit.php

<!-- Update this part --> <ul class="tabNavigation"> <li class="tab"><a href="#pagetitle"><?php echo __('Page Title'); ?></a></li> <li class="tab"><a href="#metadata"><?php echo __('Metadata'); ?></a></li> <li class="tab"><a href="#page-setting"><?php echo __('Page Setting'); ?></a></li> <li class="tab"><a href="#menu-setting"><?php echo __('Menu Setting'); ?></a></li> <?php Observer::notify('view_page_edit_tab_links', $page); ?> </ul> <!-- Change id 'settings' to 'page-setting' --> <div id="page-setting" class="page">...</div> <!-- Add New div 'menu-setting' --> <div id="menu-setting" class="page"> <div id="div-menu-setting" class="title display_flex" title="<?php echo __('Menu Setting'); ?>"> <div id="icon"> <h3><?php echo __('Icon'); ?></h3> <div id="meta-pages" class="pages"> <div class="drop-area" data-class="icon"> <input type="hidden" name="crop_icon" id="hidden_icon"> <input type="file" class="imgFile" name="icon" accept=".png, .jpg, .jpeg, .svg, .ico, .webp" data-max-size="500" data-min-width="80" data-min-height="80" hidden/> <img class="ori_img" src="<?php echo !empty($page) && $page->icon ? (URL_PUBLIC . 'public/page/icon/' . $page->id . '/' . rawurlencode($page->icon)) : 'none'; ?>"></img> <div class="previewContainer"> <button class="previewImg" type="button" data-url="<?php echo !empty($page) && $page->icon ? (URL_PUBLIC . 'public/page/icon/' . $page->id . '/' . rawurlencode($page->icon)) : 'none'; ?>" style="<?php echo !empty($page) && $page->icon ? 'background-image: url(' . (URL_PUBLIC . 'public/page/icon/' . $page->id . '/' . rawurlencode($page->icon)) . ');' : 'none'; ?>"> </button> <span class="plusIcon">+</span> <div class="crop_icon"> <img src="<?php echo URL_PUBLIC ?>wolf/admin/images/crop.png" alt="crop" /> </div> </div> <p>Drag & Drop or <span class="clickUpload">Browse</span></p> </div> <span class="note">(Maximum Size: 500kb)</span> <span class="note">(Minimun: 80px x 80px)</span> <span class="note">(Allowed file type: .png, .jpg, .jpeg, .svg, .ico, .webp)</span> </div> <br> <?php $icon_status = (!empty($postdata['icon_status']) ? $postdata['icon_status'] : (!empty($page->icon_status) ? $page->icon_status : '0')); $icon_display = $postdata['icon_display'] ?? ($page->icon_display ?? ''); ?> <h3><?php echo __('Status'); ?></h3> <div id="meta-pages" class="switch_container"> <label class="switch"> <input type="checkbox" class="status_switch" name="page[icon_status]" <?php echo $icon_status == 1 ? 'checked' : ''; ?> value="1"> <span class="switch_slider round"></span> </label> <label class="switch_label"><?php echo $icon_status == 1 ? 'Show' : 'Hidden'; ?><label> </div> <br> <h3><?php echo __('Display'); ?></h3> <div id="meta-pages" class="pages"> <select class="icon_display" name="page[icon_display]"> <option></option> <option value="text" <?= $icon_display === 'text' ? 'selected' : '' ?>>Show Text Only</option> <option value="icon" <?= $icon_display === 'icon' ? 'selected' : '' ?>>Show Icon Only</option> <option value="both" <?= $icon_display === 'both' ? 'selected' : '' ?>>Show Text & Icon</option> </select> </div> </div> <?php if (isset($page) && $page->id != 1) : ?> <div id="cover"> <h3><?php echo __('Cover Image'); ?></h3> <div id="meta-pages" class="pages"> <div class="drop-area" data-class="cover_image"> <input type="hidden" name="crop_cover_image" id="hidden_cover_image"> <input type="file" class="imgFile" name="cover_image" accept=".png, .jpg, .jpeg, .webp" data-max-size="500" data-min-width="600" data-min-height="450" data-ratio="4:3" data-crop="false" hidden/> <img class="ori_img" src="<?php echo !empty($page) && $page->cover_image ? (URL_PUBLIC . 'public/page/cover_image/' . $page->id . '/' . rawurlencode($page->cover_image)) : 'none'; ?>"></img> <div class="previewContainer"> <button class="previewImg" type="button" data-url="<?php echo !empty($page) && $page->cover_image ? (URL_PUBLIC . 'public/page/cover_image/' . $page->id . '/' . rawurlencode($page->cover_image)) : 'none'; ?>" style="<?php echo !empty($page) && $page->cover_image ? 'background-image: url(' . (URL_PUBLIC . 'public/page/cover_image/' . $page->id . '/' . rawurlencode($page->cover_image)) . ');' : 'none'; ?>"> </button> <span class="plusIcon">+</span> <div class="crop_icon"> <img src="<?php echo URL_PUBLIC ?>wolf/admin/images/crop.png" alt="crop" /> </div> </div> <p>Drag & Drop or <span class="clickUpload">Browse</span></p> </div> <span class="note">(Maximum Size: 500kb)</span> <span class="note">(Minimun: 600px x 450px)</span> <span class="note">(Ratio: 4:3)</span> <span class="note">(Allowed file type: .png, .jpg, .jpeg, .webp)</span> </div> <br> <?php $cover_status = (!empty($postdata['cover_status']) ? $postdata['cover_status'] : (!empty($page->cover_status) ? $page->cover_status : '0')); ?> <h3><?php echo __('Status'); ?></h3> <div id="meta-pages" class="switch_container"> <label class="switch"> <input type="checkbox" class="status_switch" name="page[cover_status]" <?php echo $cover_status == 1 ? 'checked' : ''; ?> value="1"> <span class="switch_slider round"></span> </label> <label class="switch_label"><?php echo $cover_status == 1 ? 'Show' : 'Hidden'; ?><label> </div> </div> <?php endif ?> </div> </div> <!-- Add Switch for external link redirection status --> <?php $redirection = !empty($page->redirection) ? $page->redirection : '0'; ?> <div id="meta-pages" class="switch_container"> <label class="switch"> <input type="checkbox" name="page[redirection]" <?php echo $redirection == 1 ? 'checked' : ''; ?> value="1"> <span class="switch_slider round"></span> </label> <label class="switch_label">Open New Tab<label> </div> <!-- Add Cropper --> <?php $ratios = Option::findByCategory("ratio"); usort($ratios, function($a, $b) { // Put "Free" on top if ($a->name === 'Free' && $b->name !== 'Free') return -1; if ($b->name === 'Free' && $a->name !== 'Free') return 1; // Otherwise sort alphabetically return strcasecmp($a->name, $b->name); }); ?> <div id="cropModal"> <div class="modal-content"> <h2 class="modal-title">Crop Image</h2> <div class="aspect-ratio-selector"> <label for="aspectRatio">Aspect Ratio:</label> <select id="aspectRatio"> <?php foreach ($ratios as $ratio) : ?> <option value="<?php echo $ratio->code; ?>"><?php echo $ratio->name; ?></option> <?php endforeach ?> </select> </div> <div class="crop-area"> <img id="cropImage" /> </div> <div class="modal-actions"> <button id="cropConfirm" type="button">Crop & Save</button> <button id="closeCropModal" type="button">Close</button> </div> </div> </div>
<!-- Update this part -->
<ul class="tabNavigation">
    <li class="tab"><a href="#pagetitle"><?php echo __('Page Title'); ?></a></li>
    <li class="tab"><a href="#metadata"><?php echo __('Metadata'); ?></a></li>
    <li class="tab"><a href="#page-setting"><?php echo __('Page Setting'); ?></a></li>
    <li class="tab"><a href="#menu-setting"><?php echo __('Menu Setting'); ?></a></li>
  <?php Observer::notify('view_page_edit_tab_links', $page); ?>
</ul>

<!-- Change id 'settings' to 'page-setting' -->
<div id="page-setting" class="page">...</div>
<!-- Add New div 'menu-setting' -->
<div id="menu-setting" class="page">
    <div id="div-menu-setting" class="title display_flex" title="<?php echo __('Menu Setting'); ?>">
      <div id="icon">
        <h3><?php echo __('Icon'); ?></h3>
        <div id="meta-pages" class="pages">
          <div class="drop-area" data-class="icon">
            <input type="hidden" name="crop_icon" id="hidden_icon">
            <input type="file" class="imgFile" name="icon" accept=".png, .jpg, .jpeg, .svg, .ico, .webp" data-max-size="500" data-min-width="80" data-min-height="80" hidden/>
            <img class="ori_img" src="<?php echo !empty($page) && $page->icon  ? (URL_PUBLIC . 'public/page/icon/' . $page->id . '/' . rawurlencode($page->icon)) : 'none'; ?>"></img>
            <div class="previewContainer">
              <button class="previewImg" type="button" data-url="<?php echo !empty($page) && $page->icon  ? (URL_PUBLIC . 'public/page/icon/' . $page->id . '/' . rawurlencode($page->icon))  : 'none'; ?>"
                style="<?php echo !empty($page) && $page->icon  ? 'background-image: url(' . (URL_PUBLIC . 'public/page/icon/' . $page->id . '/' . rawurlencode($page->icon)) . ');'  : 'none'; ?>">
              </button>
              <span class="plusIcon">+</span>
              <div class="crop_icon">
                <img src="<?php echo URL_PUBLIC ?>wolf/admin/images/crop.png" alt="crop" />
              </div>
            </div>
            <p>Drag & Drop or <span class="clickUpload">Browse</span></p>
          </div>
          <span class="note">(Maximum Size: 500kb)</span>
          <span class="note">(Minimun: 80px x 80px)</span>
          <span class="note">(Allowed file type: .png, .jpg, .jpeg, .svg, .ico, .webp)</span>
        </div>
        <br>

        <?php
          $icon_status = (!empty($postdata['icon_status']) ? $postdata['icon_status'] : (!empty($page->icon_status) ? $page->icon_status : '0'));
          $icon_display = $postdata['icon_display'] ?? ($page->icon_display ?? '');
        ?>
        <h3><?php echo __('Status'); ?></h3>
        <div id="meta-pages" class="switch_container">
            <label class="switch">
                <input type="checkbox" class="status_switch" name="page[icon_status]" <?php echo $icon_status == 1 ? 'checked' : ''; ?> value="1">
                <span class="switch_slider round"></span>
            </label>
            <label class="switch_label"><?php echo $icon_status == 1 ? 'Show' : 'Hidden'; ?><label>
        </div>
        <br>

        <h3><?php echo __('Display'); ?></h3>
          <div id="meta-pages" class="pages">
                <select class="icon_display" name="page[icon_display]">
                    <option></option>
                    <option value="text" <?= $icon_display === 'text' ? 'selected' : '' ?>>Show Text Only</option>
                    <option value="icon" <?= $icon_display === 'icon' ? 'selected' : '' ?>>Show Icon Only</option>
                    <option value="both" <?= $icon_display === 'both' ? 'selected' : '' ?>>Show Text & Icon</option>
                </select>
          </div>
      </div>


      <?php if (isset($page) && $page->id != 1) : ?>
        <div id="cover">
          <h3><?php echo __('Cover Image'); ?></h3>
          <div id="meta-pages" class="pages">
            <div class="drop-area" data-class="cover_image">
              <input type="hidden" name="crop_cover_image" id="hidden_cover_image">
              <input type="file" class="imgFile" name="cover_image" accept=".png, .jpg, .jpeg, .webp" data-max-size="500" data-min-width="600" data-min-height="450" data-ratio="4:3" data-crop="false" hidden/>
              <img class="ori_img" src="<?php echo !empty($page) && $page->cover_image  ? (URL_PUBLIC . 'public/page/cover_image/' . $page->id . '/' . rawurlencode($page->cover_image)) : 'none'; ?>"></img>
              <div class="previewContainer">
                <button class="previewImg" type="button" data-url="<?php echo !empty($page) && $page->cover_image  ? (URL_PUBLIC . 'public/page/cover_image/' . $page->id . '/' . rawurlencode($page->cover_image))  : 'none'; ?>"
                  style="<?php echo !empty($page) && $page->cover_image  ? 'background-image: url(' . (URL_PUBLIC . 'public/page/cover_image/' . $page->id . '/' . rawurlencode($page->cover_image)) . ');'  : 'none'; ?>">
                </button>
                <span class="plusIcon">+</span>
                <div class="crop_icon">
                  <img src="<?php echo URL_PUBLIC ?>wolf/admin/images/crop.png" alt="crop" />
                </div>
              </div>
              <p>Drag & Drop or <span class="clickUpload">Browse</span></p>
            </div>
            <span class="note">(Maximum Size: 500kb)</span>
            <span class="note">(Minimun: 600px x 450px)</span>
            <span class="note">(Ratio: 4:3)</span>
            <span class="note">(Allowed file type: .png, .jpg, .jpeg, .webp)</span>
          </div>
          <br>

          <?php
            $cover_status = (!empty($postdata['cover_status']) ? $postdata['cover_status'] : (!empty($page->cover_status) ? $page->cover_status : '0'));
          ?>
          <h3><?php echo __('Status'); ?></h3>
          <div id="meta-pages" class="switch_container">
              <label class="switch">
                  <input type="checkbox" class="status_switch" name="page[cover_status]" <?php echo $cover_status == 1 ? 'checked' : ''; ?> value="1">
                  <span class="switch_slider round"></span>
              </label>
              <label class="switch_label"><?php echo $cover_status == 1 ? 'Show' : 'Hidden'; ?><label>
          </div>
        </div>
      <?php endif ?>

    </div>
</div>

<!-- Add Switch for external link redirection status -->
<?php
    $redirection = !empty($page->redirection) ? $page->redirection : '0';
?>
<div id="meta-pages" class="switch_container">
  <label class="switch">
    <input type="checkbox" name="page[redirection]" <?php echo $redirection == 1 ? 'checked' : ''; ?> value="1">
    <span class="switch_slider round"></span>
  </label>
  <label class="switch_label">Open New Tab<label>
</div>

<!-- Add Cropper -->
<?php $ratios = Option::findByCategory("ratio"); 
        usort($ratios, function($a, $b) {
            // Put "Free" on top
            if ($a->name === 'Free' && $b->name !== 'Free') return -1;
            if ($b->name === 'Free' && $a->name !== 'Free') return 1;

            // Otherwise sort alphabetically
            return strcasecmp($a->name, $b->name);
        });
?>
<div id="cropModal">
  <div class="modal-content">
    <h2 class="modal-title">Crop Image</h2>
    <div class="aspect-ratio-selector">
        <label for="aspectRatio">Aspect Ratio:</label>
        <select id="aspectRatio">
            <?php foreach ($ratios as $ratio) : ?>
                <option value="<?php echo $ratio->code; ?>"><?php echo $ratio->name; ?></option>
            <?php endforeach ?>
        </select>
    </div>
    <div class="crop-area">
      <img id="cropImage" />
    </div>
    <div class="modal-actions">
      <button id="cropConfirm" type="button">Crop & Save</button>
      <button id="closeCropModal" type="button">Close</button>
    </div>
  </div>
</div>

Step 2

Update: PageController.php
Add 2 new functions:

  • upload_file()
  • upload_img()

Add verification in _store()

Add image upload function in _store()

function upload_file($origin, $dest, $tmp_name, $overwrite = false, $nid = null, $dbColumnName = null) { FileManagerController::_checkPermission(); // Config: max upload limit in bytes $max_upload_limit = Setting::get('max_img_size_upload'); $max_upload_bytes = $max_upload_limit * 1024 * 1024; $origin = basename($origin); $file_ext = (strpos($origin, '.') === false ? '' : '.' . pathinfo($origin, PATHINFO_EXTENSION)); $file_base = pathinfo($origin, PATHINFO_FILENAME); $file_base = preg_replace('/_\d+$/', '', $file_base); // remove _1, _2, etc. $full_dest = $dest . $origin; $file_name = $origin; // Check file size $uploaded_file_size = filesize($tmp_name); if ($uploaded_file_size > $max_upload_bytes) { Flash::set('error', __('Uploaded file exceeds the maximum allowed size of ' . $max_upload_limit . 'MB.')); return false; } // Ensure destination folder exists if (!file_exists($dest)) { if (!mkdir($dest, 0755, true)) { Flash::set('error', __('Failed to create destination directory.')); return false; } } $page = Page::findById($nid); $oldFilename = $page->$dbColumnName; // Always generate a new unique filename if file already exists for ($i = 1; file_exists($full_dest); $i++) { $file_name = $file_base . '_' . $i . $file_ext; $full_dest = $dest . $file_name; } // Delete the old file only once, if overwrite is enabled if ($page && $overwrite && $oldFilename && $oldFilename !== $file_name) { @unlink($dest . $oldFilename); } // Move uploaded file if ((is_uploaded_file($tmp_name) && move_uploaded_file($tmp_name, $full_dest)) || (!is_uploaded_file($tmp_name) && rename($tmp_name, $full_dest))) { chmod($full_dest, 0644); // Update DB if needed if ($page && $dbColumnName) { $updated = $page->update('Page',[$dbColumnName => $file_name], 'id=' . (int) $nid); if (!$updated) { Flash::set('error', __('Image has not been updated in Database: ' . $dbColumnName)); } } return $file_name; } return false; } // upload_file // Upload if any cropped image exists, if not upload the original image. function upload_img($module, $field, $overwrite=null, $id, $old_file_name=null) { $failed = null; $croppedField = 'crop_'.$field; // Case 1: Cropped image sent as Base64 (hidden input) if (!empty($_POST[$croppedField])) { $data = $_POST[$croppedField]; if (strpos($data, ',') !== false) { list(, $data) = explode(',', $data, 2); } $data = base64_decode($data); // Create a temp file to mimic an uploaded file $tmpFile = tempnam(sys_get_temp_dir(), 'crop_'); file_put_contents($tmpFile, $data); $filename = ''; if (!empty($_FILES[$field]['name'])) { $filename = $_FILES[$field]['name']; } else if ($old_file_name) { $filename = $old_file_name; } else { $filename = 'crop_'.$field.'.webp'; } $path = FILES_DIR . '/'. $module .'/' . $field . '/' . $id . '/'; // Reuse your upload_file() $upload = $this->upload_file($filename,$path,$tmpFile,$overwrite,$id,$field); // Clean up the temp file unlink($tmpFile); if (!$upload) { $failed = 'Cropped ' . ucwords(str_replace('_', ' ', $field)); } // Case 2: Original file uploaded via input[type=file] } else if (!empty($_FILES[$field]['tmp_name']) && $_FILES[$field]['error'] === UPLOAD_ERR_OK) { $upload = $this->upload_file($_FILES[$field]['name'], FILES_DIR . '/'. $module .'/' . $field . '/' . $id . '/', $_FILES[$field]['tmp_name'], $overwrite, $id, $field); if (!$upload) { $failed = ucwords(str_replace('_', ' ', $field)); } } return $failed; } private function _store($action, $id=false) { // Sanity checks if ($action == 'edit' && !$id) throw new Exception('Trying to edit page when $id is false.'); use_helper('Validate'); $data = $_POST['page']; $data['is_protected'] = !empty($data['is_protected']) ? 1 : 0; Flash::set('post_data', (object) $data); $pagesetting = array(); if ($id == 1){ // $upload = $_POST['upload']; $pagesetting = $_POST['pagesetting']; //Flash::set('post_settingdata', (object) $pagesetting); } // Add pre-save checks here $errors = false; $error_fields = false; // CSRF checks if (isset($_POST['csrf_token'])) { $csrf_token = $_POST['csrf_token']; $csrf_id = ''; if ($action === 'edit') { $csrf_id = '/'.$id; } if (!SecureToken::validateToken($csrf_token, BASE_URL.'page/'.$action.$csrf_id)) { $errors[] = __('Invalid CSRF token found!'); } } else { $errors[] = __('No CSRF token found!'); } $data['title'] = trim($data['title']); if (empty($data['title'])) { $error_fields[] = __('Page Title'); } /** homepage setting check **/ if ($id == 1){ if (empty($pagesetting['meeting_venue'])){ $error_fields[] = __('Meeting Venue'); } if (empty($pagesetting['meeting_google_map'])){ $error_fields[] = __('Meeting Google Map Link'); } if (empty($pagesetting['meeting_day'])){ $error_fields[] = __('Meeting Day'); } if (empty($pagesetting['meeting_time'])){ $error_fields[] = __('Meeting Time'); } if (empty($pagesetting['donate_short_text'])){ $error_fields[] = __('Donate Short Text'); } if (empty($pagesetting['project_short_text'])){ $error_fields[] = __('Project Short Text'); } $pagesetting_ori = PageSetting::init(); } $data['slug'] = (!empty($data['slug']) ? trim($data['slug']) : ''); if (empty($data['slug']) && $id != '1') { $error_fields[] = __('Slug'); } else { if ($data['slug'] == ADMIN_DIR) { $errors[] = __('You cannot have a slug named :slug!', array(':slug' => ADMIN_DIR)); } if (!Validate::slug($data['slug']) && (!empty($data['slug']) && $id == '1')) { $errors[] = __('Illegal value for :fieldname field!', array(':fieldname' => 'slug')); } } // Check all numerical fields for a page $fields = array('parent_id', 'layout_id', 'needs_login'); foreach ($fields as $field) { if (!Validate::digit($data[$field])) { $errors[] = __('Illegal value for :fieldname field!', array(':fieldname' => $field)); } } // Check all date fields for a page $fields = array('created_on', 'published_on', 'valid_until'); foreach ($fields as $field) { if (isset($data[$field])) { $data[$field] = trim($data[$field]); if (!empty($data[$field]) && !(bool) preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/D', (string) $data[$field])) { $errors[] = __('Illegal value for :fieldname field!', array(':fieldname' => $field)); } } } // Check all time fields for a page $fields = array('created_on_time', 'published_on_time', 'valid_until_time'); foreach ($fields as $field) { if (isset($data[$field])) { $data[$field] = trim($data[$field]); if (!empty($data[$field]) && !(bool) preg_match('/^[0-9]{2}:[0-9]{2}:[0-9]{2}$/D', (string) $data[$field])) { $errors[] = __('Illegal value for :fieldname field!', array(':fieldname' => $field)); } } } // Check alphanumerical fields $fields = array('keywords', 'description'); foreach ($fields as $field) { use_helper('Kses'); $data[$field] = kses(trim($data[$field]), array()); /* if (!empty($data[$field]) && !Validate::alpha_comma($data[$field])) { $errors[] = __('Illegal value for :fieldname field!', array(':fieldname' => $field)); } * */ } // Check behaviour_id field if (!empty($data['behaviour_id']) && !Validate::slug($data['behaviour_id'])) { $errors[] = __('Illegal value for :fieldname field!', array(':fieldname' => 'behaviour_id')); } // Make sure the title doesn't contain HTML if (Setting::get('allow_html_title') == 'off') { use_helper('Kses'); $data['title'] = kses(trim($data['title']), array()); } // verification if (empty($data['redirection'])){ $data['redirection'] = 0; } if (empty($data['icon_status'])) { $data['icon_status'] = 0; } if (empty($data['cover_status'])) { $data['cover_status'] = 0; } // Create the page object to be manipulated and populate data if ($action == 'add') { $page = new Page($data); } else { $page = Record::findByIdFrom('Page', $id); $page->setFromData($data); } // Upon errors, rebuild original page and return to screen with errors if (false !== $errors || $error_fields !== false) { $tags = $_POST['page_tag']; // Rebuild time fields if (isset($page->created_on) && isset($page->created_on_time)) { $page->created_on = $page->created_on.' '.$page->created_on_time; } if (isset($page->published_on) && isset($page->published_on_time)) { $page->published_on = $page->published_on.' '.$page->published_on_time; } if (isset($page->valid_until)) { $page->valid_until = $page->valid_until.' '.$page->valid_until_time; } // Rebuild parts $part = ''; if (!empty($_POST['part'])) { $part = $_POST['part']; $tmp = false; foreach ($part as $key => $val) { $tmp[$key] = (object) $val; } $part = $tmp; } // Set the errors to be displayed. $err_msg = ($errors != false ? implode('<br/>', $errors) : ''); $err_msg .= ($error_fields != false ? 'Please specify these fields: '.implode(', ', $error_fields) : ''); Flash::setNow('error', $err_msg); // display things ... $this->setLayout('backend'); $pagesettingobj = new stdClass(); foreach ($pagesetting as $name => $value) { $pagesettingobj->$name = $value; } $this->display('page/edit', array( 'action' => $action, 'csrf_token' => SecureToken::generateToken(BASE_URL.'page/'.$action), 'page' => (object) $page, 'pagesetting' => $pagesettingobj, 'tags' => $tags, 'filters' => Filter::findAll(), 'behaviors' => Behavior::findAll(), 'page_parts' => $part, 'layouts' => Record::findAllFrom('Layout')) ); } // Notify if ($action == 'add') { Observer::notify('page_add_before_save', $page); } else { Observer::notify('page_edit_before_save', $page); } // Time to actually save the page // @todo rebuild this so parts are already set before save? // @todo determine lazy init impact if ($page->save()) { // Get data for parts of this page $data_parts = $_POST['part']; Flash::set('post_parts_data', (object) $data_parts); if ($action == 'edit') { $old_parts = PagePart::findByPageId($id); // check if all old page part are passed in POST // if not ... we need to delete it! foreach ($old_parts as $old_part) { $not_in = true; foreach ($data_parts as $part_id => $data) { $data['name'] = trim($data['name']); if ($old_part->name == $data['name']) { $not_in = false; // this will not really create a new page part because // the id of the part is passed in $data $part = new PagePart($data); $part->page_id = $id; Observer::notify('part_edit_before_save', $part); $part->save(); Observer::notify('part_edit_after_save', $part); unset($data_parts[$part_id]); break; } } if ($not_in) $old_part->delete(); } } // add the new parts foreach ($data_parts as $data) { $data['name'] = trim($data['name']); $part = new PagePart($data); $part->page_id = $page->id; Observer::notify('part_add_before_save', $part); $part->save(); Observer::notify('part_add_after_save', $part); } // save tags $page->saveTags($_POST['page_tag']['tags']); // save homepage info - customization if ($id == 1) { PageSetting::saveFromData($pagesetting); } $overwrite = true; $failed = []; // Upload 2 type images // Make sure fields name match database name $fields = ['icon', 'cover_image']; foreach ($fields as $field) { $failed[] = $this->upload_img('page', $field, $overwrite, $page->id, $page->$field); } if (!empty(array_filter($failed))) { Flash::set('error', __('Page has been saved. Failed to update: ' . implode(', ', $failed))); } else { Flash::set('success', __('Page has been saved.')); } } else { Flash::set('error', __('Page has not been saved!')); $url = 'page/'; $url .= ( $action == 'edit') ? 'edit/'.$id : 'add/'; redirect(get_url($url)); } if ($action == 'add') { Observer::notify('page_add_after_save', $page); } else { Observer::notify('page_edit_after_save', $page); } // save and quit or save and continue editing ? if (isset($_POST['commit'])) { redirect(get_url('page')); } else { redirect(get_url('page/edit/'.$page->id)); } }
function upload_file($origin, $dest, $tmp_name, $overwrite = false, $nid = null, $dbColumnName = null) {
    FileManagerController::_checkPermission();

    // Config: max upload limit in bytes
    $max_upload_limit = Setting::get('max_img_size_upload');
    $max_upload_bytes = $max_upload_limit * 1024 * 1024;

    $origin     = basename($origin);
    $file_ext   = (strpos($origin, '.') === false ? '' : '.' . pathinfo($origin, PATHINFO_EXTENSION));
    $file_base  = pathinfo($origin, PATHINFO_FILENAME);
    $file_base  = preg_replace('/_\d+$/', '', $file_base); // remove _1, _2, etc.
    $full_dest  = $dest . $origin;
    $file_name  = $origin;

    // Check file size
    $uploaded_file_size = filesize($tmp_name);
    if ($uploaded_file_size > $max_upload_bytes) {
        Flash::set('error', __('Uploaded file exceeds the maximum allowed size of ' . $max_upload_limit . 'MB.'));
        return false;
    }

    // Ensure destination folder exists
    if (!file_exists($dest)) {
        if (!mkdir($dest, 0755, true)) {
            Flash::set('error', __('Failed to create destination directory.'));
            return false;
        }
    }

    $page = Page::findById($nid);
    $oldFilename = $page->$dbColumnName;

    // Always generate a new unique filename if file already exists
    for ($i = 1; file_exists($full_dest); $i++) {
        $file_name = $file_base . '_' . $i . $file_ext;
        $full_dest = $dest . $file_name;
    }

    // Delete the old file only once, if overwrite is enabled
    if ($page && $overwrite && $oldFilename && $oldFilename !== $file_name) {
        @unlink($dest . $oldFilename);
    }

    // Move uploaded file
    if ((is_uploaded_file($tmp_name) && move_uploaded_file($tmp_name, $full_dest))
        || (!is_uploaded_file($tmp_name) && rename($tmp_name, $full_dest))) {
        chmod($full_dest, 0644);

        // Update DB if needed
        if ($page && $dbColumnName) {
            $updated = $page->update('Page',[$dbColumnName => $file_name], 'id=' . (int) $nid);
            if (!$updated) {
                Flash::set('error', __('Image has not been updated in Database: ' . $dbColumnName));
            }
        }

        return $file_name;
    }

    return false;
} // upload_file

// Upload if any cropped image exists, if not upload the original image.
function upload_img($module, $field, $overwrite=null, $id, $old_file_name=null) {
    $failed = null;
    $croppedField = 'crop_'.$field;

    // Case 1: Cropped image sent as Base64 (hidden input)
    if (!empty($_POST[$croppedField])) {
        $data = $_POST[$croppedField];

        if (strpos($data, ',') !== false) {
            list(, $data) = explode(',', $data, 2);
        }

        $data = base64_decode($data);

        // Create a temp file to mimic an uploaded file
        $tmpFile = tempnam(sys_get_temp_dir(), 'crop_');
        file_put_contents($tmpFile, $data);

        $filename = '';

        if (!empty($_FILES[$field]['name'])) {
            $filename = $_FILES[$field]['name'];
        } else if ($old_file_name) {
            $filename = $old_file_name;
        } else {
            $filename = 'crop_'.$field.'.webp';
        }

        $path = FILES_DIR . '/'. $module .'/' . $field . '/' . $id . '/';

        // Reuse your upload_file()
        $upload = $this->upload_file($filename,$path,$tmpFile,$overwrite,$id,$field);

        // Clean up the temp file
        unlink($tmpFile);

        if (!$upload) {
            $failed = 'Cropped ' . ucwords(str_replace('_', ' ', $field));
        }

    // Case 2: Original file uploaded via input[type=file]
    } else if (!empty($_FILES[$field]['tmp_name']) && $_FILES[$field]['error'] === UPLOAD_ERR_OK) {
        $upload = $this->upload_file($_FILES[$field]['name'], FILES_DIR . '/'. $module .'/' . $field . '/' . $id . '/', $_FILES[$field]['tmp_name'], $overwrite, $id, $field);

        if (!$upload) {
            $failed = ucwords(str_replace('_', ' ', $field));
        }
    }
    
    return $failed;
}

private function _store($action, $id=false) { 
    // Sanity checks
    if ($action == 'edit' && !$id)
        throw new Exception('Trying to edit page when $id is false.');

    use_helper('Validate');
    $data = $_POST['page'];
    $data['is_protected'] = !empty($data['is_protected']) ? 1 : 0;
    
    Flash::set('post_data', (object) $data);

    $pagesetting = array();
    if ($id == 1){
        // $upload = $_POST['upload'];
        $pagesetting = $_POST['pagesetting'];
        //Flash::set('post_settingdata', (object) $pagesetting);
    }

    // Add pre-save checks here
    $errors = false;
    $error_fields = false;
    // CSRF checks
    if (isset($_POST['csrf_token'])) {
        $csrf_token = $_POST['csrf_token'];
        $csrf_id = '';
        if ($action === 'edit') { $csrf_id = '/'.$id; }
        if (!SecureToken::validateToken($csrf_token, BASE_URL.'page/'.$action.$csrf_id)) {
            $errors[] = __('Invalid CSRF token found!');
        }
    }
    else {
        $errors[] = __('No CSRF token found!');
    }

    $data['title'] = trim($data['title']);
    if (empty($data['title'])) {
        $error_fields[] = __('Page Title');
    }

    /** homepage setting check **/
    if ($id == 1){
        if (empty($pagesetting['meeting_venue'])){
            $error_fields[] = __('Meeting Venue');
        }
        if (empty($pagesetting['meeting_google_map'])){
            $error_fields[] = __('Meeting Google Map Link');
        }
        if (empty($pagesetting['meeting_day'])){
            $error_fields[] = __('Meeting Day');
        }
        if (empty($pagesetting['meeting_time'])){
            $error_fields[] = __('Meeting Time');
        }
        if (empty($pagesetting['donate_short_text'])){
            $error_fields[] = __('Donate Short Text');
        }
        if (empty($pagesetting['project_short_text'])){
            $error_fields[] = __('Project Short Text');
        }
        
        $pagesetting_ori = PageSetting::init();
    }

    $data['slug'] = (!empty($data['slug']) ? trim($data['slug']) : '');
    if (empty($data['slug']) && $id != '1') {
        $error_fields[] = __('Slug');
    }
    else {
        if ($data['slug'] == ADMIN_DIR) {
            $errors[] = __('You cannot have a slug named :slug!', array(':slug' => ADMIN_DIR));
        }
        if (!Validate::slug($data['slug']) && (!empty($data['slug']) && $id == '1')) {
            $errors[] = __('Illegal value for :fieldname field!', array(':fieldname' => 'slug'));
        }
    }

    // Check all numerical fields for a page
    $fields = array('parent_id', 'layout_id', 'needs_login');
    foreach ($fields as $field) {
        if (!Validate::digit($data[$field])) {
            $errors[] = __('Illegal value for :fieldname field!', array(':fieldname' => $field));
        }
    }

    // Check all date fields for a page
    $fields = array('created_on', 'published_on', 'valid_until');
    foreach ($fields as $field) {
        if (isset($data[$field])) {
            $data[$field] = trim($data[$field]);
            if (!empty($data[$field]) && !(bool) preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/D', (string) $data[$field])) {
                $errors[] = __('Illegal value for :fieldname field!', array(':fieldname' => $field));
            }
        }
    }

    // Check all time fields for a page
    $fields = array('created_on_time', 'published_on_time', 'valid_until_time');
    foreach ($fields as $field) {
        if (isset($data[$field])) {
            $data[$field] = trim($data[$field]);
            if (!empty($data[$field]) && !(bool) preg_match('/^[0-9]{2}:[0-9]{2}:[0-9]{2}$/D', (string) $data[$field])) {
                $errors[] = __('Illegal value for :fieldname field!', array(':fieldname' => $field));
            }
        }
    }

    // Check alphanumerical fields
    $fields = array('keywords', 'description');
    foreach ($fields as $field) {
        use_helper('Kses');
        $data[$field] = kses(trim($data[$field]), array());
        /*
        if (!empty($data[$field]) && !Validate::alpha_comma($data[$field])) {
            $errors[] = __('Illegal value for :fieldname field!', array(':fieldname' => $field));
        }
            *
            */
    }

    // Check behaviour_id field
    if (!empty($data['behaviour_id']) && !Validate::slug($data['behaviour_id'])) {
        $errors[] = __('Illegal value for :fieldname field!', array(':fieldname' => 'behaviour_id'));
    }

    // Make sure the title doesn't contain HTML
    if (Setting::get('allow_html_title') == 'off') {
        use_helper('Kses');
        $data['title'] = kses(trim($data['title']), array());
    }

    // verification
    if (empty($data['redirection'])){
        $data['redirection'] = 0;
    }

    if (empty($data['icon_status'])) {
        $data['icon_status'] = 0;
    }

    if (empty($data['cover_status'])) {
        $data['cover_status'] = 0;
    }

    // Create the page object to be manipulated and populate data
    if ($action == 'add') {
        $page = new Page($data);
    }
    else {
        $page = Record::findByIdFrom('Page', $id);
        $page->setFromData($data);
    }

    // Upon errors, rebuild original page and return to screen with errors
    if (false !== $errors || $error_fields !== false) {
        $tags = $_POST['page_tag'];

        // Rebuild time fields
        if (isset($page->created_on) && isset($page->created_on_time)) {
            $page->created_on = $page->created_on.' '.$page->created_on_time;
        }

        if (isset($page->published_on) && isset($page->published_on_time)) {
            $page->published_on = $page->published_on.' '.$page->published_on_time;
        }

        if (isset($page->valid_until)) {
            $page->valid_until = $page->valid_until.' '.$page->valid_until_time;
        }

        // Rebuild parts
        $part = '';
        if (!empty($_POST['part'])) {
            $part = $_POST['part'];

            $tmp = false;
            foreach ($part as $key => $val) {
                $tmp[$key] = (object) $val;
            }
            $part = $tmp;
        }

        // Set the errors to be displayed.
        $err_msg =  ($errors != false ? implode('<br/>', $errors) : '');
        $err_msg .= ($error_fields != false ? 'Please specify these fields: '.implode(', ', $error_fields) : '');
        Flash::setNow('error', $err_msg);

        // display things ...
        $this->setLayout('backend');
        
        $pagesettingobj = new stdClass();
        foreach ($pagesetting as $name => $value) {
            $pagesettingobj->$name = $value;
        }	
    
        $this->display('page/edit', array(
            'action' => $action,
            'csrf_token' => SecureToken::generateToken(BASE_URL.'page/'.$action),
            'page' => (object) $page,
            'pagesetting' => $pagesettingobj,
            'tags' => $tags,
            'filters' => Filter::findAll(),
            'behaviors' => Behavior::findAll(),
            'page_parts' => $part,
            'layouts' => Record::findAllFrom('Layout'))
        );
    }

    // Notify
    if ($action == 'add') {
        Observer::notify('page_add_before_save', $page);
    }
    else {
        Observer::notify('page_edit_before_save', $page);
    }

    // Time to actually save the page
    // @todo rebuild this so parts are already set before save?
    // @todo determine lazy init impact
    if ($page->save()) {
        // Get data for parts of this page
        $data_parts = $_POST['part'];
        Flash::set('post_parts_data', (object) $data_parts);

        if ($action == 'edit') {
            $old_parts = PagePart::findByPageId($id);

            // check if all old page part are passed in POST
            // if not ... we need to delete it!
            foreach ($old_parts as $old_part) {
                $not_in = true;
                foreach ($data_parts as $part_id => $data) {
                    $data['name'] = trim($data['name']);
                    if ($old_part->name == $data['name']) {
                        $not_in = false;

                        // this will not really create a new page part because
                        // the id of the part is passed in $data
                        $part = new PagePart($data);
                        $part->page_id = $id;

                        Observer::notify('part_edit_before_save', $part);
                        $part->save();
                        Observer::notify('part_edit_after_save', $part);

                        unset($data_parts[$part_id]);

                        break;
                    }
                }

                if ($not_in)
                    $old_part->delete();
            }
        }

        // add the new parts
        foreach ($data_parts as $data) {
            $data['name'] = trim($data['name']);
            $part = new PagePart($data);
            $part->page_id = $page->id;
            Observer::notify('part_add_before_save', $part);
            $part->save();
            Observer::notify('part_add_after_save', $part);
        }

        // save tags
        $page->saveTags($_POST['page_tag']['tags']);

        // save homepage info - customization
        if ($id == 1) {
            PageSetting::saveFromData($pagesetting);
        }

        $overwrite = true;
        $failed = [];

        // Upload 2 type images
        // Make sure fields name match database name 
        $fields = ['icon', 'cover_image'];

        foreach ($fields as $field) {
            $failed[] = $this->upload_img('page', $field, $overwrite, $page->id, $page->$field);
        }

        if (!empty(array_filter($failed))) {
            Flash::set('error', __('Page has been saved. Failed to update: ' . implode(', ', $failed)));
        } else {
            Flash::set('success', __('Page has been saved.'));
        }
        

    }
    else {
        Flash::set('error', __('Page has not been saved!'));
        $url = 'page/';
        $url .= ( $action == 'edit') ? 'edit/'.$id : 'add/';
        redirect(get_url($url));
    }

    if ($action == 'add') {
        Observer::notify('page_add_after_save', $page);
    }
    else {
        Observer::notify('page_edit_after_save', $page);
    }

    // save and quit or save and continue editing ?
    if (isset($_POST['commit'])) {
        redirect(get_url('page'));
    }
    else {
        redirect(get_url('page/edit/'.$page->id));
    }
}

Step 3

Update: style.css

/* For page image */ #page_edit_form #div-menu-setting { align-items: flex-start; gap: 3rem; } #page_edit_form #icon .previewImg { height: 160px; width: 160px; } #page_edit_form #cover .previewImg { height: 200px; width: 267px; } select.icon_display { padding: 4.5px 10px; }
/* For page image */
#page_edit_form #div-menu-setting {
  align-items: flex-start;
  gap: 3rem;
}

#page_edit_form #icon .previewImg {
  height: 160px;
  width: 160px;
}

#page_edit_form #cover .previewImg {
  height: 200px;
  width: 267px;
}

select.icon_display {
  padding: 4.5px 10px;
}

Step 4

Update: backend.js

// For module 'page' -> 'external_link' redirection $(document).ready(function () { function toggle_redirection() { if ($("#external_url").val() !== "") { $("#external_url").parent().next().fadeIn(); } else { $("#external_url").parent().next().hide(); } } toggle_redirection(); $(document).on("keyup", "#external_url", function() { toggle_redirection(); }) })
// For module 'page' -> 'external_link' redirection
$(document).ready(function () {
    function toggle_redirection() {
        if ($("#external_url").val() !== "") {
            $("#external_url").parent().next().fadeIn();
        } else {
            $("#external_url").parent().next().hide();
        }
    }

    toggle_redirection();
    
    $(document).on("keyup", "#external_url", function() {
        toggle_redirection();
    })
})

Step 5

Update Database: wolf_page
Add 6 new columns

ALTER TABLE wolf_page ADD COLUMN `redirection` INT(1) DEFAULT '0' AFTER `location`, ADD COLUMN `icon` TEXT AFTER `redirection`, ADD COLUMN `cover_image` TEXT AFTER `icon`, ADD COLUMN `icon_status` INT(1) DEFAULT NULL AFTER `cover_image`, ADD COLUMN `cover_status` INT(1) DEFAULT NULL AFTER `icon_status`, ADD COLUMN `icon_display` VARCHAR(100) DEFAULT NULL AFTER `cover_status`;
ALTER TABLE wolf_page
ADD COLUMN `redirection` INT(1) DEFAULT '0' AFTER `location`,
ADD COLUMN `icon` TEXT AFTER `redirection`,
ADD COLUMN `cover_image` TEXT AFTER `icon`,
ADD COLUMN `icon_status` INT(1) DEFAULT NULL AFTER `cover_image`,
ADD COLUMN `cover_status` INT(1) DEFAULT NULL AFTER `icon_status`,
ADD COLUMN `icon_display` VARCHAR(100) DEFAULT NULL AFTER `cover_status`;

Step 6

Update Snippet: menubar

<?php function titleWithIcon($page, $defaultTitle = '') { $title = ''; $icon = ''; // Image if icon enabled, display is not "text", and file exists if ((int)$page->icon_status === 1 && $page->icon_display !== 'text' && !empty($page->icon)) { $icon = '<img class="menu-icon" src="'.URL_PUBLIC.'public/page/icon/'.$page->id.'/'.$page->icon.'" alt="menu">'; } // Add text if display is not "icon" OR image is missing if ($page->icon_display !== 'icon' || empty($icon)) { $title = ucfirst($defaultTitle ?: $page->title); } return $icon . $title; } function getCoverImg($page) { $cover_image = ""; if ((int)$page->cover_status === 1 && !empty($page->cover_image)) { $cover_image = URL_PUBLIC.'public/page/cover_image/'.$page->id.'/'.rawurlencode($page->cover_image); } return $cover_image; } function snippet_sitemap($page_submenu, $root) { $out = ''; $count = count($page_submenu->children(array())); if ($count > 0 && $page_submenu->level() < 2) { if ($page_submenu->id == 1) { $out = '<ul id="menulist"> <li> <a href="' . URL_PUBLIC . '" ' . (url_match($page_submenu->slug) ? ' class="active"' : 'class=""') . '>'.titleWithIcon($page_submenu, 'HOME').'</a> </li>'; } else { if (!empty(getCoverImg($page_submenu))) { $out = '<div class="thumb"><ul>'; } else { $out = '<ul>'; } } $idx = 1; foreach ($page_submenu->children(array()) as $menuPage) { if ($menuPage->location == 'top') { $active = url_start_with($menuPage->url) ? ' class="active"' : 'class=""'; $data_img = ' data-img = "' . getCoverImg($menuPage) . '"'; if ($menuPage->type == "link") { if ($menuPage->external_url == '') { $out .= '<li><span>' . titleWithIcon($menuPage) . '</span>' . snippet_sitemap($menuPage, 0) .'</li>'; } else { $newtab = (int)$menuPage->redirection == 1 ? ' target="_blank"' : ''; $out .= '<li>' . $menuPage->extlink(titleWithIcon($menuPage), $active . $newtab . $data_img, $menuPage->external_url) . snippet_sitemap($menuPage, 0) .'</li>'; } } else { $out .= '<li>' . $menuPage->link(titleWithIcon($menuPage), $active . $data_img) . snippet_sitemap($menuPage, 0) .'</li>'; } } $idx += 1; } if (!empty(getCoverImg($page_submenu))) { $out .= '</ul><div class="page-cover-img" data-img="'.getCoverImg($page_submenu).'"></div></div>'; } else { $out .= '</ul>'; } } else { if (!empty(getCoverImg($page_submenu))) { $out = '<div class="thumb"><div class="page-cover-img" data-img="'.getCoverImg($page_submenu).'"></div></div>'; } else { $out = ''; } } return $out; } ?> <?php echo snippet_sitemap($this->find('/'), 1); ?>
<?php

function titleWithIcon($page, $defaultTitle = '') {
    $title = '';
    $icon   = '';

    // Image if icon enabled, display is not "text", and file exists
    if ((int)$page->icon_status === 1 && $page->icon_display !== 'text' && !empty($page->icon)) {
        $icon = '<img class="menu-icon" src="'.URL_PUBLIC.'public/page/icon/'.$page->id.'/'.$page->icon.'" alt="menu">';
    }

    // Add text if display is not "icon" OR image is missing
    if ($page->icon_display !== 'icon' || empty($icon)) {
        $title = ucfirst($defaultTitle ?: $page->title);
    }

    return $icon . $title;
}

function getCoverImg($page) {
    $cover_image = "";
    if ((int)$page->cover_status === 1 && !empty($page->cover_image)) {
        $cover_image = URL_PUBLIC.'public/page/cover_image/'.$page->id.'/'.rawurlencode($page->cover_image);
    }
    return $cover_image;
}

function snippet_sitemap($page_submenu, $root)
{
    $out = '';
    $count = count($page_submenu->children(array()));
    if ($count > 0 && $page_submenu->level() < 2) {
        if ($page_submenu->id == 1) {
            $out = '<ul id="menulist">
            <li>
                <a href="' . URL_PUBLIC . '" ' . (url_match($page_submenu->slug) ? ' class="active"' : 'class=""') . '>'.titleWithIcon($page_submenu, 'HOME').'</a>
            </li>';
        } else {
            if (!empty(getCoverImg($page_submenu))) {
                $out = '<div class="thumb"><ul>';
            } else {
                $out = '<ul>';
            }
        }

        $idx = 1;
        foreach ($page_submenu->children(array()) as $menuPage) {
            if ($menuPage->location == 'top') {
                $active = url_start_with($menuPage->url) ? ' class="active"' : 'class=""';
                $data_img = ' data-img = "' . getCoverImg($menuPage) . '"';
                if ($menuPage->type == "link") {
                    if ($menuPage->external_url == '') {
                        $out .= '<li><span>' . titleWithIcon($menuPage) . '</span>' . snippet_sitemap($menuPage, 0) .'</li>';
                    } else {
                        $newtab = (int)$menuPage->redirection == 1 ? ' target="_blank"' : '';
                        $out .= '<li>' . $menuPage->extlink(titleWithIcon($menuPage), $active . $newtab . $data_img, $menuPage->external_url) . snippet_sitemap($menuPage, 0) .'</li>';
                    }
                } else {
                    $out .= '<li>' . $menuPage->link(titleWithIcon($menuPage), $active . $data_img) . snippet_sitemap($menuPage, 0) .'</li>';
                }
            }
            $idx += 1;
        }

        if (!empty(getCoverImg($page_submenu))) {
            $out .= '</ul><div class="page-cover-img" data-img="'.getCoverImg($page_submenu).'"></div></div>';
        } else {
            $out .= '</ul>';
        }


    } else {
        if (!empty(getCoverImg($page_submenu))) {
            $out = '<div class="thumb"><div class="page-cover-img" data-img="'.getCoverImg($page_submenu).'"></div></div>';
        } else {
            $out = '';
        }
    }

    return $out;
}
?>
<?php
    echo snippet_sitemap($this->find('/'), 1);
?>

Step 7

Update Snippet: mobile-menu

<!-- responsive menu --> <script type="text/javascript"> $(function(){ let cloneMenu = $('#menulist').clone(); cloneMenu.find('.thumb > ul').unwrap(); cloneMenu.slicknav({ label: 'MENU', prependTo:'#mobile-menu' }); }); </script>
<!-- responsive menu -->
<script type="text/javascript">
	$(function(){
        let cloneMenu = $('#menulist').clone();
        cloneMenu.find('.thumb > ul').unwrap();
		cloneMenu.slicknav({
		    label: 'MENU',
	            prependTo:'#mobile-menu'
                });
	});
</script>

Step 8

Update: menu.css

*Adjust according to your design

/* hide the sub levels and give them a positon absolute so that they take up no room */ /* Allan - command this out for multiple div */ /* #menu ul ul { padding-top: 26px; width: auto; z-index: 120; list-style: none; position: absolute; top: 51px; left: 0px; margin-bottom: -15px; overflow: hidden; visibility: hidden; text-align: left; } */ /* make the second level visible when hover on first level list OR link */ /* Allan - command this out for multiple div */ /* #menu ul :hover ul { visibility: visible; height: auto; z-index: 200; } */ /* Menu too much, have to reduce left right padding */ #menu a { padding: 0 10px; } a > .menu-icon { height: 30px; width: 30px; margin: 0; } #menu #menulist > li > a { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 77px; gap: 0.5rem; } #menu #menulist > li ul > li > a { display: flex; align-items: center; justify-content: flex-start; gap: 0.5rem; } /* For Mobile */ .slicknav_menu li > a, .slicknav_menu li > a > a { display: flex !important; align-items: center; justify-content: flex-start; gap: 0.5rem; } #menu > #menulist > li > .thumb { background-color: #0050a2; display: flex; align-items: flex-start; justify-content: center; gap: 1rem; /* padding-top: 26px; */ padding: 1rem; width: auto; z-index: 120; list-style: none; position: absolute; top: 77px; left: 0px; margin-bottom: -15px; overflow: hidden; visibility: hidden; text-align: left; } #menu ul :hover .thumb { visibility: visible !important; height: auto; z-index: 200; } #menu > #menulist > li > .thumb > .page-cover-img { height: 225px; width: 300px; flex-shrink: 0; background-repeat: no-repeat; background-position: center; background-size: contain; }
/* hide the sub levels and give them a positon absolute so that they take up no room */
/* Allan - command this out for multiple div */
/* #menu ul ul {
    padding-top: 26px;
    width: auto;
    z-index: 120;
    list-style: none;
    position: absolute;
    top: 51px;
    left: 0px;
    margin-bottom: -15px;
    overflow: hidden;
    visibility: hidden;
	text-align: left;
} */

/* make the second level visible when hover on first level list OR link */
/* Allan - command this out for multiple div */
/* #menu ul :hover ul {
    visibility: visible;
    height: auto;
    z-index: 200;
} */

/* Menu too much, have to reduce left right padding */
#menu a {
    padding: 0 10px;
}

a > .menu-icon {
    height: 30px;
    width: 30px;
    margin: 0;
}

#menu #menulist > li > a {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 77px;
    gap: 0.5rem;
}

#menu #menulist > li ul > li > a {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 0.5rem;
}

/* For Mobile */
.slicknav_menu li > a,
.slicknav_menu li > a > a {
    display: flex !important;
    align-items: center;
    justify-content: flex-start;
    gap: 0.5rem;
}

#menu > #menulist > li > .thumb {
    background-color: #0050a2;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    gap: 1rem;

    /* padding-top: 26px; */
    padding: 1rem;
    width: auto;
    z-index: 120;
    list-style: none;
    position: absolute;
    top: 77px;
    left: 0px;
    margin-bottom: -15px;
    overflow: hidden;
    visibility: hidden;
	text-align: left;
}

#menu ul :hover .thumb {
    visibility: visible !important;
    height: auto;
    z-index: 200;
}

#menu > #menulist > li > .thumb > .page-cover-img {
    height: 225px;
    width: 300px;
    flex-shrink: 0;
    background-repeat: no-repeat;
    background-position: center;
    background-size: contain;
}

Step 9

Update: default.css

*Adjust according to your design

@media only screen and (max-width: 2000px) { #menulist > li:nth-last-child(-n + 2) .thumb { flex-direction: row-reverse; right: 0; left: unset !important; } } @media only screen and (max-width: 1700px) { #menulist > li:nth-last-child(-n + 5) .thumb { flex-direction: row-reverse; right: 0; left: unset !important; } } @media only screen and (max-width: 1300px) { #menu a, #menu a:visited { padding: 0 8px; } } @media only screen and (max-width: 1200px) { #menulist > li:nth-child(5) .thumb, #menulist > li:nth-child(6) .thumb { left: 50% !important; transform: translateX(-50%); flex-direction: unset; right: unset; } } @media only screen and (max-width: 1150px) { #menu { height: 77px; } #menu ul ul { top: 0; } } @media only screen and (max-width: 1024px) { #menu ul ul { top: 0; } #menulist > li:nth-child(5) .thumb, #menulist > li:nth-child(6) .thumb { left: 50% !important; transform: translateX(-50%); flex-direction: unset; right: unset; } /* Menu too long, need to show mobile menu early */ #menu { display: none; } #mobile-menu { display: block; } .slicknav_btn { position: absolute !important; right: 30px; top: 45px; } }
@media only screen and (max-width: 2000px) {
	#menulist > li:nth-last-child(-n + 2) .thumb {
		flex-direction: row-reverse; 
		right: 0;
		left: unset !important;
	}
}

@media only screen and (max-width: 1700px) {
	#menulist > li:nth-last-child(-n + 5) .thumb {
		flex-direction: row-reverse; 
		right: 0;
		left: unset !important;
	}
}

@media only screen and (max-width: 1300px) {
    #menu a, #menu a:visited {
        padding: 0 8px;
    }
}

@media only screen and (max-width: 1200px) {
	#menulist > li:nth-child(5) .thumb,
	#menulist > li:nth-child(6) .thumb {
		left: 50% !important;
		transform: translateX(-50%);
		flex-direction: unset; 
		right: unset;
	}
}

@media only screen and (max-width: 1150px) {
    #menu {
        height: 77px;
    }

	#menu ul ul {
		top: 0;
	}
}

@media only screen and (max-width: 1024px) {
    #menu ul ul {
        top: 0;
    }

	#menulist > li:nth-child(5) .thumb,
	#menulist > li:nth-child(6) .thumb {
		left: 50% !important;
		transform: translateX(-50%);
		flex-direction: unset; 
		right: unset;
	}

	/* Menu too long, need to show mobile menu early */
	#menu { display: none; }
	#mobile-menu { display: block; }
	.slicknav_btn { position: absolute !important; right: 30px; top: 45px; }
}

Step 10

Update: rckuc.js

$(document).ready(function() { $('#menulist a').hover(function() { let imgUrl = $(this).attr("data-img"); let imgContainer = $(".thumb > .page-cover-img"); if (imgUrl) { imgContainer.css("background-image", "url(" + imgUrl + ")"); } else { imgContainer.each(function() { $(this).css("background-image", "url(" + $(this).attr("data-img") + ")"); }) } }); })
$(document).ready(function() {
	$('#menulist a').hover(function() {
		let imgUrl = $(this).attr("data-img");
		let imgContainer = $(".thumb > .page-cover-img");

		if (imgUrl) {
			imgContainer.css("background-image", "url(" + imgUrl + ")");
		} else {
			imgContainer.each(function() {
				$(this).css("background-image", "url(" + $(this).attr("data-img") + ")");
			})
		}
	});
})
Code Copied To Clipboard!