feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run

- Base: wintercms/winter branch 1.2 (full framework)
- Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS
- Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication
- Partials: hero (offline-first), features, modes (offline/nube toggle),
  screenshots, pricing (3 planes), comparison, FAQ, CTA
- Plugin VivesPOS.Site with ContactForm
- Dockerfile: PHP 8.2 Apache, port 80, healthcheck
- Added winter/wn-pages, blog, sitemap, seo plugins
- Active theme set to vivespos
This commit is contained in:
2026-08-21 19:29:00 -06:00
commit 1f72193a64
3266 changed files with 531480 additions and 0 deletions

View File

@@ -0,0 +1,777 @@
<?php
namespace Cms\Widgets;
use Backend\Classes\WidgetBase;
use Cms\Classes\Asset;
use Cms\Classes\Theme;
use DirectoryIterator;
use Exception;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Response;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Winter\Storm\Exception\ApplicationException;
use Winter\Storm\Filesystem\Definitions as FileDefinitions;
use Winter\Storm\Support\Facades\File;
use Winter\Storm\Support\Facades\Input;
use Winter\Storm\Support\Facades\Url;
use Winter\Storm\Support\Str;
use Winter\Storm\Support\Svg;
/**
* CMS asset list widget.
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class AssetList extends WidgetBase
{
use \Backend\Traits\SelectableWidget;
protected $searchTerm = false;
protected $theme;
/**
* @var string Message to display when there are no records in the list.
*/
public $noRecordsMessage = 'cms::lang.asset.no_list_records';
/**
* @var string Message to display when the Delete button is clicked.
*/
public $deleteConfirmation = 'cms::lang.asset.delete_confirm';
/**
* @var array Valid asset file extensions
*/
protected $assetExtensions;
public function __construct($controller, $alias)
{
$this->alias = $alias;
$this->theme = Theme::getEditTheme();
$this->selectionInputName = 'file';
$this->assetExtensions = FileDefinitions::get('assetExtensions');
parent::__construct($controller, []);
$this->bindToController();
}
/**
* @inheritDoc
*/
protected function loadAssets()
{
$this->addCss('css/assetlist.css', 'core');
$this->addJs('js/assetlist.js', 'core');
}
/**
* Renders the widget.
* @return string
*/
public function render()
{
return $this->makePartial('body', [
'data' => $this->getData()
]);
}
//
// Event handlers
//
public function onOpenDirectory()
{
$path = Input::get('path');
if (!$this->validatePath($path)) {
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path'));
}
$delay = Input::get('delay');
if ($delay) {
usleep(1000000*$delay);
}
$this->putSession('currentPath', $path);
return [
'#'.$this->getId('asset-list') => $this->makePartial('items', ['items' => $this->getData()])
];
}
public function onRefresh()
{
return [
'#'.$this->getId('asset-list') => $this->makePartial('items', ['items' => $this->getData()])
];
}
public function onUpdate()
{
$this->extendSelection();
return $this->onRefresh();
}
public function onDeleteFiles()
{
$this->validateRequestTheme();
$fileList = Request::input('file');
$error = null;
$deleted = [];
try {
$assetsPath = $this->getAssetsPath();
foreach ($fileList as $path => $selected) {
if ($selected) {
if (!$this->validatePath($path)) {
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path'));
}
$fullPath = $assetsPath.'/'.$path;
if (File::exists($fullPath)) {
if (!File::isDirectory($fullPath)) {
if (!@File::delete($fullPath)) {
throw new ApplicationException(Lang::get(
'cms::lang.asset.error_deleting_file',
['name' => $path]
));
}
}
else {
$empty = File::isDirectoryEmpty($fullPath);
if ($empty === false) {
throw new ApplicationException(Lang::get(
'cms::lang.asset.error_deleting_dir_not_empty',
['name' => $path]
));
}
if (!@rmdir($fullPath)) {
throw new ApplicationException(Lang::get(
'cms::lang.asset.error_deleting_dir',
['name' => $path]
));
}
}
$deleted[] = $path;
$this->removeSelection($path);
}
}
}
}
catch (Exception $ex) {
$error = $ex->getMessage();
}
return [
'deleted' => $deleted,
'error' => $error,
'theme' => Request::input('theme')
];
}
public function onLoadRenamePopup()
{
$this->validateRequestTheme();
$path = Input::get('renamePath');
if (!$this->validatePath($path)) {
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path'));
}
$this->vars['originalPath'] = $path;
$this->vars['name'] = basename($path);
return $this->makePartial('rename_form');
}
public function onApplyName()
{
$this->validateRequestTheme();
$newName = trim(Input::get('name'));
if (!strlen($newName)) {
throw new ApplicationException(Lang::get('cms::lang.asset.name_cant_be_empty'));
}
if (!$this->validatePath($newName)) {
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path'));
}
if (!$this->validateName($newName)) {
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_name'));
}
$originalPath = Input::get('originalPath');
if (!$this->validatePath($originalPath)) {
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path'));
}
$originalFullPath = $this->getFullPath($originalPath);
if (!file_exists($originalFullPath)) {
throw new ApplicationException(Lang::get('cms::lang.asset.original_not_found'));
}
if (!is_dir($originalFullPath) && !$this->validateFileType($newName)) {
throw new ApplicationException(Lang::get(
'cms::lang.asset.type_not_allowed',
['allowed_types' => implode(', ', $this->assetExtensions)]
));
}
$newFullPath = $this->getFullPath(dirname($originalPath).'/'.$newName);
if (file_exists($newFullPath) && $newFullPath !== $originalFullPath) {
throw new ApplicationException(Lang::get('cms::lang.asset.already_exists'));
}
// Sanitize content if the file is being renamed to an SVG extension
$newExt = strtolower(File::extension($newName));
$oldExt = strtolower(File::extension(basename($originalPath)));
if ($newExt === 'svg' && $oldExt !== $newExt) {
File::put($originalFullPath, Svg::sanitize(File::get($originalFullPath)));
}
if (!@rename($originalFullPath, $newFullPath)) {
throw new ApplicationException(Lang::get('cms::lang.asset.error_renaming'));
}
return [
'#'.$this->getId('asset-list') => $this->makePartial('items', ['items' => $this->getData()])
];
}
public function onLoadNewDirPopup()
{
$this->validateRequestTheme();
return $this->makePartial('new_dir_form');
}
public function onNewDirectory()
{
$this->validateRequestTheme();
$newName = trim(Input::get('name'));
if (!strlen($newName)) {
throw new ApplicationException(Lang::get('cms::lang.asset.name_cant_be_empty'));
}
if (!$this->validatePath($newName)) {
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path'));
}
if (!$this->validateName($newName)) {
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_name'));
}
$newFullPath = $this->getCurrentPath().'/'.$newName;
if (file_exists($newFullPath)) {
throw new ApplicationException(Lang::get('cms::lang.asset.already_exists'));
}
if (!File::makeDirectory($newFullPath)) {
throw new ApplicationException(Lang::get(
'cms::lang.cms_object.error_creating_directory',
['name' => $newName]
));
}
return [
'#'.$this->getId('asset-list') => $this->makePartial('items', ['items' => $this->getData()])
];
}
public function onLoadMovePopup()
{
$this->validateRequestTheme();
$fileList = Request::input('file');
$directories = [];
$selectedList = array_filter($fileList, function ($value) {
return $value == 1;
});
$this->listDestinationDirectories($directories, $selectedList);
$this->vars['directories'] = $directories;
$this->vars['selectedList'] = base64_encode(json_encode(array_keys($selectedList)));
return $this->makePartial('move_form');
}
public function onMove()
{
$this->validateRequestTheme();
$selectedList = Input::get('selectedList');
if (!strlen($selectedList)) {
throw new ApplicationException(Lang::get('cms::lang.asset.selected_files_not_found'));
}
$destinationDir = Input::get('dest');
if (!strlen($destinationDir)) {
throw new ApplicationException(Lang::get('cms::lang.asset.select_destination_dir'));
}
$destinationFullPath = $this->getFullPath($destinationDir);
if (!file_exists($destinationFullPath) || !is_dir($destinationFullPath)) {
throw new ApplicationException(Lang::get('cms::lang.asset.destination_not_found'));
}
$list = @json_decode(@base64_decode($selectedList));
if ($list === false) {
throw new ApplicationException(Lang::get('cms::lang.asset.selected_files_not_found'));
}
foreach ($list as $path) {
if (!$this->validatePath($path)) {
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path'));
}
$basename = basename($path);
$originalFullPath = $this->getFullPath($path);
$newFullPath = realpath(rtrim($destinationFullPath, '/')) . '/' . $basename;
$safeDir = $this->getAssetsPath();
if ($originalFullPath == $newFullPath) {
continue;
}
if (!starts_with($newFullPath, $safeDir)) {
throw new ApplicationException(Lang::get(
'cms::lang.asset.error_moving_file',
['file' => $basename]
));
}
if (is_file($originalFullPath)) {
if (!@File::move($originalFullPath, $newFullPath)) {
throw new ApplicationException(Lang::get(
'cms::lang.asset.error_moving_file',
['file' => $basename]
));
}
}
elseif (is_dir($originalFullPath)) {
if (!@File::copyDirectory($originalFullPath, $newFullPath)) {
throw new ApplicationException(Lang::get(
'cms::lang.asset.error_moving_directory',
['dir' => $basename]
));
}
if (strpos($originalFullPath, '../') !== false) {
throw new ApplicationException(Lang::get(
'cms::lang.asset.error_deleting_directory',
['dir' => $basename]
));
}
if (strpos($originalFullPath, $safeDir) !== 0) {
throw new ApplicationException(Lang::get(
'cms::lang.asset.error_deleting_directory',
['dir' => $basename]
));
}
if (!@File::deleteDirectory($originalFullPath)) {
throw new ApplicationException(Lang::get(
'cms::lang.asset.error_deleting_directory',
['dir' => $basename]
));
}
}
}
return [
'#'.$this->getId('asset-list') => $this->makePartial('items', ['items' => $this->getData()])
];
}
public function onSearch()
{
$this->setSearchTerm(Input::get('search'));
$this->extendSelection();
return $this->onRefresh();
}
/*
* Methods for the internal use
*/
protected function getData()
{
$assetsPath = $this->getAssetsPath();
// theme dir does not exist (i.e. in a child theme without an assets directory
if (!is_dir(dirname($assetsPath))) {
return [];
}
if (!file_exists($assetsPath) || !is_dir($assetsPath)) {
if (!File::makeDirectory($assetsPath)) {
throw new ApplicationException(Lang::get(
'cms::lang.cms_object.error_creating_directory',
['name' => $assetsPath]
));
}
}
$searchTerm = Str::lower($this->getSearchTerm());
if (!strlen($searchTerm)) {
$currentPath = $this->getCurrentPath();
return $this->getDirectoryContents(
new DirectoryIterator($currentPath)
);
}
return $this->findFiles();
}
protected function getAssetsPath()
{
return $this->theme->getPath().'/assets';
}
protected function getThemeFileUrl($path)
{
return Url::asset('themes/'.$this->theme->getDirName().'/assets'.$path);
}
public function getCurrentRelativePath()
{
$path = $this->getSession('currentPath', '/');
if (!$this->validatePath($path)) {
return null;
}
if ($path == '.') {
return null;
}
return ltrim($path, '/');
}
protected function getCurrentPath()
{
$assetsPath = $this->getAssetsPath();
$path = $assetsPath.'/'.$this->getCurrentRelativePath();
if (!is_dir($path)) {
return $assetsPath;
}
return $path;
}
protected function getRelativePath($path)
{
$prefix = $this->getAssetsPath();
if (substr($path, 0, strlen($prefix)) == $prefix) {
$path = substr($path, strlen($prefix));
}
return $path;
}
protected function getFullPath($path)
{
return $this->getAssetsPath().'/'.ltrim($path, '/');
}
protected function validatePath($path)
{
if (!preg_match('/^[0-9a-z\.\s_\-\/]+$/i', $path)) {
return false;
}
if (strpos($path, '..') !== false || strpos($path, './') !== false) {
return false;
}
return true;
}
protected function validateName($name)
{
if (!preg_match('/^[0-9a-z\.\s_\-]+$/i', $name)) {
return false;
}
if (strpos($name, '..') !== false) {
return false;
}
return true;
}
protected function getDirectoryContents($dir)
{
$editableAssetTypes = Asset::getEditableExtensions();
$result = [];
$files = [];
foreach ($dir as $node) {
if (substr($node->getFileName(), 0, 1) == '.') {
continue;
}
if ($node->isDir() && !$node->isDot()) {
$result[$node->getFilename()] = (object)[
'type' => 'directory',
'path' => File::normalizePath($this->getRelativePath($node->getPathname())),
'name' => $node->getFilename(),
'editable' => false
];
}
elseif ($node->isFile()) {
$files[] = (object)[
'type' => 'file',
'path' => File::normalizePath($this->getRelativePath($node->getPathname())),
'name' => $node->getFilename(),
'editable' => in_array(strtolower($node->getExtension()), $editableAssetTypes)
];
}
}
// Sort directories & files in alphabetical order
$sortByName = function ($a, $b) {
return strcmp($a->name, $b->name);
};
usort($result, $sortByName);
usort($files, $sortByName);
foreach ($files as $file) {
$result[] = $file;
}
return $result;
}
protected function listDestinationDirectories(&$result, $excludeList, $startDir = null, $level = 0)
{
if ($startDir === null) {
$startDir = $this->getAssetsPath();
$result['/'] = 'assets';
$level = 1;
}
$dirs = new DirectoryIterator($startDir);
foreach ($dirs as $node) {
if (substr($node->getFileName(), 0, 1) == '.') {
continue;
}
if ($node->isDir() && !$node->isDot()) {
$fullPath = $node->getPathname();
$relativePath = $this->getRelativePath($fullPath);
if (array_key_exists($relativePath, $excludeList)) {
continue;
}
$result[$relativePath] = str_repeat('&nbsp;', $level*4).$node->getFilename();
$this->listDestinationDirectories($result, $excludeList, $fullPath, $level+1);
}
}
}
protected function getSearchTerm()
{
return $this->searchTerm !== false ? $this->searchTerm : $this->getSession('search');
}
protected function isSearchMode()
{
return strlen($this->getSearchTerm());
}
protected function getThemeSessionKey($prefix)
{
return $prefix.$this->theme->getDirName();
}
protected function getUpPath()
{
$path = $this->getCurrentRelativePath();
if (!strlen(rtrim(ltrim($path, '/'), '/'))) {
return null;
}
return dirname($path);
}
protected function validateRequestTheme()
{
if ($this->theme->getDirName() != Request::input('theme')) {
throw new ApplicationException(trans('cms::lang.theme.edit.not_match'));
}
}
/**
* Check for valid asset file extension
* @param string
* @return bool
*/
protected function validateFileType($name)
{
$extension = strtolower(File::extension($name));
if (!in_array($extension, $this->assetExtensions)) {
return false;
}
return true;
}
/**
* Process file uploads submitted via AJAX
*
* @return void
* @throws ApplicationException If the file "file_data" wasn't detected in the request or if the file failed to pass validation / security checks
*/
public function onUpload()
{
$this->validateRequestTheme();
$fileName = null;
try {
/**
* @var \Illuminate\Http\UploadedFile
*/
$uploadedFile = Request::file('file_data');
if (!is_object($uploadedFile)) {
return;
}
$fileName = $uploadedFile->getClientOriginalName();
/*
* Check valid upload
*/
if (!$uploadedFile->isValid()) {
throw new ApplicationException(Lang::get('cms::lang.asset.file_not_valid'));
}
/*
* Check file size
*/
$maxSize = UploadedFile::getMaxFilesize();
if ($uploadedFile->getSize() > $maxSize) {
throw new ApplicationException(Lang::get(
'cms::lang.asset.too_large',
['max_size' => File::sizeToString($maxSize)]
));
}
/*
* Check for valid file extensions
*/
if (!$this->validateFileType($fileName)) {
throw new ApplicationException(Lang::get(
'cms::lang.asset.type_not_allowed',
['allowed_types' => implode(', ', $this->assetExtensions)]
));
}
if (strtolower(File::extension($fileName)) === 'svg') {
File::put($uploadedFile->getRealPath(), Svg::extract($uploadedFile->getRealPath()));
}
/*
* Accept the uploaded file
*/
$uploadedFile = $uploadedFile->move($this->getCurrentPath(), $fileName);
File::chmod($uploadedFile->getRealPath());
$response = Response::make('success');
}
catch (Exception $ex) {
$message = $fileName !== null
? Lang::get('cms::lang.asset.error_uploading_file', ['name' => $fileName, 'error' => $ex->getMessage()])
: $ex->getMessage();
$response = Response::make($message);
}
// Override the controller response
$this->controller->setResponse($response);
}
protected function setSearchTerm($term)
{
$this->searchTerm = trim($term);
$this->putSession('search', $this->searchTerm);
}
protected function findFiles()
{
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($this->getAssetsPath(), RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST,
RecursiveIteratorIterator::CATCH_GET_CHILD
);
$editableAssetTypes = Asset::getEditableExtensions();
$searchTerm = Str::lower($this->getSearchTerm());
$words = explode(' ', $searchTerm);
$result = [];
foreach ($iterator as $item) {
if (!$item->isDir()) {
if (substr($item->getFileName(), 0, 1) == '.') {
continue;
}
$path = $this->getRelativePath($item->getPathname());
if ($this->pathMatchesSearch($words, $path)) {
$result[] = (object)[
'type' => 'file',
'path' => File::normalizePath($path),
'name' => $item->getFilename(),
'editable' => in_array(strtolower($item->getExtension()), $editableAssetTypes)
];
}
}
}
return $result;
}
protected function pathMatchesSearch(&$words, $path)
{
foreach ($words as $word) {
$word = trim($word);
if (!strlen($word)) {
continue;
}
if (!Str::contains(Str::lower($path), $word)) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,247 @@
<?php namespace Cms\Widgets;
use App;
use Str;
use Lang;
use Input;
use System\Classes\PluginManager;
use Cms\Classes\ComponentHelpers;
use Backend\Classes\WidgetBase;
/**
* Component list widget.
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class ComponentList extends WidgetBase
{
use \Backend\Traits\CollapsableWidget;
protected $searchTerm = false;
protected $pluginComponentList;
public function __construct($controller, $alias)
{
$this->alias = $alias;
parent::__construct($controller, []);
$this->bindToController();
}
/**
* Renders the widget.
* @return string
*/
public function render()
{
return $this->makePartial('body', [
'data' => $this->getData()
]);
}
/*
* Event handlers
*/
public function onSearch()
{
$this->setSearchTerm(Input::get('search'));
return $this->updateList();
}
/*
* Methods for th internal use
*/
protected function getData()
{
$searchTerm = Str::lower($this->getSearchTerm());
$searchWords = [];
if (strlen($searchTerm)) {
$searchWords = explode(' ', $searchTerm);
}
$pluginManager = PluginManager::instance();
$plugins = $pluginManager->getPlugins();
$this->prepareComponentList();
$items = [];
foreach ($plugins as $plugin) {
$components = $this->getPluginComponents($plugin);
if (!is_array($components)) {
continue;
}
$pluginDetails = $plugin->pluginDetails();
$pluginName = $pluginDetails['name'] ?? Lang::get('system::lang.plugin.unnamed');
$pluginIcon = $pluginDetails['icon'] ?? 'icon-puzzle-piece';
$pluginDescription = $pluginDetails['description'] ?? null;
$pluginClass = get_class($plugin);
$pluginItems = [];
foreach ($components as $componentInfo) {
$className = $componentInfo->className;
$alias = $componentInfo->alias;
$component = App::make($className);
if ($component->isHidden) {
continue;
}
$componentDetails = $component->componentDetails();
$component->alias = '--alias--';
$item = (object)[
'title' => ComponentHelpers::getComponentName($component),
'description' => ComponentHelpers::getComponentDescription($component),
'plugin' => $pluginName,
'propertyConfig' => ComponentHelpers::getComponentsPropertyConfig($component),
'propertyValues' => ComponentHelpers::getComponentPropertyValues($component, $alias),
'className' => get_class($component),
'pluginIcon' => $pluginIcon,
'alias' => $alias,
'name' => $componentInfo->duplicateAlias
? $componentInfo->className
: $componentInfo->alias
];
if ($searchWords && !$this->itemMatchesSearch($searchWords, $item)) {
continue;
}
if (!array_key_exists($pluginClass, $items)) {
$group = (object)[
'title' => $pluginName,
'description' => $pluginDescription,
'pluginClass' => $pluginClass,
'icon' => $pluginIcon,
'items' => []
];
$items[$pluginClass] = $group;
}
$pluginItems[] = $item;
}
usort($pluginItems, function ($a, $b) {
return strcmp($a->title, $b->title);
});
if (isset($items[$pluginClass])) {
$items[$pluginClass]->items = $pluginItems;
}
}
uasort($items, function ($a, $b) {
return strcmp($a->title, $b->title);
});
return $items;
}
protected function prepareComponentList()
{
$pluginManager = PluginManager::instance();
$plugins = $pluginManager->getPlugins();
$componentList = [];
foreach ($plugins as $plugin) {
$components = $plugin->registerComponents();
if (!is_array($components)) {
continue;
}
foreach ($components as $className => $alias) {
$duplicateAlias = false;
foreach ($componentList as $componentInfo) {
if ($componentInfo->alias == $alias) {
$componentInfo->duplicateAlias = true;
$duplicateAlias = true;
}
}
$componentList[] = (object)[
'className' => $className,
'alias' => $alias,
'duplicateAlias' => $duplicateAlias,
'pluginClass' => get_class($plugin)
];
}
}
$this->pluginComponentList = $componentList;
}
protected function getPluginComponents($plugin)
{
$result = [];
$pluginClass = get_class($plugin);
foreach ($this->pluginComponentList as $componentInfo) {
if ($componentInfo->pluginClass == $pluginClass) {
$result[] = $componentInfo;
}
}
return $result;
}
protected function getSearchTerm()
{
return $this->searchTerm !== false ? $this->searchTerm : $this->getSession('search');
}
protected function setSearchTerm($term)
{
$this->searchTerm = trim($term);
$this->putSession('search', $this->searchTerm);
}
protected function updateList()
{
return [
'#' . $this->getId('component-list') => $this->makePartial('items', [
'items' => $this->getData()
])
];
}
protected function itemMatchesSearch(&$words, $item)
{
foreach ($words as $word) {
$word = trim($word);
if (!strlen($word)) {
continue;
}
if (!$this->itemContainsWord($word, $item)) {
return false;
}
}
return true;
}
protected function itemContainsWord($word, $item)
{
if (Str::contains(Str::lower($item->title), $word)) {
return true;
}
if (Str::contains(Str::lower($item->description), $word) && strlen($item->description)) {
return true;
}
if (Str::contains(Str::lower($item->plugin), $word) && strlen($item->plugin)) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,26 @@
<?php namespace Cms\Widgets;
use Backend\Widgets\MediaManager as BackendMediaManager;
/**
* Media Manager widget.
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
* @deprecated Use Backend\Widgets\MediaManager. Remove if year >= 2020.
*/
class MediaManager extends BackendMediaManager
{
/**
* Constructor.
*/
public function __construct()
{
traceLog('Widget Cms\Widgets\MediaManager has been deprecated, use ' . BackendMediaManager::class . ' instead.');
$this->assetPath = '/modules/backend/widgets/mediamanager/assets';
$this->viewPath = base_path('/modules/backend/widgets/mediamanager/partials');
parent::__construct(...func_get_args());
}
}

View File

@@ -0,0 +1,415 @@
<?php namespace Cms\Widgets;
use Str;
use File;
use Input;
use Request;
use Cms\Classes\Theme;
use Backend\Classes\WidgetBase;
/**
* Template list widget.
* This widget displays templates of different types.
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class TemplateList extends WidgetBase
{
const SORTING_FILENAME = 'fileName';
use \Backend\Traits\SelectableWidget;
use \Backend\Traits\CollapsableWidget;
protected $searchTerm = false;
protected $dataSource;
protected $theme;
/**
* @var string object property to use as a title.
*/
public $titleProperty;
/**
* @var array a list of object properties to use in the description area.
* The array should include the property names and corresponding titles:
* ['url'=>'URL']
*/
public $descriptionProperties = [];
/**
* @var string object property to use as a description.
*/
public $descriptionProperty;
/**
* @var string Message to display when there are no records in the list.
*/
public $noRecordsMessage = 'cms::lang.template.no_list_records';
/**
* @var string Message to display when the Delete button is clicked.
*/
public $deleteConfirmation = 'cms::lang.template.delete_confirm';
/**
* @var string Specifies the item type.
*/
public $itemType;
/**
* @var string Extra CSS class name to apply to the control.
*/
public $controlClass;
/**
* @var string A list of file name patterns to suppress / hide.
*/
public $ignoreDirectories = [];
/**
* @var boolean Defines sorting properties.
* The sorting feature is disabled if there are no sorting properties defined.
*/
public $sortingProperties = [];
/*
* Public methods
*/
public function __construct($controller, $alias, callable $dataSource)
{
$this->alias = $alias;
$this->dataSource = $dataSource;
$this->theme = Theme::getEditTheme();
$this->selectionInputName = 'template';
$this->collapseSessionKey = $this->getThemeSessionKey('groups');
parent::__construct($controller, []);
if (!Request::isXmlHttpRequest()) {
$this->resetSelection();
}
$configFile = 'config_' . snake_case($alias) .'.yaml';
$config = $this->makeConfig($configFile);
foreach ($config as $field => $value) {
if (property_exists($this, $field)) {
$this->$field = $value;
}
}
$this->bindToController();
}
/**
* Renders the widget.
* @return string
*/
public function render()
{
$toolbarClass = Str::contains($this->controlClass, 'hero') ? 'separator' : null;
$this->vars['toolbarClass'] = $toolbarClass;
return $this->makePartial('body', [
'data' => $this->getData()
]);
}
/*
* Event handlers
*/
public function onSearch()
{
$this->setSearchTerm(Input::get('search'));
$this->extendSelection();
return $this->updateList();
}
public function onUpdate()
{
$this->extendSelection();
return $this->updateList();
}
public function onApplySorting()
{
$this->setSortingProperty(Input::get('sortProperty'));
$result = $this->updateList();
$result['#'.$this->getId('sorting-options')] = $this->makePartial('sorting-options');
return $result;
}
//
// Methods for the internal use
//
protected function getData()
{
/*
* Load the data
*/
$items = call_user_func($this->dataSource);
if ($items instanceof \Winter\Storm\Support\Collection) {
$items = $items->all();
}
$items = $this->removeIgnoredDirectories($items);
$items = array_map([$this, 'normalizeItem'], $items);
$this->sortItems($items);
/*
* Apply the search
*/
$filteredItems = [];
$searchTerm = Str::lower($this->getSearchTerm());
if (strlen($searchTerm)) {
/*
* Exact
*/
foreach ($items as $index => $item) {
if ($this->itemContainsWord($searchTerm, $item, true)) {
$filteredItems[] = $item;
unset($items[$index]);
}
}
/*
* Fuzzy
*/
$words = explode(' ', $searchTerm);
foreach ($items as $item) {
if ($this->itemMatchesSearch($words, $item)) {
$filteredItems[] = $item;
}
}
}
else {
$filteredItems = $items;
}
/*
* Group the items
*/
$result = [];
$foundGroups = [];
foreach ($filteredItems as $itemData) {
$pos = strpos($itemData->fileName, '/');
if ($pos !== false) {
$group = substr($itemData->fileName, 0, $pos);
if (!array_key_exists($group, $foundGroups)) {
$newGroup = (object)[
'title' => $group,
'items' => []
];
$foundGroups[$group] = $newGroup;
}
$foundGroups[$group]->items[] = $itemData;
}
else {
$result[] = $itemData;
}
}
// Sort folders by name regardless of the
// selected sorting options.
ksort($foundGroups);
foreach ($foundGroups as $group) {
$result[] = $group;
}
return $result;
}
protected function sortItems(&$items)
{
$sortingProperty = $this->getSortingProperty();
usort($items, function ($a, $b) use ($sortingProperty) {
return strcmp($a->$sortingProperty, $b->$sortingProperty);
});
}
protected function removeIgnoredDirectories($items)
{
if (!$this->ignoreDirectories) {
return $items;
}
$ignoreCache = [];
$items = array_filter($items, function ($item) use (&$ignoreCache) {
$fileName = $item->getBaseFileName();
$dirName = dirname($fileName);
if (isset($ignoreCache[$dirName])) {
return false;
}
foreach ($this->ignoreDirectories as $ignoreDir) {
if (File::fileNameMatch($dirName, $ignoreDir)) {
$ignoreCache[$dirName] = true;
return false;
}
}
return true;
});
return $items;
}
protected function normalizeItem($item)
{
$description = null;
if ($descriptionProperty = $this->descriptionProperty) {
$description = $item->$descriptionProperty;
}
$descriptions = [];
foreach ($this->descriptionProperties as $property => $title) {
if ($item->$property) {
$descriptions[$title] = $item->$property;
}
}
$result = [
'title' => $this->getItemTitle($item),
'fileName' => $item->getFileName(),
'description' => $description,
'descriptions' => $descriptions,
'dragValue' => $this->getItemDragValue($item)
];
foreach ($this->sortingProperties as $property => $name) {
$result[$property] = $item->$property;
}
return (object) $result;
}
protected function getItemDragValue($item)
{
if ($item instanceof \Cms\Classes\Partial) {
return "{% partial '".$item->getBaseFileName()."' %}";
}
if ($item instanceof \Cms\Classes\Content) {
return "{% content '".$item->getBaseFileName()."' %}";
}
if ($item instanceof \Cms\Classes\Page) {
return "{{ '".$item->getBaseFileName()."'|page }}";
}
return '';
}
protected function getItemTitle($item)
{
$titleProperty = $this->titleProperty;
if ($titleProperty) {
return $item->$titleProperty ?: basename($item->getFileName());
}
return basename($item->getFileName());
}
protected function setSearchTerm($term)
{
$this->searchTerm = trim($term);
$this->putSession('search', $this->searchTerm);
}
protected function getSearchTerm()
{
return $this->searchTerm !== false ? $this->searchTerm : $this->getSession('search');
}
protected function updateList()
{
return [
'#'.$this->getId('template-list') => $this->makePartial('items', ['items' => $this->getData()])
];
}
protected function itemMatchesSearch($words, $item)
{
foreach ($words as $word) {
$word = trim($word);
if (!strlen($word)) {
continue;
}
if (!$this->itemContainsWord($word, $item)) {
return false;
}
}
return true;
}
protected function itemContainsWord($word, $item, $exact = false)
{
$operator = $exact ? 'is' : 'contains';
if (strlen($item->title) && Str::$operator(Str::lower($item->title), $word)) {
return true;
}
if (Str::$operator(Str::lower($item->fileName), $word)) {
return true;
}
if (Str::$operator(Str::lower($item->description), $word) && strlen($item->description)) {
return true;
}
foreach ($item->descriptions as $value) {
if (Str::$operator(Str::lower($value), $word) && strlen($value)) {
return true;
}
}
return false;
}
protected function getThemeSessionKey($prefix)
{
return $prefix.$this->theme->getDirName();
}
protected function getSortingProperty()
{
$property = $this->getSession($this->getThemeSessionKey('sorting_property'), self::SORTING_FILENAME);
if (!array_key_exists($property, $this->sortingProperties)) {
return self::SORTING_FILENAME;
}
return $property;
}
protected function setSortingProperty($property)
{
$this->putSession($this->getThemeSessionKey('sorting_property'), $property);
}
}

View File

@@ -0,0 +1,63 @@
.control-assetlist p.no-data{padding:22px;margin:0;color:#666;font-size:14px;text-align:center;font-weight:400;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}
.control-assetlist p.parent,
.control-assetlist ul li{font-weight:300;line-height:150%;margin-bottom:0}
.control-assetlist p.parent.active a,
.control-assetlist ul li.active a{background:#ddd;position:relative}
.control-assetlist p.parent.active a:after,
.control-assetlist ul li.active a:after{position:absolute;height:100%;width:4px;left:0;top:0;background:#2da7c7;display:block;content:' '}
.control-assetlist p.parent a.link,
.control-assetlist ul li a.link{display:block;position:relative;word-wrap:break-word;padding:10px 50px 10px 20px;outline:none;font-weight:400;color:#405261;font-size:14px}
.control-assetlist p.parent a.link:hover,
.control-assetlist ul li a.link:hover,
.control-assetlist p.parent a.link:focus,
.control-assetlist ul li a.link:focus,
.control-assetlist p.parent a.link:active,
.control-assetlist ul li a.link:active{text-decoration:none}
.control-assetlist p.parent a.link span,
.control-assetlist ul li a.link span{display:block}
.control-assetlist p.parent a.link span.description,
.control-assetlist ul li a.link span.description{color:#8f8f8f;font-size:12px;font-weight:400;word-wrap:break-word}
.control-assetlist p.parent a.link span.description strong,
.control-assetlist ul li a.link span.description strong{color:#405261;font-weight:400}
.control-assetlist p.parent.directory a.link,
.control-assetlist ul li.directory a.link,
.control-assetlist p.parent.parent a.link,
.control-assetlist ul li.parent a.link{padding-left:40px}
.control-assetlist p.parent.directory a.link:after,
.control-assetlist ul li.directory a.link:after,
.control-assetlist p.parent.parent a.link:after,
.control-assetlist ul li.parent a.link:after{display:block;position:absolute;width:10px;height:10px;top:10px;left:20px;font-family:"Font Awesome 6 Free";font-weight:900;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-style:normal;font-variant:normal;text-rendering:auto;content:"\f07b";color:#a1aab1;font-size:14px}
.control-assetlist p.parent.parent a.link,
.control-assetlist ul li.parent a.link{padding-left:41px;background-color:#fff;word-wrap:break-word}
.control-assetlist p.parent.parent a.link:before,
.control-assetlist ul li.parent a.link:before{content:'';display:block;position:absolute;left:0;top:0;width:100%;height:1px;background:#ecf0f1}
.control-assetlist p.parent.parent a.link:after,
.control-assetlist ul li.parent a.link:after{font-size:13px;color:#103141;width:18px;height:18px;top:11px;left:22px;opacity:0.5;filter:alpha(opacity=50);font-family:"Font Awesome 6 Free";font-weight:900;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-style:normal;font-variant:normal;text-rendering:auto;content:"\f053"}
.control-assetlist p.parent a.link:hover{background:#ddd !important}
.control-assetlist p.parent a.link:hover:after{opacity:1;filter:alpha(opacity=100)}
.control-assetlist p.parent a.link:hover:before{display:none}
.control-assetlist ul{padding:0;margin:0}
.control-assetlist ul li{font-weight:300;line-height:150%;position:relative;list-style:none}
.control-assetlist ul li.active a.link,
.control-assetlist ul li a.link:hover{background:#ddd}
.control-assetlist ul li.active a.link{position:relative}
.control-assetlist ul li.active a.link:after{position:absolute;height:100%;width:4px;left:0;top:0;background:#2da7c7;display:block;content:' '}
.control-assetlist ul li div.controls{position:absolute;right:45px;top:10px}
.control-assetlist ul li div.controls .dropdown{width:14px;height:21px}
.control-assetlist ul li div.controls .dropdown.open a.control{display:block!important}
.control-assetlist ul li div.controls .dropdown.open a.control:before{visibility:visible;display:block}
.control-assetlist ul li div.controls a.control{color:#405261;font-size:14px;visibility:hidden;overflow:hidden;width:14px;height:21px;display:none;text-decoration:none;cursor:pointer;opacity:0.5;filter:alpha(opacity=50)}
.control-assetlist ul li div.controls a.control:before{visibility:visible;display:block;margin-right:0}
.control-assetlist ul li div.controls a.control:hover{opacity:1;filter:alpha(opacity=100)}
.control-assetlist ul li:hover{background:#ddd}
.control-assetlist ul li:hover div.controls,
.control-assetlist ul li:hover a.control{display:block!important}
.control-assetlist ul li:hover div.controls>a.control,
.control-assetlist ul li:hover a.control>a.control{display:block!important}
.control-assetlist ul li .checkbox{position:absolute;top:-5px;right:-5px}
.control-assetlist ul li .checkbox label{margin-right:0}
.control-assetlist ul li .checkbox label:before{border-color:#ccc}
.control-assetlist div.list-container{position:relative;-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}
.control-assetlist div.list-container.animate ul{-webkit-transition:all 0.2s ease;transition:all 0.2s ease}
.control-assetlist div.list-container.goForward ul{-webkit-transform:translate(-350px,0);-ms-transform:translate(-350px,0);transform:translate(-350px,0)}
.control-assetlist div.list-container.goBackward ul{-webkit-transform:translate(350px,0);-ms-transform:translate(350px,0);transform:translate(350px,0)}

View File

@@ -0,0 +1,184 @@
/*
* Asset list
*/
+function ($) { "use strict";
var AssetList = function (form, alias) {
this.$form = $(form)
this.alias = alias
this.$form.on('ajaxSuccess', $.proxy(this.onAjaxSuccess, this))
this.$form.on('click', 'ul.list > li.directory > a', $.proxy(this.onDirectoryClick, this))
this.$form.on('click', 'ul.list > li.file > a', $.proxy(this.onFileClick, this))
this.$form.on('click', 'p.parent > a', $.proxy(this.onDirectoryClick, this))
this.$form.on('click', 'a[data-control=delete-asset]', $.proxy(this.onDeleteClick, this))
this.$form.on('oc.list.setActiveItem', $.proxy(this.onSetActiveItem, this))
this.setupUploader()
}
// Event handlers
// =================
AssetList.prototype.onDirectoryClick = function(e) {
this.gotoDirectory(
$(e.currentTarget).data('path'),
$(e.currentTarget).parent().hasClass('parent')
)
return false;
}
AssetList.prototype.gotoDirectory = function(path, gotoParent) {
var $container = $('div.list-container', this.$form),
self = this
if (gotoParent !== undefined && gotoParent)
$container.addClass('goBackward')
else
$container.addClass('goForward')
$.wn.stripeLoadIndicator.show()
this.$form.request(this.alias+'::onOpenDirectory', {
data: {
path: path,
d: 0.2
},
complete: function() {
self.updateUi()
$container.trigger('oc.scrollbar.gotoStart')
},
error: function(jqXHR, textStatus, errorThrown) {
$container.removeClass('goForward goBackward')
alert(jqXHR.responseText.length ? jqXHR.responseText : jqXHR.statusText)
}
}).always(function(){
$.wn.stripeLoadIndicator.hide()
})
}
AssetList.prototype.onDeleteClick = function(e) {
var $el = $(e.currentTarget),
self = this
if (!confirm($el.data('confirmation')))
return false
this.$form.request(this.alias+'::onDeleteFiles', {
success: function(data) {
if (data.error !== undefined && $.type(data.error) === 'string' && data.error.length)
$.wn.flashMsg({text: data.error, 'class': 'error'})
},
complete: function() {
self.refresh()
}
})
return false
}
AssetList.prototype.onAjaxSuccess = function() {
this.updateUi()
}
AssetList.prototype.onUploadFail = function(file, message) {
if (file.xhr.status === 413) {
message = 'Server rejected the file because it was too large, try increasing post_max_size';
}
if (!message) {
message = 'Error uploading file'
}
$.wn.alert(message)
this.refresh()
}
AssetList.prototype.onUploadSuccess = function(file, data) {
if (data !== 'success') {
$.wn.alert(data)
}
}
AssetList.prototype.onUploadComplete = function(file, data) {
$.wn.stripeLoadIndicator.hide()
this.refresh()
}
AssetList.prototype.onUploadStart = function() {
$.wn.stripeLoadIndicator.show()
}
AssetList.prototype.onFileClick = function(event) {
var $link = $(event.currentTarget),
$li = $link.parent()
var e = $.Event('open.oc.list', {relatedTarget: $li.get(0), clickEvent: event})
this.$form.trigger(e, this)
if (e.isDefaultPrevented())
return false;
}
AssetList.prototype.onSetActiveItem = function(event, dataId) {
$('ul li.file', this.$form).removeClass('active')
if (dataId)
$('ul li.file[data-id="'+dataId+'"]', this.$form).addClass('active')
}
// Service functions
// =================
AssetList.prototype.updateUi = function() {
$('button[data-control=asset-tools]', self.$form).trigger('oc.triggerOn.update')
}
AssetList.prototype.refresh = function() {
var self = this;
this.$form.request(this.alias+'::onRefresh', {
complete: function() {
self.updateUi()
}
})
}
AssetList.prototype.setupUploader = function() {
var self = this,
$link = $('[data-control="upload-assets"]', this.$form),
uploaderOptions = {
method: 'POST',
url: window.location,
paramName: 'file_data',
previewsContainer: $('<div />').get(0),
clickable: $link.get(0),
timeout: 0,
headers: {}
}
/*
* Add CSRF token to headers
*/
var token = $('meta[name="csrf-token"]').attr('content')
if (token) {
uploaderOptions.headers['X-CSRF-TOKEN'] = token
}
var dropzone = new Dropzone($('<div />').get(0), uploaderOptions)
dropzone.on('error', $.proxy(self.onUploadFail, self))
dropzone.on('success', $.proxy(self.onUploadSuccess, self))
dropzone.on('complete', $.proxy(self.onUploadComplete, self))
dropzone.on('sending', function(file, xhr, formData) {
$.each(self.$form.serializeArray(), function (index, field) {
formData.append(field.name, field.value)
})
xhr.setRequestHeader('X-WINTER-REQUEST-HANDLER', self.alias + '::onUpload')
self.onUploadStart()
})
}
$(document).ready(function(){
new AssetList($('#asset-list-container').closest('form'), $('#asset-list-container').data('alias'))
})
}(window.jQuery);

View File

@@ -0,0 +1,236 @@
@import "../../../../../backend/assets/less/core/boot.less";
.control-assetlist {
p.no-data {
padding: 22px;
margin: 0;
color: @color-filelist-norecords-text;
font-size: 14px;
text-align: center;
font-weight: 400;
.border-radius(@border-radius-base);
}
p.parent, ul li {
font-weight: 300;
line-height: 150%;
margin-bottom: 0;
&.active a {
background: @color-list-active;
position: relative;
&:after {
position: absolute;
height: 100%;
width: 4px;
left: 0;
top: 0;
background: @color-list-active-border;
display: block;
content: ' ';
}
}
a.link {
display: block;
position: relative;
word-wrap: break-word;
padding: 10px 50px 10px 20px;
outline: none;
font-weight: 400;
color: @color-text-title;
font-size: 14px;
&:hover, &:focus, &:active {text-decoration: none;}
span {
display: block;
&.description {
color: @color-text-description;
font-size: 12px;
font-weight: 400;
word-wrap: break-word;
strong {
color: @color-text-title;
font-weight: 400;
}
}
}
}
&.directory, &.parent {
a.link {
padding-left: 40px;
&:after {
display: block;
position: absolute;
width: 10px;
height: 10px;
top: 10px;
left: 20px;
.icon(@folder);
color: @color-list-icon;
font-size: 14px;
}
}
}
&.parent {
a.link {
padding-left: 41px;
background-color: @color-list-parent-bg;
word-wrap: break-word;
&:before {
content: '';
height: 1px;
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 1px;
background: #ecf0f1;
}
&:after {
font-size: 13px;
color: @color-list-nav-arrow;
width: 18px;
height: 18px;
top: 11px;
left: 22px;
.opacity(0.5);
.icon(@chevron-left);
}
}
}
}
p.parent a.link:hover {
background: @color-list-active!important;
&:after {
.opacity(1);
}
&:before {
display: none;
}
}
ul {
padding: 0;
margin: 0;
li {
font-weight: 300;
line-height: 150%;
position: relative;
list-style: none;
&.active a.link, a.link:hover {background: @color-list-active;}
&.active a.link {
position: relative;
&:after {
position: absolute;
height: 100%;
width: 4px;
left: 0;
top: 0;
background: @color-list-active-border;
display: block;
content: ' ';
}
}
div.controls {
position: absolute;
right: 45px;
top: 10px;
.dropdown {
width: 14px;
height: 21px;
&.open a.control {
display: block!important;
&:before {
visibility: visible;
display: block;
}
}
}
a.control {
color: @color-text-title;
font-size: 14px;
visibility: hidden;
overflow: hidden;
width: 14px;
height: 21px;
display: none;
text-decoration: none;
cursor: pointer;
.opacity(0.5);
&:before {
visibility: visible;
display: block;
margin-right: 0;
}
&:hover {
.opacity(1);
}
}
}
&:hover {
background: @color-list-active;
div.controls, a.control {
display: block!important;
> a.control {
display: block!important;
}
}
}
.checkbox {
position: absolute;
top: -5px;
right: -5px;
label {
margin-right: 0;
&:before {
border-color: @color-filelist-cb-border;
}
}
}
}
}
div.list-container {
position: relative;
.translate(0, 0);
&.animate ul {
.transition(all 0.2s ease);
}
&.goForward ul {
.translate(-350px, 0);
}
&.goBackward ul {
.translate(350px, 0);
}
}
}

View File

@@ -0,0 +1,9 @@
<?= $this->makePartial('toolbar') ?>
<div class="layout-row" id="asset-list-container" data-alias="<?= $this->alias ?>">
<div class="layout-cell">
<div class="layout-relative">
<?= $this->makePartial('files', ['data'=>$data]) ?>
</div>
</div>
</div>
<input type="hidden" name="theme" value="<?= e($this->theme->getDirName()) ?>">

View File

@@ -0,0 +1,7 @@
<div class="layout-absolute">
<div class="control-scrollbar" data-control="scrollbar">
<div class="control-assetlist" data-control="assetlist" id="<?= $this->getId('asset-list') ?>">
<?= $this->makePartial('items', ['items'=>$data]) ?>
</div>
</div>
</div>

View File

@@ -0,0 +1,69 @@
<?php
$searchMode = $this->isSearchMode();
if (($upPath = $this->getUpPath()) !== null && !$searchMode):
?>
<p class="parent">
<a href="<?= $upPath ?>" data-path="<?= $upPath ?>" class="link"><?= $this->getCurrentRelativePath() ?></a>
</p>
<?php endif ?>
<div class="list-container animate">
<?php if ($items): ?>
<ul class="list">
<?php foreach ($items as $item):
$dataId = 'asset-'.$this->theme->getDirName().'-'.ltrim($item->path, '/');
?>
<li
class="<?= $item->type ?>"
<?php if ($item->editable): ?>
data-editable
<?php endif ?>
data-item-path="<?= e(ltrim($item->path, '/')) ?>"
data-item-theme="<?= e($this->theme->getDirName()) ?>"
data-item-type="asset" data-id="<?= e($dataId) ?>"
>
<a class="link" target="_blank" data-path="<?= $item->path ?>" href="<?= $this->getThemeFileUrl($item->path) ?>">
<?= e($item->name) ?>
<?php if ($searchMode): ?>
<span class="description">
<?= e(dirname($item->path)) ?>
</span>
<?php endif ?>
</a>
<div class="controls">
<a
href="javascript:;"
class="control icon btn-primary wn-icon-terminal"
title="<?= e(trans('cms::lang.asset.rename')) ?>"
data-control="popup"
data-request-data="renamePath: '<?= e($item->path) ?>'"
data-handler="<?= $this->getEventHandler('onLoadRenamePopup') ?>"
><?= e(trans('cms::lang.asset.rename')) ?></a>
</div>
<input type="hidden" name="file[<?= e($item->path) ?>]" value="0"/>
<div class="checkbox custom-checkbox nolabel">
<?php $cbId = 'cb'.md5($item->path) ?>
<input
id="<?= $cbId ?>"
type="checkbox"
name="file[<?= e($item->path) ?>]"
<?= $this->isItemSelected($item->path) ? 'checked' : null ?>
data-request="<?= $this->getEventHandler('onSelect') ?>"
value="1">
<label for="<?= $cbId ?>"><?= e(trans('cms::lang.asset.select')) ?></label>
</div>
</li>
<?php endforeach ?>
</ul>
<?php else: ?>
<p class="no-data"><?= e(trans($this->noRecordsMessage)) ?></p>
<?php endif ?>
</div>
<?php if (!isset($nested)): ?>
<input type="hidden" name="theme" value="<?= e($this->theme->getDirName()) ?>">
<?php endif ?>

View File

@@ -0,0 +1,41 @@
<?= Form::open([
'data-request'=>$this->getEventHandler('onMove'),
'data-request-success'=>"\$(this).trigger('close.oc.popup')",
'data-stripe-load-indicator'=>1,
'id'=>'asset-move-popup-form'
]) ?>
<div class="modal-header">
<button type="button" class="close" data-dismiss="popup">&times;</button>
<h4 class="modal-title"><?= e(trans('cms::lang.asset.move_popup_title')) ?></h4>
</div>
<div class="modal-body">
<div class="form-group">
<label><?= e(trans('cms::lang.asset.move_destination')) ?></label>
<select
class="form-control custom-select"
name="dest"
data-placeholder="<?= e(trans('backend::lang.media.move_please_select')) ?>">
<option></option>
<?php foreach ($directories as $path => $directory): ?>
<option value="<?= e($path) ?>"><?= e($directory) ?></option>
<?php endforeach ?>
</select>
</div>
<input type="hidden" name="theme" value="<?= e($this->theme->getDirName()) ?>">
<input type="hidden" name="selectedList" value="<?= e($selectedList) ?>">
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary">
<?= e(trans('backend::lang.media.move_button')) ?>
</button>
<button
type="button"
class="btn btn-default"
data-dismiss="popup">
<?= e(trans('backend::lang.form.cancel')) ?>
</button>
</div>
<?= Form::close() ?>

View File

@@ -0,0 +1,43 @@
<?= Form::open([
'data-request'=>$this->getEventHandler('onNewDirectory'),
'data-request-success'=>"\$(this).trigger('close.oc.popup')",
'data-stripe-load-indicator'=>1,
'id'=>'asset-new-dir-popup-form'
]) ?>
<div class="modal-header">
<button type="button" class="close" data-dismiss="popup">&times;</button>
<h4 class="modal-title"><?= e(trans('cms::lang.asset.directory_popup_title')) ?></h4>
</div>
<div class="modal-body">
<div class="form-group">
<label><?= e(trans('cms::lang.asset.directory_name')) ?></label>
<input
type="text"
name="name"
value=""
class="form-control"
autocomplete="off" />
</div>
<input type="hidden" name="theme" value="<?= e($this->theme->getDirName()) ?>">
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary">
<?= e(trans('backend::lang.form.create')) ?>
</button>
<button
type="button"
class="btn btn-default"
data-dismiss="popup">
<?= e(trans('backend::lang.form.cancel')) ?>
</button>
</div>
<script>
setTimeout(
function(){ $('#asset-new-dir-popup-form input.form-control').focus() },
310
)
</script>
<?= Form::close() ?>

View File

@@ -0,0 +1,43 @@
<?= Form::ajax($this->getEventHandler('onApplyName'), [
'success' => "\$el.trigger('close.oc.popup');",
'data-stripe-load-indicator' => 1,
'id' => 'asset-rename-popup-form'
]) ?>
<div class="modal-header">
<button type="button" class="close" data-dismiss="popup">&times;</button>
<h4 class="modal-title"><?= e(trans('backend::lang.media.rename_popup_title')) ?></h4>
</div>
<div class="modal-body">
<div class="form-group">
<label><?= e(trans('backend::lang.media.rename_new_name')) ?></label>
<input
type="text"
name="name"
value="<?= e($name) ?>"
class="form-control"
autocomplete="off" />
</div>
<input type="hidden" name="originalPath" value="<?= e($originalPath) ?>" />
<input type="hidden" name="theme" value="<?= e($this->theme->getDirName()) ?>" />
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary">
<?= e(trans('backend::lang.form.apply')) ?>
</button>
<button
type="button"
class="btn btn-default"
data-dismiss="popup">
<?= e(trans('backend::lang.form.cancel')) ?>
</button>
</div>
<script>
setTimeout(
function(){ $('#asset-rename-popup-form input.form-control').focus() },
310
)
</script>
<?= Form::close() ?>

View File

@@ -0,0 +1,78 @@
<div class="layout-row min-size">
<div class="control-toolbar toolbar-padded">
<!-- Control Panel -->
<div class="toolbar-item" data-calculate-width>
<div class="btn-group">
<div class="dropdown last">
<button type="button" class="btn btn-default wn-icon-plus"
data-control="create-asset"
data-toggle="dropdown"
><?= e(trans('cms::lang.sidebar.add')) ?></button>
<ul class="dropdown-menu offset-left" role="menu" data-dropdown-title="<?= e(trans('cms::lang.asset.drop_down_add_title')) ?>">
<li role="presentation"><a role="menuitem" tabindex="-1" href="javascript:;" data-control="create-template" class="wn-icon-file-text-o"><?= e(trans('cms::lang.asset.create_file')) ?></a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="javascript:;" data-control="upload-assets" class="wn-icon-upload"><?= e(trans('cms::lang.asset.upload_files')) ?></a></li>
<li role="presentation" class="divider"></li>
<li role="presentation"><a
role="menuitem"
tabindex="-1"
href="javascript:;"
class="wn-icon-folder-o"
data-control="popup"
data-handler="<?= $this->getEventHandler('onLoadNewDirPopup') ?>"
><?= e(trans('cms::lang.asset.create_directory')) ?></a></li>
</ul>
</div>
<div class="dropdown hide"
id="<?= $this->getId('tools-button') ?>"
data-trigger-action="show"
data-trigger="<?= '#'.$this->getId('asset-list') ?> input[type=checkbox]"
data-trigger-condition="checked">
<button type="button" class="btn btn-default empty wn-icon-wrench last"
data-toggle="dropdown"
data-control="asset-tools"
></button>
<ul class="dropdown-menu" role="menu" data-dropdown-title="<?= e(trans('cms::lang.asset.drop_down_operation_title')) ?>">
<li role="presentation"><a
role="menuitem"
tabindex="-1"
href="javascript:;"
data-control="delete-asset"
data-confirmation="<?= e(trans($this->deleteConfirmation)) ?>"
class="wn-icon-trash-o"
><?= e(trans('cms::lang.asset.delete')) ?></a></li>
<li role="presentation"><a
role="menuitem"
tabindex="-1"
href="javascript:;"
class="wn-icon-angle-double-right"
data-control="popup"
data-handler="<?= $this->getEventHandler('onLoadMovePopup') ?>"
><?= e(trans('cms::lang.asset.move')) ?></a></li>
</ul>
</div>
</div>
</div>
<!-- Asset Search -->
<div class="relative toolbar-item loading-indicator-container size-input-text">
<input
type="text"
name="search"
value="<?= e($this->getSearchTerm()) ?>"
class="form-control icon search" autocomplete="off"
placeholder="<?= e(trans('cms::lang.sidebar.search')) ?>"
data-track-input
data-load-indicator
data-load-indicator-opaque
data-request-success="$('<?= '#'.$this->getId('tools-button') ?>').trigger('oc.triggerOn.update')"
data-request="<?= $this->getEventHandler('onSearch') ?>"
/>
</div>
</div>
</div>

View File

@@ -0,0 +1,8 @@
<?= $this->makePartial('toolbar') ?>
<div class="layout-row">
<div class="layout-cell">
<div class="layout-relative">
<?= $this->makePartial('components', ['data'=>$data]) ?>
</div>
</div>
</div>

View File

@@ -0,0 +1,39 @@
<div class="components subitems">
<div class="layout">
<div class="layout-row">
<?php
$count = count($components);
?>
<?php foreach ($components as $index => $component): ?>
<?php if ($index > 0 && ($index % 2) == 0): ?>
</div>
<?php if ($index == ($count - 1)): ?>
</div>
<div class="layout single">
<?php endif ?>
<div class="layout-row">
<?php endif ?>
<div class="layout-cell" data-control="dragcomponent" data-component>
<div class="layout-relative">
<span class="name"><?= e($component->title) ?></span>
<span class="description"><?= e($component->description) ?></span>
<span class="alias wn-icon-code"><?= e($component->alias) ?></span>
<input type="hidden" name="component_properties[]" data-inspector-values value="<?= e($component->propertyValues) ?>">
<input type="hidden" data-inspector-config value="<?= e($component->propertyConfig) ?>">
<input type="hidden" data-inspector-class value="<?= $component->className ?>">
<input type="hidden" data-component-icon value="<?= 'wn-'.e($component->pluginIcon) ?>">
<input type="hidden" data-component-default-alias value="<?= e($component->alias) ?>">
<input type="hidden" data-component-name value="<?= e($component->name) ?>">
<input type="hidden" name="component_names[]" value="">
<input type="hidden" name="component_aliases[]" value="">
<a href="#" class="remove">&times;</a>
</div>
</div>
<?php endforeach ?>
</div>
</div>
</div>

View File

@@ -0,0 +1,11 @@
<div class="layout-absolute">
<div class="control-scrollbar" data-control="scrollbar">
<div
class="control-filelist component-list"
data-control="filelist"
data-group-status-handler="<?= $this->getEventHandler('onSetCollapseStatus') ?>"
id="<?= $this->getId('component-list') ?>">
<?= $this->makePartial('items', ['items'=>$data]) ?>
</div>
</div>
</div>

View File

@@ -0,0 +1,16 @@
<?php if ($items): ?>
<ul>
<?php foreach ($items as $item): ?>
<li class="group" data-status="<?= $this->getCollapseStatus($item->pluginClass, false) ? 'expanded' : 'collapsed' ?>" data-group-id="<?= e($item->pluginClass) ?>">
<div class="group">
<h4><a href="#"><?= e(trans($item->title)) ?></a></h4>
<i class="<?= e($item->icon) ?>"></i>
<span class="description"><?= e(trans($item->description)) ?></span>
</div>
<?= $this->makePartial('component_list', ['components'=>$item->items]) ?>
</li>
<?php endforeach ?>
</ul>
<?php else: ?>
<p class="no-data"><?= e(trans('cms::lang.component.no_records')) ?></p>
<?php endif ?>

View File

@@ -0,0 +1,16 @@
<div class="layout-row min-size">
<div class="control-toolbar toolbar-padded">
<!-- Component Search -->
<div class="relative toolbar-item loading-indicator-container size-input-text">
<input placeholder="<?= e(trans('cms::lang.sidebar.search')) ?>" type="text" name="search" value="<?= e($this->getSearchTerm()) ?>"
class="form-control icon search" autocomplete="off"
data-track-input
data-load-indicator
data-load-indicator-opaque
data-request="<?= $this->getEventHandler('onSearch') ?>"
/>
</div>
</div>
</div>

View File

@@ -0,0 +1,8 @@
<?= $this->makePartial('toolbar') ?>
<div class="layout-row">
<div class="layout-cell">
<div class="layout-relative">
<?= $this->makePartial('templates', ['data' => $data]) ?>
</div>
</div>
</div>

View File

@@ -0,0 +1,59 @@
<?php if ($items): ?>
<ul>
<?php foreach ($items as $item): ?>
<?php if (property_exists($item, 'items')): ?>
<?php if ($item->items): ?>
<li class="group" data-status="<?= $this->getCollapseStatus($item->title, false) ? 'expanded' : 'collapsed' ?>">
<h4><a href="javascript:;"><?= e($item->title) ?></a></h4>
<?= $this->makePartial('items', ['items'=>$item->items, 'nested'=>true]) ?>
</li>
<?php endif ?>
<?php else: ?>
<?php
$dataId = $this->itemType.'-'.$this->theme->getDirName().'-'.$item->fileName;
?>
<li
class="item"
data-item-path="<?= e($item->fileName) ?>"
data-item-theme="<?= e($this->theme->getDirName()) ?>"
data-item-type="<?= $this->itemType ?>"
data-id="<?= e($dataId) ?>">
<a href="javascript:;"
data-control="dragvalue"
data-text-value="<?= $item->dragValue ?>">
<span class="title"><?= e($item->title) ?></span>
<span class="description" title="<?= e($item->description) ?>">
<?php foreach ($item->descriptions as $title => $value): ?>
<?php if (strlen($value)): ?>
<?= e($title) ?>: <strong><?= e($value) ?></strong>
<?php endif ?>
<?php endforeach ?>
<?= e($item->description) ?>
</span>
<span class="borders"></span>
</a>
<input type="hidden" name="template[<?= e($item->fileName) ?>]" value="0" />
<div class="checkbox custom-checkbox nolabel">
<?php $cbId = 'cb' . md5($this->itemType . '/' . $item->fileName) ?>
<input
id="<?= $cbId ?>"
type="checkbox"
name="template[<?= e($item->fileName) ?>]"
<?= $this->isItemSelected($item->fileName) ? 'checked' : null ?>
data-request="<?= $this->getEventHandler('onSelect') ?>"
value="1">
<label for="<?= $cbId ?>">Select</label>
</div>
</li>
<?php endif ?>
<?php endforeach ?>
</ul>
<?php else: ?>
<p class="no-data"><?= e(trans($this->noRecordsMessage)) ?></p>
<?php endif ?>
<?php if (!isset($nested)): ?>
<input type="hidden" name="theme" value="<?= e($this->theme->getDirName()) ?>">
<?php endif ?>

View File

@@ -0,0 +1,12 @@
<?php foreach ($this->sortingProperties as $propertyName => $propertyTitle): ?>
<li
role="presentation"
<?php if ($this->getSortingProperty() == $propertyName): ?>
class="active"
<?php endif ?>
>
<a role="menuitem" tabindex="-1" href="javascript:;" data-stripe-load-indicator data-request="<?= $this->getEventHandler('onApplySorting') ?>" data-request-data="sortProperty: '<?= e($propertyName) ?>'">
<?= e(trans($propertyTitle)) ?>
</a>
</li>
<?php endforeach ?>

View File

@@ -0,0 +1,11 @@
<div class="layout-absolute">
<div class="control-scrollbar" data-control="scrollbar">
<div
class="control-filelist <?= $this->controlClass ?>"
data-control="filelist"
data-group-status-handler="<?= $this->getEventHandler('onSetCollapseStatus') ?>"
id="<?= $this->getId('template-list') ?>">
<?= $this->makePartial('items', ['items' => $data]) ?>
</div>
</div>
</div>

View File

@@ -0,0 +1,51 @@
<div class="layout-row min-size">
<div class="control-toolbar toolbar-padded <?= $toolbarClass ?>">
<!-- Control Panel -->
<div class="toolbar-item" data-calculate-width>
<div class="btn-group">
<button
type="button"
class="btn btn-default wn-icon-plus <?= !$this->sortingProperties ? 'last' : null ?>"
data-control="create-template"><?= e(trans('cms::lang.sidebar.add')) ?></button>
<?php if ($this->sortingProperties): ?>
<div class="dropdown">
<button
type="button"
class="btn btn-default empty wn-icon-sort-alpha-asc"
data-toggle="dropdown"></button>
<ul
class="dropdown-menu offset-left"
data-dropdown-title="<?= e(trans('cms::lang.template.order_by')) ?>"
id="<?= $this->getId('sorting-options') ?>"
role="menu">
<?= $this->makePartial('sorting-options') ?>
</ul>
</div>
<?php endif?>
<button type="button" class="btn btn-danger empty wn-icon-trash-o hide"
id="<?= $this->getId('delete-button') ?>"
data-control="delete-template"
data-confirmation="<?= e(trans($this->deleteConfirmation)) ?>"
data-trigger-action="show"
data-trigger="<?= '#'.$this->getId('template-list') ?> input[type=checkbox]"
data-trigger-condition="checked"></button>
</div>
</div>
<!-- Template Search -->
<div class="relative toolbar-item loading-indicator-container size-input-text">
<input placeholder="<?= e(trans('cms::lang.sidebar.search')) ?>" type="text" name="search" value="<?= e($this->getSearchTerm()) ?>"
class="form-control icon search" autocomplete="off"
data-track-input
data-load-indicator
data-load-indicator-opaque
data-request-success="$('<?= '#'.$this->getId('delete-button') ?>').trigger('oc.triggerOn.update')"
data-request="<?= $this->getEventHandler('onSearch') ?>"
/>
</div>
</div>
</div>