[October](https://octobercms.com) is a Content Management System (CMS) and web platform whose sole purpose is to make your development workflow simple again. It was born out of frustration with existing systems. We feel building websites has become a convoluted and confusing process that leaves developers unsatisfied. We want to turn you around to the simpler side and get back to basics.
October's mission is to show the world that web development is not rocket science.
[](https://octobercms.com/)
[](https://docs.octobercms.com/)
[](https://octobercms.com/changelog)
[](./LICENSE.md)
> *Please note*: October CMS is open source and every new account includes a complimentary license for the first year. After that, a license is required to continue receiving updates and access the Marketplace ecosystem.
## Installing October
Instructions on how to install October can be found at the [installation guide](https://docs.octobercms.com/3.x/setup/installation.html).
### Quick Start Installation
If you have composer installed, run this in your terminal to install October CMS from command line. This will place the files in a directory named **myoctober**.
composer create-project october/october myoctober
If you plan on using a database, run this command inside the application directory.
php artisan october:install
## Learning October
The best place to learn October CMS is by [reading the documentation](https://docs.octobercms.com) or [following some tutorials](https://octobercms.com/support/articles/tutorials).
You may also watch this [introductory video](https://www.youtube.com/watch?v=yLZTOeOS7wI). Make sure to check out our [official YouTube channel](https://www.youtube.com/c/OctoberCMSOfficial). There is also the excellent video series by [Watch & Learn](https://watch-learn.com/series/making-websites-with-october-cms).
For code examples of building with October CMS, visit the [RainLab Plugin Suite](https://github.com/rainlab) or the [October Demos Repo](https://github.com/octoberdemos).
## Coding Standards
Please follow the following guides and code standards:
* [PSR 4 Coding Standards](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md)
* [PSR 2 Coding Style Guide](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)
* [PSR 1 Coding Standards](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md)
## Security Vulnerabilities
Please review [our security policy](https://github.com/octobercms/october/security/policy) on how to report security vulnerabilities.
## Development Team
October CMS was founded in 2014 by Alexey Bobkov and Sam Georges. Today it is supported by a worldwide network of [partners](https://octobercms.com/partners) and contributors.
## Foundation library
The CMS uses [Laravel](https://laravel.com) as a foundation PHP framework.
## Contact
For announcements and updates:
* [Contact Us Page](https://octobercms.com/contact)
* [Follow us on Twitter](https://twitter.com/octobercms)
* [Like us on Facebook](https://facebook.com/octobercms)
To chat or hang out:
* [Join us on Discord](https://discord.gg/gEKgwSZ)
## License
The October CMS platform is licensed software, see [End User License Agreement](./LICENSE.md) (EULA) for more details.
================================================
FILE: app/Provider.php
================================================
make(Illuminate\Contracts\Console\Kernel::class);
$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArgvInput,
new Symfony\Component\Console\Output\ConsoleOutput
);
/*
|--------------------------------------------------------------------------
| Shutdown The Application
|--------------------------------------------------------------------------
|
| Once Artisan has finished running. We will fire off the shutdown events
| so that any final work may be done by the application before we shut
| down the process. This is the last thing to happen to the request.
|
*/
$kernel->terminate($input, $status);
exit($status);
================================================
FILE: bootstrap/app.php
================================================
withRouting(
// web: __DIR__.'/../routes/web.php',
// commands: __DIR__.'/../routes/console.php',
// health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
//
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
================================================
FILE: bootstrap/autoload.php
================================================
Application not ready
If you deploy build artifacts, ensure the vendor/ directory is included or that your deploy step runs Composer before switching traffic.
If you are using the Deploy plugin for this application, use the Check Beacon
function now to verify the deployment.
HTML;
exit(1);
}
================================================
FILE: bootstrap/providers.php
================================================
env('APP_NAME', 'October CMS'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
| You can create a CMS page with route "/error" to set the contents
| of this page. Otherwise a default error page is shown.
|
*/
'debug' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| your application so that it is used when running Artisan tasks.
|
*/
'url' => env('APP_URL', 'http://localhost'),
'asset_url' => env('ASSET_URL'),
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by the translation service provider. You are free to set this value
| to any of the locales which will be supported by the application.
|
*/
'locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is used by the Illuminate encrypter service and should be set
| to a random, 32 character string, otherwise these encrypted strings
| will not be safe. Please do this before deploying an application!
|
*/
'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY'),
'previous_keys' => [
...array_filter(
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
),
],
/*
|--------------------------------------------------------------------------
| Maintenance Mode Driver
|--------------------------------------------------------------------------
|
| These configuration options determine the driver used to determine and
| manage Laravel's "maintenance mode" status. The "cache" driver will
| allow maintenance mode to be controlled across multiple machines.
|
| Supported drivers: "file", "cache"
|
*/
'maintenance' => [
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
'store' => env('APP_MAINTENANCE_STORE', 'database'),
],
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. We have gone
| ahead and set this to a sensible default for you out of the box.
|
|-------------------------------- WARNING! --------------------------------
|
| Before you change this value, consider carefully if that is actually
| what you want to do. It is highly recommended that this is always set
| to UTC (as your server & DB timezone should be as well) and instead
| you can use backend.timezone or cms.timezone to set the default
| timezone used to display dates & times.
|
*/
'timezone' => 'UTC',
];
================================================
FILE: config/backend.php
================================================
http://localhost/admin
|
*/
'uri' => env('BACKEND_URI', 'admin'),
/*
|--------------------------------------------------------------------------
| Backend Skin
|--------------------------------------------------------------------------
|
| Specifies the backend skin class to use.
|
*/
'skin' => Backend\Skins\Standard::class,
/*
|--------------------------------------------------------------------------
| Default Branding
|--------------------------------------------------------------------------
|
| The default backend customization settings. These values are all optional
| and remember to set the enabled value to true. Supported values:
|
| - menu_mode: inline, text, tile, collapse, icons, left
| - color_mode: light, dark, auto
| - color_palette: default, classic, oxford, console, valentino, punch
| - login_background_type: color, wallpaper, gradient, ai_images
| - login_background_wallpaper_size: auto, cover
| - login_image_type: autumn_images, custom
|
*/
'brand' => [
'enabled' => false,
'app_name' => env('APP_NAME', 'October CMS'),
'tagline' => 'Administration Panel',
'menu_mode' => 'icons',
'color_mode' => 'light',
'color_palette' => 'default',
'logo_path' => '~/app/assets/images/logo.png',
'favicon_path' => '~/app/assets/images/favicon.png',
'menu_logo_path' => '~/app/assets/images/menu_logo.png',
'dashboard_icon_path' => '~/app/assets/images/dashboard_icon.png',
'stylesheet_path' => '~/app/assets/css/brand_styles.css',
'login_background_type' => 'color',
'login_background_color' => '#fef6eb',
'login_background_wallpaper' => '~/app/assets/images/login_wallpaper.png',
'login_background_wallpaper_size' => 'auto',
'login_image_type' => 'autumn_images',
'login_custom_image' => '~/app/assets/images/loginimage.png',
],
/*
|--------------------------------------------------------------------------
| Turbo Router
|--------------------------------------------------------------------------
|
| Enhance the backend experience using PJAX (push state and AJAX) so when
| you click a link, the page is automatically swapped client-side without
| the cost of a full page load.
|
*/
'turbo_router' => env('BACKEND_TURBO_ROUTER', false),
/*
|--------------------------------------------------------------------------
| Force HTTPS security
|--------------------------------------------------------------------------
|
| Use this setting to force a secure protocol when accessing any backend
| pages, including the authentication pages. This is usually handled by
| web server config, but can be handled by the app for added security.
|
*/
'force_secure' => false,
/*
|--------------------------------------------------------------------------
| Remember Login
|--------------------------------------------------------------------------
|
| Define live duration of backend sessions:
|
| true - session never expires (cookie expiration in 5 years)
| false - session has a limited time (see session.lifetime)
| null - the form login displays a checkbox that allow user to choose
|
*/
'force_remember' => null,
/*
|--------------------------------------------------------------------------
| Force Single Session
|--------------------------------------------------------------------------
|
| Use this setting to prevent concurrent sessions. When enabled, backend
| users cannot sign in to multiple devices at the same time. When a new
| sign in occurs, all other sessions for that user are invalidated.
|
*/
'force_single_session' => false,
/*
|--------------------------------------------------------------------------
| Force Mail Setting
|--------------------------------------------------------------------------
|
| Use this setting to remove the option to configure the mail settings
| via the backend. This can be used in developer environments to prevent
| accidentally sending mail via the configured database.
|
*/
'force_mail_setting' => false,
/*
|--------------------------------------------------------------------------
| Password Policy
|--------------------------------------------------------------------------
|
| Specify the password policy for backend administrators.
|
| allow_reset - Allow administrators to reset their own passwords via self service
| min_length - Password minimum length between 4 - 128 chars
| require_uppercase - Require at least one uppercase letter (A–Z)
| require_lowercase - Require at least one lowercase letter (a–z)
| require_number - Require at least one number
| require_nonalpha - Require at least one non-alphanumeric character
| expire_days - Enable password expiration after number of days, false to disable
|
*/
'password_policy' => [
'allow_reset' => true,
'min_length' => 4,
'require_uppercase' => false,
'require_lowercase' => false,
'require_number' => false,
'require_nonalpha' => false,
'expire_days' => false,
],
/*
|--------------------------------------------------------------------------
| Peer Management
|--------------------------------------------------------------------------
|
| When enabled, admin users can manage other users at the same role level
| in addition to their users below their role (direct reports).
|
| When disabled, users can only manage their direct reports and not peers.
|
*/
'user_peer_management' => false,
/*
|--------------------------------------------------------------------------
| Default Avatar
|--------------------------------------------------------------------------
|
| The default avatar used for backend accounts that have no avatar defined.
|
| local - Use a local default image of a user
| gravatar - Use the Gravatar service to generate a unique image
| - Specify a custom URL to a default avatar
|
*/
'default_avatar' => 'gravatar',
/*
|--------------------------------------------------------------------------
| Backend Locale
|--------------------------------------------------------------------------
|
| This acts as the default setting for a backend user's locale. This can
| be changed by the user at any time using the backend preferences.
|
*/
'locale' => env('APP_LOCALE', 'en'),
/*
|--------------------------------------------------------------------------
| Backend Timezone
|--------------------------------------------------------------------------
|
| This acts as the default setting for a backend user's timezone. This can
| be changed by the user at any time using the backend preferences. All
| dates displayed in the backend will be converted to this timezone.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Middleware Group
|--------------------------------------------------------------------------
|
| The name of the middleware group to apply to all backend application routes.
| You may use this to apply your own middleware definition.
|
*/
'middleware_group' => 'web',
];
================================================
FILE: config/broadcasting.php
================================================
env('BROADCASTING_DEFAULT', 'pusher'),
/*
|--------------------------------------------------------------------------
| Broadcast Connections
|--------------------------------------------------------------------------
|
| Here you may define all of the broadcast connections that will be used
| to broadcast events to other systems or over websockets. Samples of
| each available type of connection are provided inside this array.
|
*/
'connections' => [
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_KEY'),
'secret' => env('PUSHER_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => 'eu',
'encrypted' => true,
],
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
],
'log' => [
'driver' => 'log',
],
],
];
================================================
FILE: config/cache.php
================================================
env('CACHE_STORE', 'file'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
*/
'stores' => [
'apc' => [
'driver' => 'apc',
],
'array' => [
'driver' => 'array',
'serialize' => false,
],
'database' => [
'driver' => 'database',
'connection' => env('DB_CACHE_CONNECTION'),
'table' => env('DB_CACHE_TABLE', 'cache'),
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/data'),
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
],
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
'octane' => [
'driver' => 'octane',
],
'failover' => [
'driver' => 'failover',
'stores' => [
'database',
'array',
],
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing a RAM based store such as APC or Memcached, there might
| be other applications utilizing the same cache. So, we'll specify a
| value to get prefixed to all our keys so we can avoid collisions.
|
*/
'prefix' => env('CACHE_PREFIX', 'october-cache-'),
];
================================================
FILE: config/cms.php
================================================
env('ACTIVE_THEME', 'demo'),
/*
|--------------------------------------------------------------------------
| Database Themes
|--------------------------------------------------------------------------
|
| Globally forces all themes to store template changes in the database,
| instead of the file system. If this feature is enabled, changes will
| not be stored in the file system.
|
| false - All theme templates are sourced from the filesystem.
| true - Source theme templates from the database with fallback to the filesystem.
|
*/
'database_templates' => env('CMS_DB_TEMPLATES', false),
/*
|--------------------------------------------------------------------------
| Template Strictness
|--------------------------------------------------------------------------
|
| When enabled, an error is thrown when a component, variable, or attribute
| used does not exist. When disabled, a null value is returned instead.
|
*/
'strict_variables' => env('CMS_STRICT_VARIABLES', false),
'strict_components' => env('CMS_STRICT_COMPONENTS', false),
/*
|--------------------------------------------------------------------------
| Frontend Timezone
|--------------------------------------------------------------------------
|
| This acts as the default setting for a frontend user's timezone used when
| converting dates from the system setting, typically set to UTC.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Template Caching
|--------------------------------------------------------------------------
|
| Specifies the number of minutes the CMS object cache lives. After the interval
| is expired item are re-cached. Note that items are re-cached automatically when
| the corresponding template file is modified.
|
*/
'template_cache_ttl' => 1440,
/*
|--------------------------------------------------------------------------
| Twig Cache
|--------------------------------------------------------------------------
|
| Store a temporary cache of parsed Twig templates in the local filesystem.
|
*/
'enable_twig_cache' => env('CMS_TWIG_CACHE', true),
/*
|--------------------------------------------------------------------------
| Determines if the routing caching is enabled.
|--------------------------------------------------------------------------
|
| If the caching is enabled, the page URL map is saved in the cache. If a page
| URL was changed on the disk, the old URL value could be still saved in the cache.
| To update the cache the clear:cache command should be used. It is recommended
| to disable the caching during the development, and enable it in the production mode.
|
*/
'enable_route_cache' => env('CMS_ROUTE_CACHE', true),
/*
|--------------------------------------------------------------------------
| Page URL Exceptions (Beta)
|--------------------------------------------------------------------------
|
| This configuration can be used to bypass CMS routing logic, such as the
| maintenance mode page and site definition prefix. The key matches a page
| URL match with support for wildcards. The following exception values can
| be configured separated by the pipe character (|).
|
| maintenance - Skip maintenance mode and always allow access to this page
| site - Skip the multisite definition matching engine
|
*/
'url_exceptions' => [
// '/api/*' => 'maintenance',
// '/sitemap.xml' => 'site|maintenance',
],
/*
|--------------------------------------------------------------------------
| Time to live for the URL map.
|--------------------------------------------------------------------------
|
| The URL map used in the CMS page routing process. By default
| the map is updated every time when a page is saved in the backend or when the
| interval, in minutes, specified with the url_cache_ttl parameter expires.
|
*/
'url_cache_ttl' => 60,
/*
|--------------------------------------------------------------------------
| Determines if the asset caching is enabled.
|--------------------------------------------------------------------------
|
| If the caching is enabled, combined assets are cached. If a asset file
| is changed on the disk, the old file contents could be still saved in the cache.
| To update the cache the clear cache command should be used. It is recommended
| to disable the caching during the development, and enable it in the production mode.
|
*/
'enable_asset_cache' => env('CMS_ASSET_CACHE', true),
/*
|--------------------------------------------------------------------------
| Determines if the asset minification is enabled.
|--------------------------------------------------------------------------
|
| If the minification is enabled, combined assets are compressed (minified).
| It is recommended to disable the minification during development, and
| enable it in production mode.
|
*/
'enable_asset_minify' => env('CMS_ASSET_MINIFY', false),
/*
|--------------------------------------------------------------------------
| Check Import Timestamps When Combining Assets
|--------------------------------------------------------------------------
|
| If deep hashing is enabled, the combiner cache will be reset when a change
| is detected on imported files, in addition to those referenced directly.
| This will cause slower page performance. If set to null, deep hashing
| is used when debug mode (app.debug) is enabled.
|
*/
'enable_asset_deep_hashing' => env('CMS_ASSET_DEEP_HASHING', null),
/*
|--------------------------------------------------------------------------
| Site Redirect Policy
|--------------------------------------------------------------------------
|
| Controls the behavior when the root URL is opened without a matched site.
|
| detect - detect the site based on the browser language
| primary - use the primary site
| - use a specific site identifier (id)
|
*/
'redirect_policy' => env('CMS_REDIRECT_POLICY', 'detect'),
/*
|--------------------------------------------------------------------------
| Force Bytecode Invalidation
|--------------------------------------------------------------------------
|
| When using Opcache with opcache.validate_timestamps set to 0 or APC
| with apc.stat set to 0 and Twig cache enabled, clearing the template
| cache won't update the cache, set to true to get around this.
|
*/
'force_bytecode_invalidation' => true,
/*
|--------------------------------------------------------------------------
| Safe Mode
|--------------------------------------------------------------------------
|
| If safe mode is enabled, the PHP code section is disabled in the CMS
| for security reasons. If set to null, safe mode is enabled when
| debug mode (app.debug) is disabled.
|
*/
'safe_mode' => env('CMS_SAFE_MODE', null),
/*
|--------------------------------------------------------------------------
| Middleware Group
|--------------------------------------------------------------------------
|
| The name of the middleware group to apply to all CMS application routes.
| You may use this to apply your own middleware definition, or use some
| of the defaults: web, api
|
*/
'middleware_group' => 'web',
/*
|--------------------------------------------------------------------------
| V1 Security Policy
|--------------------------------------------------------------------------
|
| When using safe mode configuration, the Twig sandbox becomes very strict and
| uses an allow-list to protect calling unapproved methods. Instead, you may
| use V1, which is a more relaxed policy that uses a block-list, it blocks
| most of the unsecure methods but is not as secure as an allow-list.
|
*/
'security_policy_v1' => env('CMS_SECURITY_POLICY_V1', false),
/*
|--------------------------------------------------------------------------
| V1 Exception Policy
|--------------------------------------------------------------------------
|
| When debug mode is off, throwing exceptions in AJAX will display a generic
| message, except for specific exception types such as ApplicationException
| and ValidationException (allow-list). Instead, you may use V1, which is
| a more relaxed policy that allows all messages and blocks common exception
| types (block-list) but may still leak information in rare cases.
|
*/
'exception_policy_v1' => env('CMS_EXCEPTION_POLICY_V1', false),
];
================================================
FILE: config/database.php
================================================
env('DB_CONNECTION', 'mysql'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Here are each of the database connections setup for your application.
| Of course, examples of configuring each database platform that is
| supported by Laravel is shown below to make development simple.
|
|
| All database work in Laravel is done through the PHP PDO facilities
| so make sure you have the driver for your particular database of
| choice installed on your machine before you begin development.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => null,
'journal_mode' => null,
'synchronous' => null,
'transaction_mode' => 'DEFERRED',
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => env('DB_SCHEMA', 'public'),
'sslmode' => env('DB_SSLMODE', 'prefer'),
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DB_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk have not actually be run in the databases.
|
*/
'migrations' => [
'table' => 'migrations',
'update_date_on_publish' => true,
],
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer set of commands than a typical key-value systems
| such as APC or Memcached. Laravel makes it easy to dig right in.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', 'october_database_'),
'persistent' => env('REDIS_PERSISTENT', false),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
],
];
================================================
FILE: config/editor.php
================================================
[
'enabled' => false,
'stylesheet_path' => '~/app/assets/css/editor_styles.css',
'toolbar_buttons' => 'paragraphFormat, paragraphStyle, quote, bold, italic, align, formatOL, formatUL, insertTable, insertSnippet, insertPageLink, insertImage, insertVideo, insertAudio, insertFile, insertHR, fullscreen, html',
'allow_tags' => 'a, abbr, address, area, article, aside, audio, b, base, bdi, bdo, blockquote, br, button, canvas, caption, cite, code, col, colgroup, datalist, dd, del, details, dfn, dialog, div, dl, dt, em, embed, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, hr, i, iframe, img, input, ins, kbd, keygen, label, legend, li, link, main, map, mark, menu, menuitem, meter, nav, noscript, object, ol, optgroup, option, output, p, param, pre, progress, queue, rp, rt, ruby, s, samp, script, style, section, select, small, source, span, strike, strong, sub, summary, sup, table, tbody, td, textarea, tfoot, th, thead, time, title, tr, track, u, ul, var, video, wbr',
'allow_empty_tags' => 'textarea, a, i, iframe, object, video, style, script, .icon, .bi, .fa, .fr-emoticon, .fr-inner, path, line',
'no_wrap_tags' => 'figure, script, style',
'remove_tags' => 'script, style',
'line_breaker_tags' => 'figure, table, hr, iframe, form, dl',
'allow_attrs' => '',
'paragraph_formats' => [
'N' => 'Normal',
'H1' => 'Heading 1',
'H2' => 'Heading 2',
'H3' => 'Heading 3',
'H4' => 'Heading 4',
'PRE' => 'Code',
],
'style_paragraph' => [
'oc-text-bordered' => 'Bordered',
'oc-text-gray' => 'Gray',
'oc-text-spaced' => 'Spaced',
'oc-text-uppercase' => 'Uppercase',
],
'style_inline' => [
'oc-class-code' => 'Code',
'oc-class-highlighted' => 'Highlighted',
'oc-class-transparency' => 'Transparent',
],
'style_link' => [
'oc-link-green' => 'Green',
'oc-link-strong' => 'Strong',
],
'style_table' => [
'oc-dashed-borders' => 'Dashed Borders',
'oc-alternate-rows' => 'Alternate Rows',
],
'style_table_cell' => [
'oc-cell-highlighted' => 'Highlighted',
'oc-cell-thick-border' => 'Thick Border',
],
'style_image' => [
'oc-img-rounded' => 'Rounded',
'oc-img-bordered' => 'Bordered',
],
'editor_options' => [],
],
];
================================================
FILE: config/filesystems.php
================================================
env('FILESYSTEM_DISK', 'local'),
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Here you may configure as many filesystem "disks" as you wish, and you
| may even configure multiple disks of the same driver. Defaults have
| been setup for each driver as an example of the required options.
|
| Supported Drivers: "local", "ftp", "sftp", "s3"
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'serve' => true,
'throw' => false,
'report' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => rtrim(env('APP_URL', ''), '/').'/storage/app/public',
'visibility' => 'public',
'throw' => false,
'report' => false,
],
'uploads' => [
'driver' => 'local',
'root' => storage_path('app/uploads'),
'url' => '/storage/app/uploads',
'visibility' => 'public',
'throw' => false,
'report' => false,
],
'media' => [
'driver' => 'local',
'root' => storage_path('app/media'),
'url' => '/storage/app/media',
'visibility' => 'public',
'throw' => false,
'report' => false,
],
'resources' => [
'driver' => 'local',
'root' => storage_path('app/resources'),
'url' => '/storage/app/resources',
'visibility' => 'public',
'throw' => false,
'report' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'report' => false,
],
],
/*
|--------------------------------------------------------------------------
| Symbolic Links
|--------------------------------------------------------------------------
|
| Here you may configure the symbolic links that will be created when the
| `storage:link` Artisan command is executed. The array keys should be
| the locations of the links and the values should be their targets.
|
| For October CMS, we recommend using the `october:mirror` command instead
|
*/
'links' => [
public_path('storage/app/public') => storage_path('app/public'),
],
];
================================================
FILE: config/hashing.php
================================================
'bcrypt',
/*
|--------------------------------------------------------------------------
| Bcrypt Options
|--------------------------------------------------------------------------
|
| Here you may specify the configuration options that should be used when
| passwords are hashed using the Bcrypt algorithm. This will allow you
| to control the amount of time it takes to hash the given password.
|
*/
'bcrypt' => [
'rounds' => env('BCRYPT_ROUNDS', 10),
],
/*
|--------------------------------------------------------------------------
| Argon Options
|--------------------------------------------------------------------------
|
| Here you may specify the configuration options that should be used when
| passwords are hashed using the Argon algorithm. These will allow you
| to control the amount of time it takes to hash the given password.
|
*/
'argon' => [
'memory' => 1024,
'threads' => 2,
'time' => 2,
],
];
================================================
FILE: config/logging.php
================================================
env('LOG_CHANNEL', 'stack'),
/*
|--------------------------------------------------------------------------
| Deprecations Log Channel
|--------------------------------------------------------------------------
|
| This option controls the log channel that should be used to log warnings
| regarding deprecated PHP and library features. This allows you to get
| your application ready for upcoming major versions of dependencies.
|
*/
'deprecations' => [
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
],
/*
|--------------------------------------------------------------------------
| Log Channels
|--------------------------------------------------------------------------
|
| Here you may configure the log channels for your application. Out of
| the box, Laravel uses the Monolog PHP logging library. This gives
| you a variety of powerful log handlers / formatters to utilize.
|
| Available Drivers: "single", "daily", "slack", "syslog",
| "errorlog", "monolog",
| "custom", "stack"
|
*/
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/system.log'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/system.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => env('LOG_DAILY_DAYS', 14),
'replace_placeholders' => true,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => env('LOG_SLACK_USERNAME', 'October CMS Log'),
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
],
'papertrail' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
],
'processors' => [PsrLogMessageProcessor::class],
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'handler_with' => [
'stream' => 'php://stderr',
],
'formatter' => env('LOG_STDERR_FORMATTER'),
'processors' => [PsrLogMessageProcessor::class],
],
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
'replace_placeholders' => true,
],
'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];
================================================
FILE: config/mail.php
================================================
env('MAIL_MAILER', 'smtp'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations
|--------------------------------------------------------------------------
|
| Here you may configure all of the mailers used by your application plus
| their respective settings. Several examples have been configured for
| you and you are free to add your own as your application requires.
|
| Laravel supports a variety of mail "transport" drivers to be used while
| sending an e-mail. You will specify which one you are using for your
| mailers below. You are free to add additional mailers as required.
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
| "postmark", "resend", "log", "array",
| "failover", "roundrobin"
|
*/
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'scheme' => env('MAIL_SCHEME'),
'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'),
'port' => env('MAIL_PORT', 2525),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
],
'ses' => [
'transport' => 'ses',
],
'postmark' => [
'transport' => 'postmark',
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
// 'client' => [
// 'timeout' => 5,
// ],
],
'resend' => [
'transport' => 'resend',
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -t -i'),
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
'retry_after' => 60,
],
'roundrobin' => [
'transport' => 'roundrobin',
'mailers' => [
'ses',
'postmark',
],
'retry_after' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all e-mails sent by your application to be sent from
| the same address. Here, you may specify a name and address that is
| used globally for all e-mails that are sent by your application.
|
*/
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'noreply@example.tld'),
'name' => env('MAIL_FROM_NAME', 'October CMS'),
],
/*
|--------------------------------------------------------------------------
| Global "To" Address
|--------------------------------------------------------------------------
|
| When testing your application, you may need all e-mails to be sent to
| one developer's address. Here, you may specify a name and address that is
| used globally for all e-mails that are sent by your application.
|
*/
'to' => [
'address' => env('MAIL_TO_ADDRESS', null),
'name' => env('MAIL_TO_NAME', null),
],
];
================================================
FILE: config/media.php
================================================
10,
/*
|--------------------------------------------------------------------------
| Automatically Rename Filenames
|--------------------------------------------------------------------------
|
| When a media file is uploaded, automatically transform its filename to
| something consistent. The "slug" mode will slug the file name for all
| uploads.
|
| Supported: "null", "slug"
|
*/
'auto_rename' => env('MEDIA_AUTO_RENAME', null),
/*
|--------------------------------------------------------------------------
| Clean Vector Files
|--------------------------------------------------------------------------
|
| When a vector file (SVG) file is uploaded, automatically process its
| contents to remove scripts and other potentially dangerous content.
|
*/
'clean_vectors' => true,
/*
|--------------------------------------------------------------------------
| Ignored Files and Patterns
|--------------------------------------------------------------------------
|
| The media manager wil ignore file names and patterns specified here
|
*/
'ignore_files' => ['.svn', '.git', '.DS_Store', '.AppleDouble'],
'ignore_patterns' => ['^\..*'],
/*
|--------------------------------------------------------------------------
| Allowed Extensions
|--------------------------------------------------------------------------
|
| Only allow the following extensions to be uploaded and stored.
|
*/
'default_extensions' => ['jpg', 'jpeg', 'bmp', 'png', 'webp', 'avif', 'gif', 'svg', 'js', 'map', 'ico', 'css', 'less', 'scss', 'ics', 'odt', 'doc', 'docx', 'ppt', 'pptx', 'pdf', 'swf', 'txt', 'ods', 'xls', 'xlsx', 'eot', 'woff', 'woff2', 'ttf', 'flv', 'wmv', 'mp3', 'ogg', 'wav', 'avi', 'mov', 'mp4', 'mpeg', 'webm', 'mkv', 'rar', 'zip'],
/*
|--------------------------------------------------------------------------
| Image Extensions
|--------------------------------------------------------------------------
|
| File extensions corresponding to the Image document type
|
*/
'image_extensions' => ['jpg', 'jpeg', 'bmp', 'png', 'webp', 'avif', 'gif', 'svg'],
/*
|--------------------------------------------------------------------------
| Video Extensions
|--------------------------------------------------------------------------
|
| File extensions corresponding to the Video document type
|
*/
'video_extensions' => ['mp4', 'avi', 'mov', 'mpg', 'mpeg', 'mkv', 'webm'],
/*
|--------------------------------------------------------------------------
| Audio Extensions
|--------------------------------------------------------------------------
|
| File extensions corresponding to the Audio document type
|
*/
'audio_extensions' => ['mp3', 'wav', 'wma', 'm4a', 'ogg'],
];
================================================
FILE: config/multisite.php
================================================
true,
/*
|--------------------------------------------------------------------------
| Multisite Features
|--------------------------------------------------------------------------
|
| Use multisite for the features defined below. Be sure to clear the application
| cache after modifying these settings.
|
| - system_plugin_sites - Plugins can be enabled/disabled per site
| - system_plugin_site_groups - Plugins can be enabled/disabled per site group
| - system_asset_combiner - Asset combiner cache keys are unique to the site
| - cms_maintenance_setting - Maintenance Mode Settings are unique for each site
| - backend_mail_setting - Mail Settings are unique for each site
|
| There are also some known vendor implementations.
|
| - rainlab_googleanalytics_setting - Google Analytics for each site
| - responsiv_campaign_message - Mailing list campaigns for each site
|
*/
'features' => [
'system_plugin_sites' => false,
'system_plugin_site_groups' => false,
'system_asset_combiner' => false,
'cms_maintenance_setting' => false,
'backend_mail_setting' => false,
'dashboard_traffic_statistics' => false,
// Vendor
'rainlab_googleanalytics_setting' => false,
'responsiv_campaign_message' => false,
],
];
================================================
FILE: config/queue.php
================================================
env('QUEUE_CONNECTION', 'sync'),
/*
|--------------------------------------------------------------------------
| Queue Connections
|--------------------------------------------------------------------------
|
| Here you may configure the connection options for every queue backend
| used by your application. An example configuration is provided for
| each backend supported by Laravel. You're also free to add more.
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
| "deferred", "background", "failover", "null"
|
*/
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'connection' => env('DB_QUEUE_CONNECTION'),
'table' => env('DB_QUEUE_TABLE', 'jobs'),
'queue' => env('DB_QUEUE', 'default'),
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
'after_commit' => false,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
'queue' => env('BEANSTALKD_QUEUE', 'default'),
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
'block_for' => 0,
'after_commit' => false,
],
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => null,
'after_commit' => false,
],
'deferred' => [
'driver' => 'deferred',
],
'background' => [
'driver' => 'background',
],
'failover' => [
'driver' => 'failover',
'connections' => [
'database',
'deferred',
],
],
],
/*
|--------------------------------------------------------------------------
| Job Batching
|--------------------------------------------------------------------------
|
| The following options configure the database and table that store job
| batching information. These options can be updated to any database
| connection and table which has been defined by your application.
|
*/
'batching' => [
'database' => env('DB_CONNECTION', 'mysql'),
'table' => 'job_batches',
],
/*
|--------------------------------------------------------------------------
| Failed Queue Jobs
|--------------------------------------------------------------------------
|
| These options configure the behavior of failed queue job logging so you
| can control how and where failed jobs are stored. Laravel ships with
| support for storing failed jobs in a simple file or in a database.
|
| Supported drivers: "database", "database-uuids", "dynamodb", "file", "null"
|
*/
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database'),
'database' => env('DB_CONNECTION', 'mysql'),
'table' => 'failed_jobs',
],
];
================================================
FILE: config/services.php
================================================
[
'key' => env('POSTMARK_API_KEY'),
],
'resend' => [
'key' => env('RESEND_API_KEY'),
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'slack' => [
'notifications' => [
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
],
],
];
================================================
FILE: config/session.php
================================================
env('SESSION_DRIVER', 'file'),
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to expire immediately when the browser is closed then you may
| indicate that via the expire_on_close configuration option.
|
*/
'lifetime' => (int) env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
/*
|--------------------------------------------------------------------------
| Session Encryption
|--------------------------------------------------------------------------
|
| This option allows you to easily specify that all of your session data
| should be encrypted before it's stored. All encryption is performed
| automatically by Laravel and you may use the session like normal.
|
*/
'encrypt' => env('SESSION_ENCRYPT', false),
/*
|--------------------------------------------------------------------------
| Session File Location
|--------------------------------------------------------------------------
|
| When utilizing the "file" session driver, the session files are placed
| on disk. The default storage location is defined here; however, you
| are free to provide another location where they should be stored.
|
*/
'files' => storage_path('framework/sessions'),
/*
|--------------------------------------------------------------------------
| Session Database Connection
|--------------------------------------------------------------------------
|
| When using the "database" or "redis" session drivers, you may specify a
| connection that should be used to manage these sessions. This should
| correspond to a connection in your database configuration options.
|
*/
'connection' => env('SESSION_CONNECTION'),
/*
|--------------------------------------------------------------------------
| Session Database Table
|--------------------------------------------------------------------------
|
| When using the "database" session driver, you may specify the table to
| be used to store sessions. Of course, a sensible default is defined
| for you; however, you're welcome to change this to another table.
|
*/
'table' => env('SESSION_TABLE', 'sessions'),
/*
|--------------------------------------------------------------------------
| Session Cache Store
|--------------------------------------------------------------------------
|
| When using one of the framework's cache driven session backends, you may
| define the cache store which should be used to store the session data
| between requests. This must match one of your defined cache stores.
|
| Affects: "dynamodb", "memcached", "redis"
|
*/
'store' => env('SESSION_STORE'),
/*
|--------------------------------------------------------------------------
| Session Sweeping Lottery
|--------------------------------------------------------------------------
|
| Some session drivers must manually sweep their storage location to get
| rid of old sessions from storage. Here are the chances that it will
| happen on a given request. By default, the odds are 2 out of 100.
|
*/
'lottery' => [2, 100],
/*
|--------------------------------------------------------------------------
| Session Cookie Name
|--------------------------------------------------------------------------
|
| Here you may change the name of the session cookie that is created by
| the framework. Typically, you should not need to change this value
| since doing so does not grant a meaningful security improvement.
|
*/
'cookie' => env('SESSION_COOKIE', 'october_session'),
/*
|--------------------------------------------------------------------------
| Session Cookie Path
|--------------------------------------------------------------------------
|
| The session cookie path determines the path for which the cookie will
| be regarded as available. Typically, this will be the root path of
| your application, but you're free to change this when necessary.
|
*/
'path' => env('SESSION_PATH', '/'),
/*
|--------------------------------------------------------------------------
| Session Cookie Domain
|--------------------------------------------------------------------------
|
| This value determines the domain and subdomains the session cookie is
| available to. By default, the cookie will be available to the root
| domain without subdomains. Typically, this shouldn't be changed.
|
*/
'domain' => env('SESSION_DOMAIN'),
/*
|--------------------------------------------------------------------------
| HTTPS Only Cookies
|--------------------------------------------------------------------------
|
| By setting this option to true, session cookies will only be sent back
| to the server if the browser has a HTTPS connection. This will keep
| the cookie from being sent to you when it can't be done securely.
|
*/
'secure' => env('SESSION_SECURE_COOKIE'),
/*
|--------------------------------------------------------------------------
| HTTP Access Only
|--------------------------------------------------------------------------
|
| Setting this value to true will prevent JavaScript from accessing the
| value of the cookie and the cookie will only be accessible through
| the HTTP protocol. It's unlikely you should disable this option.
|
*/
'http_only' => env('SESSION_HTTP_ONLY', true),
/*
|--------------------------------------------------------------------------
| Same-Site Cookies
|--------------------------------------------------------------------------
|
| This option determines how your cookies behave when cross-site requests
| take place, and can be used to mitigate CSRF attacks. By default, we
| will set this value to "lax" to permit secure cross-site requests.
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
| Supported: "lax", "strict", "none", null
|
*/
'same_site' => env('SESSION_SAME_SITE', 'lax'),
/*
|--------------------------------------------------------------------------
| Partitioned Cookies
|--------------------------------------------------------------------------
|
| Setting this value to true will tie the cookie to the top-level site for
| a cross-site context. Partitioned cookies are accepted by the browser
| when flagged "secure" and the Same-Site attribute is set to "none".
|
*/
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
];
================================================
FILE: config/system.php
================================================
env('LOAD_MODULES'),
/*
|--------------------------------------------------------------------------
| Disable Specified Plugins
|--------------------------------------------------------------------------
|
| Specify plugin codes which will always be disabled in the application.
|
| DISABLE_PLUGINS="October.Demo,RainLab.Blog"
|
*/
'disable_plugins' => env('DISABLE_PLUGINS'),
/*
|--------------------------------------------------------------------------
| Link Policy
|--------------------------------------------------------------------------
|
| Controls how URL links are generated throughout the application.
|
| detect - detect hostname and use the current schema
| secure - detect hostname and force HTTPS schema
| insecure - detect hostname and force HTTP schema
| force - force hostname and schema using app.url config value
|
| By default most links use their fully qualified URLs or reference their
| CDN location. In some cases you may prefer relative links where possible
| if so, set the relative_links value to true.
|
*/
'link_policy' => env('LINK_POLICY', 'detect'),
'relative_links' => env('RELATIVE_LINKS', false),
/*
|--------------------------------------------------------------------------
| System Paths
|--------------------------------------------------------------------------
|
| Specify location to core system paths. Local paths are relative if they
| do not have a leading slash. URLs can be relative to the base application
| URL or you can specify a full path URL.
|
| PLUGINS_PATH="plugins"
| PLUGINS_ASSET_URL="/plugins"
|
| THEMES_PATH="/absolute/path/to/themes"
| THEMES_ASSET_URL="http://localhost/themes"
|
*/
'plugins_path' => env('PLUGINS_PATH'),
'plugins_asset_url' => env('PLUGINS_ASSET_URL'),
'themes_path' => env('THEMES_PATH'),
'themes_asset_url' => env('THEMES_ASSET_URL'),
'storage_path' => env('STORAGE_PATH'),
'cache_path' => env('CACHE_PATH'),
/*
|--------------------------------------------------------------------------
| Default Permission Masks
|--------------------------------------------------------------------------
|
| Specifies a default file and folder permission as a string (eg: "755") for
| created files and directories in the system paths. It is recommended
| to use file as "644" and folder as "755".
|
*/
'default_mask' => [
'file' => env('DEFAULT_FILE_MASK'),
'folder' => env('DEFAULT_FOLDER_MASK'),
],
/*
|--------------------------------------------------------------------------
| Cross Site Request Forgery (CSRF) Protection
|--------------------------------------------------------------------------
|
| If the CSRF protection is enabled, all "postback" & AJAX requests are
| checked for a valid security token.
|
*/
'enable_csrf_protection' => env('ENABLE_CSRF', true),
/*
|--------------------------------------------------------------------------
| Convert Line Endings
|--------------------------------------------------------------------------
|
| Determines if October CMS should convert line endings from the Windows
| style \r\n to the Unix style \n.
|
*/
'convert_line_endings' => env('CONVERT_LINE_ENDINGS', false),
/*
|--------------------------------------------------------------------------
| Cookie Encryption
|--------------------------------------------------------------------------
|
| October CMS encrypts/decrypts cookies by default. You can specify cookies
| that should not be encrypted or decrypted here. This is useful, for
| example, when you want to pass data from frontend to server side backend
| via cookies, and vice versa.
|
*/
'unencrypt_cookies' => env('UNENCRYPT_COOKIES', [
// 'my_cookie',
]),
/*
|--------------------------------------------------------------------------
| Automatically Mirror to Public Directory
|--------------------------------------------------------------------------
|
| Performed after a composer update.
|
| true - automatically mirror asset to the public directory
| false - never mirror assets to public directory
| null - only mirror assets when debug mode is OFF (in production)
|
*/
'auto_mirror_public' => env('AUTO_MIRROR_PUBLIC', false),
/*
|--------------------------------------------------------------------------
| Automatically Rollback Plugins
|--------------------------------------------------------------------------
|
| Attempt to automatically reverse database migrations for a plugin when
| they are uninstalled using composer. This is disabled by default
| to prevent data loss.
|
*/
'auto_rollback_plugins' => env('AUTO_ROLLBACK_PLUGINS', false),
/*
|--------------------------------------------------------------------------
| Base Directory Restriction
|--------------------------------------------------------------------------
|
| Restricts loading backend template and config files to within the base
| directory of the application. For example, when using the symlink option
| in composer for local packages.
|
| Warning: This should never be disabled in production for security reasons.
|
*/
'restrict_base_dir' => env('RESTRICT_BASE_DIR', true),
/*
|--------------------------------------------------------------------------
| Log Deprecation Warnings
|--------------------------------------------------------------------------
|
| This logs deprecation warnings from PHP code, either by the language or
| from developer code, in the event log. This should be set to true in
| development environments to ensure code is maintained and breaking
| changes are fixed before they happen.
|
*/
'log_deprecations' => env('LOG_DEPRECATIONS', false),
];
================================================
FILE: config/view.php
================================================
[
app_path('views'),
],
/*
|--------------------------------------------------------------------------
| Compiled View Path
|--------------------------------------------------------------------------
|
| This option determines where all the compiled Blade templates will be
| stored for your application. Typically, this is within the storage
| directory. However, as usual, you are free to change this value.
|
*/
'compiled' => env(
'VIEW_COMPILED_PATH',
realpath(storage_path('framework/views'))
),
];
================================================
FILE: index.php
================================================
make(\Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
$response->send();
$kernel->terminate($request, $response);
================================================
FILE: modules/backend/ServiceProvider.php
================================================
registerSingletons();
}
/**
* boot the module events.
*/
public function boot()
{
parent::boot('backend');
}
/**
* registerSingletons
*/
protected function registerSingletons()
{
$this->app->singleton('backend.helper', \Backend\Helpers\Backend::class);
$this->app->singleton('backend.roles', \Backend\Classes\RoleManager::class);
$this->app->scoped('backend.auth', fn () => \Backend\Classes\AuthManager::instance());
$this->app->scoped('backend.menu', \Backend\Classes\NavigationManager::class);
$this->app->scoped('backend.widgets', \Backend\Classes\WidgetManager::class);
}
/**
* registerReportWidgets
*/
public function registerReportWidgets()
{
return [
\Backend\ReportWidgets\Welcome::class => [
'label' => "Welcome",
'context' => 'dashboard'
],
];
}
/**
* registerMailTemplates
*/
public function registerMailTemplates()
{
return [
'backend:invite' => 'backend::mail.invite',
'backend:restore' => 'backend::mail.restore',
'backend:contact-form' => 'backend::mail.contact-form',
];
}
/**
* registerNavigation
*/
public function registerNavigation()
{
// return [
// 'dashboard' => [
// 'label' => "Dashboard",
// 'icon' => 'icon-dashboard',
// 'iconSvg' => 'modules/backend/assets/images/dashboard-icon.svg',
// 'url' => Backend::url('backend'),
// 'permissions' => ['dashboard.*', 'dashboard'],
// 'order' => 10
// ]
// ];
}
/**
* registerPermissions
*/
public function registerPermissions()
{
return [
// General
'general.backend' => [
'label' => 'Access the Backend Panel',
'tab' => 'General',
'order' => 200
],
'general.backend.view_offline' => [
'label' => 'View Backend During Maintenance',
'tab' => 'General',
'order' => 300
],
'general.backend.perform_updates' => [
'label' => 'Perform Software Updates',
'tab' => 'General',
'roles' => UserRole::CODE_DEVELOPER,
'order' => 300
],
// Administrators
'admins.manage' => [
'label' => 'Manage Admins',
'tab' => 'Administrators',
'order' => 200
],
'admins.manage.create' => [
'label' => 'Create Admins',
'tab' => 'Administrators',
'order' => 300
],
// 'admins.manage.moderate' => [
// 'label' => 'Moderate Admins',
// 'comment' => 'Manage account suspension and ban admin accounts',
// 'tab' => 'Administrators',
// 'order' => 400
// ],
'admins.manage.other_admins' => [
'label' => 'Manage Other Admins',
'comment' => 'Allow users to reset passwords and update emails.',
'tab' => 'Administrators',
'order' => 700
],
'admins.manage.delete' => [
'label' => 'Delete Admins',
'tab' => 'Administrators',
'order' => 800
],
'admins.roles' => [
'label' => 'Role Permissions',
'comment' => 'Allow users to create new roles and manage roles lower than their highest role.',
'tab' => 'Administrators',
'order' => 500
],
'admins.groups' => [
'label' => 'Team Groups',
'tab' => 'Administrators',
'order' => 600
],
// Preferences
'preferences' => [
'label' => "Manage Backend Preferences",
'tab' => 'Preferences',
'order' => 400
],
'preferences.code_editor' => [
'label' => "Manage Code Editor Preferences",
'tab' => 'Preferences',
'order' => 500
],
// Settings
'settings.customize_backend' => [
'label' => "Customize Backend Styles",
'tab' => 'Settings',
'order' => 400
],
'settings.editor_settings' => [
'label' => 'Global Editor Settings',
'comment' => "Change the global editor preferences.",
'tab' => 'Settings',
'order' => 500
]
];
}
/**
* registerFormWidgets
*/
public function registerFormWidgets()
{
return [
\Backend\FormWidgets\CodeEditor::class => 'codeeditor',
\Backend\FormWidgets\RichEditor::class => 'richeditor',
\Backend\FormWidgets\MarkdownEditor::class => 'markdown',
\Backend\FormWidgets\FileUpload::class => 'fileupload',
\Backend\FormWidgets\Relation::class => 'relation',
\Backend\FormWidgets\DatePicker::class => 'datepicker',
\Backend\FormWidgets\ColorPicker::class => 'colorpicker',
\Backend\FormWidgets\DataTable::class => 'datatable',
\Backend\FormWidgets\RecordFinder::class => 'recordfinder',
\Backend\FormWidgets\Repeater::class => 'repeater',
\Backend\FormWidgets\TagList::class => 'taglist',
\Backend\FormWidgets\NestedForm::class => 'nestedform',
\Backend\FormWidgets\Sensitive::class => 'sensitive',
];
}
/**
* registerFilterWidgets
*/
public function registerFilterWidgets()
{
return [
\Backend\FilterWidgets\Group::class => 'group',
\Backend\FilterWidgets\Date::class => 'date',
\Backend\FilterWidgets\Text::class => 'text',
\Backend\FilterWidgets\Number::class => 'number',
];
}
/**
* registerSettings
*/
public function registerSettings()
{
return [
'branding' => [
'label' => "Branding & Appearance",
'description' => "Customize the administration area such as name, colors and logo.",
'category' => SettingsManager::CATEGORY_BACKEND,
'icon' => 'ph ph-terminal-window',
'class' => \Backend\Models\BrandSetting::class,
'permissions' => ['settings.customize_backend'],
'order' => 400,
'keywords' => 'brand style'
],
'editor' => [
'label' => "Editor Settings",
'description' => "Change the global editor preferences.",
'category' => SettingsManager::CATEGORY_BACKEND,
'icon' => 'icon-code',
'class' => \Backend\Models\EditorSetting::class,
'permissions' => ['settings.editor_settings'],
'order' => 410,
'keywords' => 'html code class style'
],
'administrators' => [
'label' => "Administrators",
'description' => "Manage back-end administrator users, groups and permissions.",
'category' => SettingsManager::CATEGORY_TEAM,
'icon' => 'ph ph-users-three',
'url' => Backend::url('backend/users'),
'permissions' => ['admins.manage'],
'order' => 500
],
'adminroles' => [
'label' => "Role Permissions",
'description' => "Define permissions for administrators based on their role.",
'category' => SettingsManager::CATEGORY_TEAM,
'icon' => 'icon-id-card-1',
'url' => Backend::url('backend/userroles'),
'permissions' => ['admins.roles'],
'order' => 510
],
'admingroups' => [
'label' => "Team Groups",
'description' => "Add administrators to groups used for notifications and features.",
'category' => SettingsManager::CATEGORY_TEAM,
'icon' => 'icon-user-group',
'url' => Backend::url('backend/usergroups'),
'permissions' => ['admins.groups'],
'order' => 520
],
'myaccount' => [
'label' => "My Account",
'description' => "Update your account details such as name, email address and password.",
'category' => SettingsManager::CATEGORY_MYSETTINGS,
'icon' => 'icon-user-account',
'url' => Backend::url('backend/users/myaccount'),
'order' => 600,
'context' => 'mysettings',
'keywords' => "security login"
],
'preferences' => [
'label' => "Backend Preferences",
'description' => "Manage your account preferences such as desired language.",
'category' => SettingsManager::CATEGORY_MYSETTINGS,
'icon' => 'icon-app-window',
'url' => Backend::url('backend/preferences'),
'permissions' => ['preferences'],
'order' => 610,
'context' => 'mysettings'
],
'color_mode' => !BrandSetting::get('show_light_switch') ? null : [
'label' => "Color Mode",
'category' => SettingsManager::CATEGORY_MYSETTINGS,
'icon' => 'icon-adjust',
'url' => 'javascript:;',
'permissions' => ['preferences'],
'attributes' => [
'data-control' => 'color-mode-switcher',
'data-lang-light-mode' => __("Light Mode"),
'data-lang-dark-mode' => __("Dark Mode"),
'data-lang-auto-mode' => __("Auto Mode")
],
'order' => 620,
'context' => 'mysettings'
],
'access_logs' => [
'label' => 'Access Log',
'description' => 'View a list of successful back-end user sign ins.',
'category' => SettingsManager::CATEGORY_LOGS,
'icon' => 'icon-text-lock',
'url' => Backend::url('backend/accesslogs'),
'permissions' => ['utilities.logs'],
'order' => 920
]
];
}
}
================================================
FILE: modules/backend/assets/css/backend/_brand.css
================================================
/*
// Light Mode
// --------------------------------------------------*/
:root, [data-bs-theme="light"] {
/* --bs-border-color: #d7e1ea; */
/* --oc-border-focus: #72809d; */
--bs-emphasis-color: #333333;
--bs-border-color: #c4ced7;
--bs-backdrop-opacity: 0.2;
--oc-border-focus: var(--oc-primary-active-bg);
--oc-highlight-color: white;
/*
// Primary Colors
// --------------------------------------------------*/
--bs-success: var(--bs-green);
--bs-info: var(--bs-blue);
--bs-warning: var(--bs-yellow);
--bs-danger: var(--bs-red);
--oc-accent: #3498db;
--oc-accent-text: #258cd1; /* darker 5% */
--oc-selection: var(--bs-teal);
--oc-selection-rgb: 107, 196, 141;
--oc-selection-text: #39905a; /* darken 20% */
--oc-primary-bg: white;
--oc-primary-color: #5e6d8c;
--oc-primary-border: #cfd7e1;
--oc-primary-hover-bg: #8082f8; /* tint(#6a6cf7, 15%); */
--oc-primary-active-bg: #8889f9; /* tint(#6a6cf7, 20%); */
--bs-secondary: #72809d;
--bs-secondary-color: #72809d;
--oc-secondary-bg: #d7e1ea;
--oc-secondary-hover-bg: #e0e2e4; /* darken(#eeeff0, 5%); */
--oc-secondary-active-bg: #d3d6d8; /* darken(#eeeff0, 10%); */
--oc-color-neutral: #e5a91a;
--oc-color-positive: #95b753;
--oc-color-negative: #cc3300;
/*
// Form Variables
// --------------------------------------------------*/
--oc-form-control-bg: white;
--oc-form-control-disabled-bg: #e9ecef;
--oc-form-control-disabled-color: #899c9d;
--oc-input-translatable-color: #75809b;
--oc-input-translatable-bg: #ebf0fb;
--oc-input-selection-color: #000;
--oc-input-selection-bg: #b5d6fd;
/*
// Component Variables
// --------------------------------------------------*/
--oc-mainnav-color: white;
--oc-mainnav-bg: #2D3134;
--oc-mainnav-icon-color: white;
--oc-sidebar-color: #536061;
--oc-sidebar-bg: #e9edf3;
--oc-sidebar-active-color: #333;
--oc-sidebar-active-bg: white;
--oc-sidebar-active-border: var(--bs-primary);
--oc-sidebar-hover-bg: white;
--oc-editor-bg: #f0f4f8;
--oc-editor-section-color: #333;
--oc-editor-section-bg: #d7e1eA;
--oc-editor-tab-color: #5e6d8c;
--oc-editor-tab-bg: #e9edf3;
--oc-editor-tab-active-color: #2c3e4f;
--oc-editor-tab-active-bg: var(--bs-body-bg);
--oc-document-toolbar-bg: var(--bs-body-bg);
--oc-document-tabs-bg: white;
--oc-document-content-bg: white;
--oc-document-ruler-bg: #d7e1ea;
--oc-document-ruler-color: white;
--oc-document-ruler-tick: #bdc3c7;
--oc-settings-color: #536061;
--oc-settings-bg: #f0f4f8;
--oc-settings-item: white;
--oc-settings-active-color: white;
--oc-settings-active-bg: #6bc48d;
--oc-settings-hover-bg: #dfe7ee;
--oc-toolbar-color: #536061;
--oc-toolbar-bg: white;
/* --oc-toolbar-border: #ecf0f1; */
--oc-toolbar-border: #d7e1ea;
--oc-toolbar-hover-color: black;
--oc-toolbar-hover-bg: rgba(215,225,234,0.7);
--oc-tab-color: #72809d;
--oc-tab-bg: #ffffff;
--oc-tab-active-color: #35425b;
--oc-tab-border: #d7e1eA;
--oc-tab-hover-bg: rgba(215,225,234,0.7);
--oc-dropdown-trigger-border: #bcc3c7;
--oc-dropdown-trigger-color: #536061;
--oc-dropdown-trigger-bg: white;
--oc-dropdown-hover-bg: var(--bs-primary);
--oc-dropdown-hover-color: white;
--oc-dropdown-active-bg: rgba(var(--bs-primary-rgb), .95);
--oc-dropdown-active-color: white;
&,.table {
--bs-table-color: #536061;
--bs-table-bg: white;
--bs-table-striped-bg: #f7f9fb;
/* --bs-table-border-color: #ecf0f1; */
--bs-table-border-color: #d7e1ea;
--bs-table-hover-bg: #fff5cb;
--oc-table-active-bg: #fff0b2; /* darken(#fff5cb, 5%); */
}
&,.pagination {
--bs-pagination-active-bg: var(--bs-primary);
--bs-pagination-active-border-color: var(--bs-primary);
}
&,.breadcrumb {
--bs-breadcrumb-item-padding-x: 0.4rem;
}
&,.modal {
--bs-modal-bg: white;
}
}
/*
// Dark Mode
// --------------------------------------------------*/
[data-bs-theme="dark"] {
--bs-heading-color: #e0e0e0;
--bs-emphasis-color: white;
--bs-border-color: #383a3e;
--bs-backdrop-opacity: 0.5;
--oc-highlight-color: black;
/*
// Primary Colors
// --------------------------------------------------*/
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #41b862;
--bs-teal: #24b58a;
--bs-cyan: #0dcaf0;
--oc-primary-color: rgba(173, 181, 189, 0.85);
--oc-primary-bg: #141515;
--oc-primary-border: #494c50;
--bs-secondary-color: rgba(173, 181, 189, 0.75);
--oc-secondary-bg: #6f6f6f; /* darken(#888, 10%); */
--oc-secondary-hover-bg: #7b7b7b; /* darken(#888, 5%); */
--oc-secondary-active-bg: #888;
/*
// Form Variables
// --------------------------------------------------*/
--oc-form-control-bg: #1e2227;
--oc-form-control-disabled-bg: #343a40;
--oc-input-translatable-color: rgba(173, 181, 189, 0.85);
--oc-input-translatable-bg: #272b2e;
--oc-input-selection-color: #e0e0e0;
--oc-input-selection-bg: #373f47;
/*
// Component Variables
// --------------------------------------------------*/
--oc-sidebar-color: #d7e1eA;
--oc-sidebar-bg: #292a2d;
--oc-sidebar-active-color: white;
--oc-sidebar-active-bg: #424242;
--oc-sidebar-hover-bg: #424242;
--oc-editor-bg: #212529;
--oc-editor-section-bg: #383a3e;
--oc-editor-section-color: #e0e0e0;
--oc-editor-tab-color: #adb5bd;
--oc-editor-tab-bg: #181a1e;
--oc-editor-tab-active-color: #e0e0e0;
--oc-editor-tab-active-bg: #202124;
--oc-document-toolbar-bg: #202124;
--oc-document-tabs-bg: #24262a;
--oc-document-content-bg: #181a1e;
--oc-document-ruler-bg: #353b42;
--oc-document-ruler-color: #1e2227;
--oc-document-ruler-tick: #495057;
--oc-settings-color: #adb5bd;
--oc-settings-bg: #1b1f22;
--oc-settings-item: #212529;
--oc-settings-active-color: white;
--oc-settings-active-bg: #2b3442;
--oc-settings-hover-bg: #2b3442;
--oc-toolbar-color: #adb5bd;
--oc-toolbar-bg: #1e2227;
--oc-toolbar-border: #242a30;
--oc-toolbar-hover-color: white;
--oc-toolbar-hover-bg: #43484e; /* rgba(215,225,234,0.2); */
--oc-tab-color: var(--bs-body-color);
--oc-tab-bg: var(--bs-body-bg);
--oc-tab-active-color: #e0e0e0;
--oc-tab-border: #383a3e;
--oc-tab-hover-bg: rgba(215,225,234,0.2);
--oc-dropdown-trigger-border: #495057;
--oc-dropdown-trigger-color: #e0e0e0;
--oc-dropdown-trigger-bg: #12262c;
&,.table {
--bs-table-color: #adb5bd;
--bs-table-bg: #1e2227;
--bs-table-striped-bg: rgba(0,0,0,0.05);
--bs-table-border-color: #242a30;
--bs-table-hover-bg: #35425b;
--bs-table-hover-color: #e0e0e0;
--oc-table-active-bg: #2c364b; /* darken(#35425b, 5%); */
}
&,.modal {
--bs-modal-bg: #1a1d21;
}
}
================================================
FILE: modules/backend/assets/css/backend/_vars.css
================================================
/*
// Variables
// --------------------------------------------------*/
:root {
--bs-body-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
--oc-brand-primary: var(--bs-primary);
--oc-brand-secondary: var(--bs-secondary);
--oc-brand-success: var(--bs-success);
--oc-brand-info: var(--bs-info);
--oc-brand-warning: var(--bs-warning);
--oc-brand-danger: var(--bs-danger);
--oc-brand-light: var(--bs-light);
--oc-brand-dark: var(--bs-dark);
--oc-body-bg: var(--bs-body-bg);
--oc-text-color: var(--bs-body-color);
--oc-text-muted: var(--bs-secondary-color);
--oc-link-color: var(--bs-link-color);
--oc-link-hover-color: var(--bs-link-hover-color);
--oc-heading-color: var(--bs-heading-color);
--oc-emphasis-color: var(--bs-emphasis-color);
--oc-border-color: var(--bs-border-color);
--oc-popup-bg: var(--bs-modal-bg);
--oc-popup-border: 1px solid rgba(149, 165, 166, 0.2);
--oc-backdrop-opacity: var(--bs-backdrop-opacity);
/*
// "Windex" Z-Index Window Manager
// --------------------------------------------------
//
// Z-Index frequencies:
//
// 0-100 - Primary layer (body / content)
// 100-200 - Primary menus / dropdowns
//
// 300-400 - Secondary layer (full screen)
// 400-500 - Secondary menus / dropdowns
//
// 500-600 - Tertiary layer (popups)
// 600-700 - Tertiary menus / dropdowns
//
// 1000-10000 - Reserved for frequency manager
//
// 10000+ - Always on top
*/
/* Primary */
--oc-zindex-filter: 10;
--oc-zindex-button: 10;
--oc-zindex-form: 10;
--oc-zindex-checkbox: 10;
--oc-zindex-breadcrumb: 10;
--oc-zindex-chart: 10;
--oc-zindex-tab: 10;
--oc-zindex-loader: 10;
--oc-zindex-navbar: 100;
--oc-zindex-navbar-fixed: 110;
/* Secondary */
--oc-zindex-fullscreen: 300;
/* Tertiary */
--oc-zindex-modal-background: 500;
--oc-zindex-modal: 600;
--oc-zindex-popover: 600;
--oc-zindex-dropdown: 600;
/* Always on top */
--oc-zindex-inspector: 10000;
--oc-zindex-datepicker: 10100;
--oc-zindex-flashmessage: 10300;
--oc-zindex-select: 10400;
--oc-zindex-alert: 10500;
--oc-zindex-snackbar: 10600;
--oc-zindex-tooltip: 10700;
}
================================================
FILE: modules/backend/assets/css/controls/settings-nav.css
================================================
html .control-settings-nav {
background: var(--oc-settings-bg);
}
body.has-settings-nav {
.control-settings-nav ul.top-level {
opacity: 1;
}
}
.control-settings-nav ul.top-level {
opacity: 0;
}
.control-settings-nav-container {
width: 300px;
}
.control-settings-nav {
width: 300px;
flex-shrink: 0;
border-right: 1px solid var(--oc-primary-border);
position: sticky;
top: 0;
height: calc(100vh - 70px);
&.is-full {
height: 100vh;
}
.input-clear-search {
display: none;
}
&.is-search-active {
.input-clear-search {
display: block;
}
}
.settings-nav-scroll-canvas {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.settings-search-toolbar-item {
display: block;
position: relative;
background: var(--oc-form-control-bg);
input.form-control {
border: none;
outline: none;
background: transparent;
border-radius: 0;
border-bottom: 1px solid var(--oc-primary-border);
padding-left: 3rem;
padding-right: 3rem;
height: 40px;
}
.input-icon-start, .input-icon-end {
position: absolute;
top: 50%;
transform: translateY(-50%);
font-size: 1.4rem;
margin-top: .1rem;
}
.input-icon-start {
left: 1rem;
}
.input-icon-end {
right: 1rem;
}
.input-decoration {
color: var(--bs-tertiary-color);
}
.input-clear-search {
text-decoration: none;
color: var(--bs-body-color);
&:hover {
color: var(--bs-secondary-color);
}
}
}
ul {
padding: 0;
margin: 0;
list-style: none;
}
ul.top-level {
padding-bottom: 20px;
}
div.scrollbar-thumb {
background: rgba(0,0,0,.2) !important;
}
ul.top-level > li {
padding-top: 10px;
&[data-status=collapsed] {
> div.group {
h3 > span.group-collapse {
transform: scaleY(-1) translate(0, -10px);
}
}
ul {
display: none;
}
}
> div.group {
/* This makes the group title stick to the top when scrolling
background: var(--oc-settings-bg);
position: relative;
position: sticky;
top: 0;
z-index: 1;
*/
h3 {
user-select: none;
font-size: 1rem;
font-weight: 600;
padding: 9px 2px 10px 60px;
margin: 0;
position: relative;
cursor: pointer;
> span.group-icon {
position: absolute;
left: 15px;
top: 4px;
width: 30px;
height: 30px;
> i {
font-size: 1.4rem;
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
}
> span.group-collapse {
display: block;
position: absolute;
width: 10px;
height: 10px;
right: 20px;
top: 7px;
transform: scaleY(1);
transition: all 0.1s ease;
font-size: 16px;
}
}
}
> ul {
li {
padding: 1px 10px;
a {
display: block;
position: relative;
padding: 5px 5px 3px 50px;
text-decoration: none !important;
border-radius: 6px;
font-size: 1rem;
span {
display: block;
line-height: 150%;
&.header {
margin-bottom: 3px;
}
}
}
a:hover,
&.active a {
opacity: 1;
text-decoration: none;
}
}
}
}
}
/* Item Styling */
.control-settings-nav ul.top-level > li {
> div.group h3 {
color: var(--oc-settings-color);
&:before {
color: var(--oc-settings-color);
}
}
}
.control-settings-nav ul.top-level > li > ul li {
a {
color: var(--oc-settings-color);
span.header {
color: var(--oc-settings-color);
}
}
a:hover {
background: var(--oc-settings-hover-bg);
color: var(--oc-settings-color);
span.header {
color: var(--oc-settings-color);
}
}
&.active a {
color: var(--oc-settings-active-color);
background: var(--oc-settings-active-bg);
span.header {
color: var(--oc-settings-active-color);
}
}
}
.control-settings-nav {
.control-scrollbar.vertical > div.scrollbar-scrollbar {
margin-right: 0;
div.scrollbar-thumb {
opacity: 0;
transition: opacity 0.1s ease-out;
background: rgba(0,0,0,.2);
}
}
&:hover {
.control-scrollbar.vertical > div.scrollbar-scrollbar {
div.scrollbar-thumb {
opacity: 1;
}
}
}
}
body.drag-noselect {
.control-settings-nav {
.control-scrollbar.vertical > div.scrollbar-scrollbar {
div.scrollbar-thumb {
opacity: 1;
}
}
}
}
/* Expanded */
@media (max-width: 767px) {
body.settings-nav-expanded {
#layout-body {
display: none !important;
}
.control-settings-nav-container {
width: 100%;
}
.control-settings-nav {
display: block !important;
margin: 0 auto;
border-right: none;
border-left: none;
flex-shrink: inherit;
width: 100%;
}
}
body:not(.settings-nav-expanded) {
.control-settings-nav-container,
.control-settings-nav {
display: none;
}
}
}
html body.main-menu-left .control-settings-nav {
height: 100vh;
}
================================================
FILE: modules/backend/assets/css/main.css
================================================
/* Backend */
@import './backend/_brand.css';
@import './backend/_vars.css';
@import './controls/settings-nav.css';
/* temp */
@import './october.css';
================================================
FILE: modules/backend/assets/css/october.css
================================================
.clearfix:before,.clearfix:after{content:" ";display:table}.clearfix:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.oc-hide{display:none !important}.oc-show{display:block !important}.oc-invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important;visibility:hidden !important}.t-ww{word-wrap:break-word;word-break:break-word}.t-nw{white-space:nowrap}.w-0{width:0 !important}.w-60{width:60px !important}.w-120{width:120px !important}.w-130{width:130px !important}.w-140{width:140px !important}.w-150{width:150px !important}.w-200{width:200px !important}.w-300{width:300px !important}.w-350{width:350px !important}.mw-400{max-width:400px !important}.mw-450{max-width:450px !important}.mw-500{max-width:500px !important}.mw-550{max-width:550px !important}.mw-600{max-width:600px !important}.mw-650{max-width:650px !important}.mw-700{max-width:700px !important}.mw-750{max-width:750px !important}.mw-800{max-width:800px !important}.mw-850{max-width:850px !important}.mw-900{max-width:900px !important}.mw-950{max-width:950px !important}.mw-1000{max-width:1000px !important}.mw-1050{max-width:1050px !important}.mw-1100{max-width:1100px !important}.mw-1150{max-width:1150px !important}.mw-1200{max-width:1200px !important}.mw-1250{max-width:1250px !important}.mw-1400{max-width:1400px !important}.mw-auto{max-width:auto !important}@-webkit-keyframes fadeIn{0%{opacity:0}100%{opacity:1}}@keyframes fadeIn{0%{opacity:0}100%{opacity:1}}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0, -100%, 0);transform:translate3d(0, -100%, 0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0, -100%, 0);-ms-transform:translate3d(0, -100%, 0);transform:translate3d(0, -100%, 0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInDown{-webkit-animation-name:fadeInDown;animation-name:fadeInDown}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%, 0, 0);transform:translate3d(-100%, 0, 0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%, 0, 0);-ms-transform:translate3d(-100%, 0, 0);transform:translate3d(-100%, 0, 0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInLeft{-webkit-animation-name:fadeInLeft;animation-name:fadeInLeft}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%, 0, 0);transform:translate3d(100%, 0, 0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%, 0, 0);-ms-transform:translate3d(100%, 0, 0);transform:translate3d(100%, 0, 0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInRight{-webkit-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0, 100%, 0);transform:translate3d(0, 100%, 0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0, 100%, 0);-ms-transform:translate3d(0, 100%, 0);transform:translate3d(0, 100%, 0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInUp{-webkit-animation-name:fadeInUp;animation-name:fadeInUp}@-webkit-keyframes fadeOut{0%{opacity:1}100%{opacity:0}}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}.fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeOutDown{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0, 100%, 0);transform:translate3d(0, 100%, 0)}}@keyframes fadeOutDown{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0, 100%, 0);-ms-transform:translate3d(0, 100%, 0);transform:translate3d(0, 100%, 0)}}.fadeOutDown{-webkit-animation-name:fadeOutDown;animation-name:fadeOutDown}@-webkit-keyframes fadeOutLeft{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(-100%, 0, 0);transform:translate3d(-100%, 0, 0)}}@keyframes fadeOutLeft{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(-100%, 0, 0);-ms-transform:translate3d(-100%, 0, 0);transform:translate3d(-100%, 0, 0)}}.fadeOutLeft{-webkit-animation-name:fadeOutLeft;animation-name:fadeOutLeft}@-webkit-keyframes fadeOutRight{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(100%, 0, 0);transform:translate3d(100%, 0, 0)}}@keyframes fadeOutRight{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(100%, 0, 0);-ms-transform:translate3d(100%, 0, 0);transform:translate3d(100%, 0, 0)}}.fadeOutRight{-webkit-animation-name:fadeOutRight;animation-name:fadeOutRight}@-webkit-keyframes fadeOutUp{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0, -100%, 0);transform:translate3d(0, -100%, 0)}}@keyframes fadeOutUp{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0, -100%, 0);-ms-transform:translate3d(0, -100%, 0);transform:translate3d(0, -100%, 0)}}.fadeOutUp{-webkit-animation-name:fadeOutUp;animation-name:fadeOutUp}@-webkit-keyframes fadeCycle{0%{opacity:1}50%{opacity:.5}100%{opacity:1}}@keyframes fadeCycle{0%{opacity:1}50%{opacity:.5}100%{opacity:1}}.fadeCycle{-webkit-animation:fadeCycle 2s infinite;-moz-animation:fadeCycle 2s infinite;animation:fadeCycle 2s infinite}.autocomplete.dropdown-menu{background:var(--bs-modal-bg)}.autocomplete.dropdown-menu li a{padding:3px 12px}body .control-balloon-selector ul{padding:0;display:inline-block;font-size:0}body .control-balloon-selector ul li{list-style:none;line-height:36px;display:inline-block;background-color:var(--oc-form-control-bg);color:var(--bs-body-color);font-size:1rem;padding:0 14px;border:1px solid var(--bs-border-color)}body .control-balloon-selector ul li:first-child{border-top-left-radius:4px;border-bottom-left-radius:4px}body .control-balloon-selector ul li:last-child{border-top-right-radius:4px;border-bottom-right-radius:4px}body .control-balloon-selector ul li.active{background:var(--oc-selection) !important;color:white;border-color:var(--oc-selection);position:relative;z-index:1}body .control-balloon-selector ul li+li{margin-left:-1px}body .control-balloon-selector.control-disabled ul li.active{background-color:#72809d !important;border-color:#72809d !important}body .control-balloon-selector:not(.control-disabled) ul li:hover{background:var(--oc-toolbar-hover-bg);cursor:pointer}body .control-balloon-selector.form-control-sm li{font-size:12px;padding:0 10px;line-height:27px}.form-group .control-balloon-selector ul{margin-bottom:0}:root,[data-bs-theme="light"]{--oc-callout-info-bg:#e6e7fc;--oc-callout-info-icon:#6a6cf7;--oc-callout-warning-bg:#faf6e2;--oc-callout-warning-icon:#e1b810;--oc-callout-danger-bg:#fadad7;--oc-callout-danger-content-bg:#fadad7;--oc-callout-danger-icon:#ff3e1d;--oc-callout-success-bg:#ecf7da;--oc-callout-success-icon:#86cb43}[data-bs-theme="dark"]{--oc-callout-info-bg:#12262c;--oc-callout-warning-bg:#302310;--oc-callout-danger-bg:#330c06;--oc-callout-success-bg:#1b290d}.callout{font-size:14px;margin-bottom:20px}.callout.fade{opacity:0;transition:all .5s,width 0s;transform:scale(.9)}.callout.fade.show{opacity:1;transform:scale(1)}.callout>.close{width:22px;height:22px;margin:15px 15px 0;position:relative;font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;opacity:1}.callout>.close:before{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\e93e";color:var(--bs-body-color);font-size:16px;position:relative}.callout>.close:hover:before{color:var(--bs-danger)}.callout>.action{float:right;padding:10px}.callout>.header+.content{border-top:none}.callout>.header{padding:15px 20px 5px;border-radius:4px 4px 0 0;color:var(--bs-body-color);margin-bottom:-1px}.callout>.header>.custom-icon{width:27.5px;text-align:center;font-size:22px;float:left;position:relative;top:-5px;left:-5px}.callout>.header>i,.callout>.header>.custom-icon{display:none}.callout>.header:before{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;float:left;font-size:22px;position:relative;left:-3px}.callout>.header h3{letter-spacing:0;font-size:14px;font-weight:600;line-height:150%;margin:0}.callout>.header h3,.callout>.header p,.callout>.header ul,.callout>.header ol{margin-left:32px;margin-bottom:7px}.callout>.header ul,.callout>.header ol{padding-left:20px}.callout>.header:last-child{border-radius:4px;margin-bottom:0}.callout>.content{color:var(--bs-body-color);padding:0 20px 15px 52px;border-radius:0 0 4px 4px}.callout>.content h1,.callout>.content h2,.callout>.content h3,.callout>.content h4,.callout>.content h5,.callout>.content h6{color:var(--bs-body-color);text-transform:none;margin:20px 0 5px 0;line-height:150%;font-weight:600}.callout>.content h1{font-size:30px}.callout>.content h2{font-size:26px}.callout>.content h3{font-size:24px}.callout>.content h4{font-size:20px}.callout>.content h5{font-size:18px}.callout>.content h6{font-size:16px}.callout>.content *:last-child{margin-bottom:0}.callout>.content ul,.callout>.content ol{padding-left:20px}.callout>.content ul li,.callout>.content ol li{margin-bottom:5px}.callout>.content .action-panel{padding:10px 0 0 0}.callout.has-custom-icon>.header>.custom-icon{display:block}.callout.has-custom-icon>.header:before{display:none}.callout.no-title>.content{padding-top:15px;border-radius:4px}.callout.no-subheader>.header{padding-bottom:10px}.callout.no-subheader>.header+.content{margin-top:-5px}.callout.no-icon>.header h3,.callout.no-icon>.header p,.callout.no-icon>.header ul,.callout.no-icon>.header ol{margin-left:0}.callout.no-icon>.header:before{display:none}.callout.no-icon>.content{padding-left:20px}.callout.callout-danger>.header{background:var(--oc-callout-danger-bg)}.callout.callout-danger>.header>.custom-icon{color:var(--oc-callout-danger-icon)}.callout.callout-danger>.header:before{content:"\e93d";color:var(--oc-callout-danger-icon)}.callout.callout-danger>.content{background:var(--oc-callout-danger-bg)}.callout.callout-info>.header{background:var(--oc-callout-info-bg)}.callout.callout-info>.header>.custom-icon{color:var(--oc-callout-info-icon)}.callout.callout-info>.header:before{content:"\e93f";color:var(--oc-callout-info-icon)}.callout.callout-info>.content{background:var(--oc-callout-info-bg)}.callout.callout-success>.header{background:var(--oc-callout-success-bg)}.callout.callout-success>.header>.custom-icon{color:var(--oc-callout-success-icon)}.callout.callout-success>.header:before{content:"\e93c";color:var(--oc-callout-success-icon)}.callout.callout-success>.content{background:var(--oc-callout-success-bg)}.callout.callout-warning>.header{background:var(--oc-callout-warning-bg)}.callout.callout-warning>.header>.custom-icon{color:var(--oc-callout-warning-icon)}.callout.callout-warning>.header:before{content:"\e93d";color:var(--oc-callout-warning-icon)}.callout.callout-warning>.content{background:var(--oc-callout-warning-bg)}.callout.is-flush{margin-bottom:0}.callout.is-flush>.content{border-bottom-left-radius:0;border-bottom-right-radius:0}.form-group>.callout{margin-bottom:0}.control-chart{text-align:left}.control-chart div.canvas{display:inline-block;margin-right:20px;margin-bottom:20px;position:relative}.control-chart div.canvas span.center{position:absolute;display:block;text-align:center;width:100%;top:50%;margin-top:-21px;font-size:30px;font-weight:100;color:var(--bs-heading-color);z-index:9}.control-chart div.canvas svg{z-index:10}.control-chart.full-width div.canvas{margin-right:0 !important}.control-chart ul{display:inline-block;height:inherit;margin:0;padding:0;list-style:none;position:relative;vertical-align:top}.control-chart ul li{width:120px;white-space:normal;display:block;text-transform:uppercase;color:var(--bs-heading-color);font-weight:300;font-size:12px;margin-bottom:10px}.control-chart ul li span{float:right;font-weight:600}.control-chart ul li:last-child{margin-bottom:0}.control-chart div.chart-legend{display:inline-block;vertical-align:top;text-align:left}.control-chart div.chart-legend table{font-size:12px;color:var(--bs-body-color)}.control-chart div.chart-legend table tr td{padding:0 0 7px 0;vertical-align:top}.control-chart div.chart-legend table tr td.value{padding-left:10px;font-weight:600;color:var(--bs-secondary-color)}.control-chart div.chart-legend table tr td i{display:inline-block;width:13px;height:13px;border-radius:4px;text-indent:-100000em;margin-right:5px;position:relative;top:2px}.control-chart div.chart-legend table tr td.indicator{width:20px}.control-chart div.chart-legend table tr:last-child td{padding-bottom:0}.control-chart .canvas{margin-right:20px;display:inline-block}.control-chart.centered{text-align:center}.control-chart.centered .canvas{margin-right:0;display:block;margin-left:auto;margin-right:auto}.control-chart.wrap-legend div.chart-legend table tr{display:inline-block;white-space:nowrap;margin-right:20px}.control-chart.wrap-legend div.chart-legend table tr:last-child td{padding-bottom:7px}.report-container .wrapped .control-chart{text-align:left}.report-container .wrapped .control-chart .canvas{margin-right:20px;display:inline-block}.tickLabel,.flot-tick-label{color:#545454}[data-bs-theme="dark"] .tickLabel,[data-bs-theme="dark"] .flot-tick-label{color:#e0e0e0}.title-value span.goal-meter-indicator{float:left;height:24px;width:10px;margin-right:5px;position:relative;top:9px;background:var(--oc-color-negative)}.title-value span.goal-meter-indicator>span{text-indent:-10000em;display:block;position:absolute;width:10px;left:0;bottom:0;background:var(--oc-color-positive);height:0;-webkit-transition:all .2s;transition:all .2s}.title-value.goal-meter-inverse span.goal-meter-indicator{background:var(--oc-color-positive)}.title-value.goal-meter-inverse span.goal-meter-indicator>span{background:var(--oc-color-negative)}.report-container .title-value{margin-top:-18px}.report-container .title-value p{font-weight:100;font-size:40px}.report-container .title-value p.description{font-size:12px;margin-top:9px}.report-container .title-value p:before{font-size:30px;margin-right:10px}.report-container .title-value p.negative:after,.report-container .title-value p.positive:after{top:-8px}.report-container .title-value span.goal-meter-indicator{height:31px;top:4px;width:15px;margin-right:10px}.report-container .title-value span.goal-meter-indicator span{width:15px}.control-status-list>ul{margin-bottom:0;padding:0}.control-status-list>ul li{margin:0;padding:7px 15px 6px;list-style:none;display:block;font-size:13px;color:#7e8c8d;border-bottom:1px solid var(--bs-border-color)}.control-status-list>ul li:last-child{border-bottom:none}.control-status-list>ul li a:not(.btn){color:#7e8c8d;text-decoration:none}.control-status-list>ul li a:not(.btn):hover{color:var(--bs-link-color);text-decoration:none}.control-status-list>ul li a.btn:hover{color:white}.control-status-list>ul li .status-text{margin:0 5px}.control-status-list>ul li .status-text.muted{color:var(--bs-secondary-color)}.control-status-list>ul li .status-text.primary{color:var(--bs-primary)}.control-status-list>ul li .status-text.success{color:#3c763d}.control-status-list>ul li .status-text.info{color:#31708f}.control-status-list>ul li .status-text.warning{color:#8a6d3b}.control-status-list>ul li .status-text.danger{color:#a94442}.control-status-list>ul li .status-label{display:block;float:right;padding:1px 10px}.control-status-list>ul li .status-label.btn{border-radius:20px}.control-status-list>ul li .status-icon{display:inline-block;text-align:center;color:white;width:22px;height:22px;position:relative;top:-1px;-webkit-border-radius:100px;-moz-border-radius:100px;border-radius:100px}.control-status-list>ul li .status-icon>i{font-size:12px;line-height:22px}.control-status-list>ul li .status-icon{background:#999}.control-status-list>ul li .status-icon.success{background:var(--bs-success)}.control-status-list>ul li .status-icon.primary{background:var(--bs-primary)}.control-status-list>ul li .status-icon.warning{background:var(--bs-warning)}.control-status-list>ul li .status-icon.danger{background:var(--bs-danger)}.control-status-list>ul li .status-icon.info{background:var(--bs-info)}.control-status-list>ul li .status-icon.link{background:transparent}.report-container .control-status-list>ul{margin:-15px}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul,.control-dropdown.dropdown-menu{padding:0;list-style:none;background-color:var(--bs-modal-bg);position:relative;border:var(--oc-popup-border);box-shadow:-2px 2px 5px rgba(67, 86, 100, 0.12);border-radius:4px}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul.is-fixed,.control-dropdown.dropdown-menu.is-fixed{position:fixed !important}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul.offset-left,.control-dropdown.dropdown-menu.offset-left{left:10px}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a,.control-dropdown.dropdown-menu>li a,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button,.control-dropdown.dropdown-menu>li button{outline:none;padding:10px 15px;font-size:14px;display:block;color:var(--bs-body-color);position:relative;white-space:nowrap;text-decoration:none}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a:hover,.control-dropdown.dropdown-menu>li a:hover,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button:hover,.control-dropdown.dropdown-menu>li button:hover,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a:active,.control-dropdown.dropdown-menu>li a:active,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button:active,.control-dropdown.dropdown-menu>li button:active,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a:focus:active,.control-dropdown.dropdown-menu>li a:focus:active,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button:focus:active,.control-dropdown.dropdown-menu>li button:focus:active{color:var(--oc-dropdown-hover-color);background-color:var(--oc-dropdown-hover-bg) !important}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a:hover i,.control-dropdown.dropdown-menu>li a:hover i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button:hover i,.control-dropdown.dropdown-menu>li button:hover i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a:active i,.control-dropdown.dropdown-menu>li a:active i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button:active i,.control-dropdown.dropdown-menu>li button:active i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a:focus:active i,.control-dropdown.dropdown-menu>li a:focus:active i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button:focus:active i,.control-dropdown.dropdown-menu>li button:focus:active i{color:var(--oc-dropdown-hover-color)}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a:hover[class^="oc-icon-"]:before,.control-dropdown.dropdown-menu>li a:hover[class^="oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button:hover[class^="oc-icon-"]:before,.control-dropdown.dropdown-menu>li button:hover[class^="oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a:active[class^="oc-icon-"]:before,.control-dropdown.dropdown-menu>li a:active[class^="oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button:active[class^="oc-icon-"]:before,.control-dropdown.dropdown-menu>li button:active[class^="oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a:focus:active[class^="oc-icon-"]:before,.control-dropdown.dropdown-menu>li a:focus:active[class^="oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button:focus:active[class^="oc-icon-"]:before,.control-dropdown.dropdown-menu>li button:focus:active[class^="oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a:hover[class*=" oc-icon-"]:before,.control-dropdown.dropdown-menu>li a:hover[class*=" oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button:hover[class*=" oc-icon-"]:before,.control-dropdown.dropdown-menu>li button:hover[class*=" oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a:active[class*=" oc-icon-"]:before,.control-dropdown.dropdown-menu>li a:active[class*=" oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button:active[class*=" oc-icon-"]:before,.control-dropdown.dropdown-menu>li button:active[class*=" oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a:focus:active[class*=" oc-icon-"]:before,.control-dropdown.dropdown-menu>li a:focus:active[class*=" oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button:focus:active[class*=" oc-icon-"]:before,.control-dropdown.dropdown-menu>li button:focus:active[class*=" oc-icon-"]:before{color:var(--oc-dropdown-hover-color)}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a:active,.control-dropdown.dropdown-menu>li a:active,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button:active,.control-dropdown.dropdown-menu>li button:active,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a:focus:active,.control-dropdown.dropdown-menu>li a:focus:active,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button:focus:active,.control-dropdown.dropdown-menu>li button:focus:active{background-color:var(--oc-dropdown-active-bg) !important}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a[class^="oc-icon-"],.control-dropdown.dropdown-menu>li a[class^="oc-icon-"],.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button[class^="oc-icon-"],.control-dropdown.dropdown-menu>li button[class^="oc-icon-"],.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a[class*=" oc-icon-"],.control-dropdown.dropdown-menu>li a[class*=" oc-icon-"],.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button[class*=" oc-icon-"],.control-dropdown.dropdown-menu>li button[class*=" oc-icon-"]{padding-left:30px}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a[class^="oc-icon-"]:before,.control-dropdown.dropdown-menu>li a[class^="oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button[class^="oc-icon-"]:before,.control-dropdown.dropdown-menu>li button[class^="oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a[class*=" oc-icon-"]:before,.control-dropdown.dropdown-menu>li a[class*=" oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button[class*=" oc-icon-"]:before,.control-dropdown.dropdown-menu>li button[class*=" oc-icon-"]:before{position:absolute;font-size:14px;left:9px;top:14px;color:rgba(var(--bs-body-color-rgb), .6)}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a>i,.control-dropdown.dropdown-menu>li a>i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button>i,.control-dropdown.dropdown-menu>li button>i{color:rgba(var(--bs-body-color-rgb), .6);font-size:14px;margin-right:4px;margin-left:-2px}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.first-item:not(.disabled) a:hover,.control-dropdown.dropdown-menu>li.first-item:not(.disabled) a:hover,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.first-item:not(.disabled) button:hover,.control-dropdown.dropdown-menu>li.first-item:not(.disabled) button:hover,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.first-item:not(.disabled) a:focus,.control-dropdown.dropdown-menu>li.first-item:not(.disabled) a:focus,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.first-item:not(.disabled) button:focus,.control-dropdown.dropdown-menu>li.first-item:not(.disabled) button:focus,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.first-item:not(.disabled) a:active,.control-dropdown.dropdown-menu>li.first-item:not(.disabled) a:active,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.first-item:not(.disabled) button:active,.control-dropdown.dropdown-menu>li.first-item:not(.disabled) button:active{border-top-left-radius:4px;border-top-right-radius:4px}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.last-item:not(.disabled) a:hover,.control-dropdown.dropdown-menu>li.last-item:not(.disabled) a:hover,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.last-item:not(.disabled) button:hover,.control-dropdown.dropdown-menu>li.last-item:not(.disabled) button:hover,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.last-item:not(.disabled) a:focus,.control-dropdown.dropdown-menu>li.last-item:not(.disabled) a:focus,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.last-item:not(.disabled) button:focus,.control-dropdown.dropdown-menu>li.last-item:not(.disabled) button:focus,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.last-item:not(.disabled) a:active,.control-dropdown.dropdown-menu>li.last-item:not(.disabled) a:active,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.last-item:not(.disabled) button:active,.control-dropdown.dropdown-menu>li.last-item:not(.disabled) button:active{border-bottom-left-radius:4px;border-bottom-right-radius:4px}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.dropdown-title,.control-dropdown.dropdown-menu>li.dropdown-title{display:none}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.dropdown-divider,.control-dropdown.dropdown-menu>li.dropdown-divider{margin:0}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.active>a,.control-dropdown.dropdown-menu>li.active>a{font-weight:bold}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled a,.control-dropdown.dropdown-menu>li.disabled a,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled button,.control-dropdown.dropdown-menu>li.disabled button,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a[disabled],.control-dropdown.dropdown-menu>li a[disabled],.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button[disabled],.control-dropdown.dropdown-menu>li button[disabled],.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled a:hover,.control-dropdown.dropdown-menu>li.disabled a:hover,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled button:hover,.control-dropdown.dropdown-menu>li.disabled button:hover,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a[disabled]:hover,.control-dropdown.dropdown-menu>li a[disabled]:hover,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button[disabled]:hover,.control-dropdown.dropdown-menu>li button[disabled]:hover,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled a:active,.control-dropdown.dropdown-menu>li.disabled a:active,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled button:active,.control-dropdown.dropdown-menu>li.disabled button:active,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a[disabled]:active,.control-dropdown.dropdown-menu>li a[disabled]:active,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button[disabled]:active,.control-dropdown.dropdown-menu>li button[disabled]:active,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled a:focus:active,.control-dropdown.dropdown-menu>li.disabled a:focus:active,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled button:focus:active,.control-dropdown.dropdown-menu>li.disabled button:focus:active,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a[disabled]:focus:active,.control-dropdown.dropdown-menu>li a[disabled]:focus:active,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button[disabled]:focus:active,.control-dropdown.dropdown-menu>li button[disabled]:focus:active{cursor:not-allowed;background-color:var(--bs-modal-bg) !important;color:#98A0A0}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled a>i,.control-dropdown.dropdown-menu>li.disabled a>i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled button>i,.control-dropdown.dropdown-menu>li.disabled button>i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a[disabled]>i,.control-dropdown.dropdown-menu>li a[disabled]>i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button[disabled]>i,.control-dropdown.dropdown-menu>li button[disabled]>i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled a:hover>i,.control-dropdown.dropdown-menu>li.disabled a:hover>i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled button:hover>i,.control-dropdown.dropdown-menu>li.disabled button:hover>i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a[disabled]:hover>i,.control-dropdown.dropdown-menu>li a[disabled]:hover>i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button[disabled]:hover>i,.control-dropdown.dropdown-menu>li button[disabled]:hover>i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled a:active>i,.control-dropdown.dropdown-menu>li.disabled a:active>i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled button:active>i,.control-dropdown.dropdown-menu>li.disabled button:active>i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a[disabled]:active>i,.control-dropdown.dropdown-menu>li a[disabled]:active>i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button[disabled]:active>i,.control-dropdown.dropdown-menu>li button[disabled]:active>i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled a:focus:active>i,.control-dropdown.dropdown-menu>li.disabled a:focus:active>i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled button:focus:active>i,.control-dropdown.dropdown-menu>li.disabled button:focus:active>i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a[disabled]:focus:active>i,.control-dropdown.dropdown-menu>li a[disabled]:focus:active>i,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button[disabled]:focus:active>i,.control-dropdown.dropdown-menu>li button[disabled]:focus:active>i{color:rgba(var(--bs-body-color-rgb), .6)}.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled a[class^="oc-icon-"]:before,.control-dropdown.dropdown-menu>li.disabled a[class^="oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled button[class^="oc-icon-"]:before,.control-dropdown.dropdown-menu>li.disabled button[class^="oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a[disabled][class^="oc-icon-"]:before,.control-dropdown.dropdown-menu>li a[disabled][class^="oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button[disabled][class^="oc-icon-"]:before,.control-dropdown.dropdown-menu>li button[disabled][class^="oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled a[class*=" oc-icon-"]:before,.control-dropdown.dropdown-menu>li.disabled a[class*=" oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li.disabled button[class*=" oc-icon-"]:before,.control-dropdown.dropdown-menu>li.disabled button[class*=" oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li a[disabled][class*=" oc-icon-"]:before,.control-dropdown.dropdown-menu>li a[disabled][class*=" oc-icon-"]:before,.dropdown-menu.backend-dropdownmenu .dropdown-container>ul>li button[disabled][class*=" oc-icon-"]:before,.control-dropdown.dropdown-menu>li button[disabled][class*=" oc-icon-"]:before{color:rgba(var(--bs-body-color-rgb), .6)}@media (hover:hover){.control-dropdown.dropdown-menu>li a:focus,.control-dropdown.dropdown-menu>li button:focus{background:var(--oc-toolbar-hover-bg)}}.touch .control-dropdown.dropdown-menu>li a:hover{color:var(--bs-body-color);background:white}body.dropdown-open .dropdown-overlay{position:fixed;left:0;top:0;right:0;bottom:0;z-index:599}@media (max-width:768px){body.dropdown-open{overflow:hidden}body.dropdown-open .dropdown-overlay{background:rgba(0,0,0,0.4)}body.dropdown-open .control-dropdown.dropdown-menu{overflow:auto;overflow-y:scroll;position:fixed !important;margin:15px !important;top:auto !important;right:0 !important;bottom:0 !important;left:0 !important;transform:none !important;z-index:600;border-radius:8px}body.dropdown-open .control-dropdown.dropdown-menu>li a,body.dropdown-open .control-dropdown.dropdown-menu>li button{font-size:16px;padding-top:12px;padding-bottom:12px}body.dropdown-open .control-dropdown.dropdown-menu>li.first-item:not(.disabled) a:hover,body.dropdown-open .control-dropdown.dropdown-menu>li.first-item:not(.disabled) button:hover,body.dropdown-open .control-dropdown.dropdown-menu>li.first-item:not(.disabled) a:focus,body.dropdown-open .control-dropdown.dropdown-menu>li.first-item:not(.disabled) button:focus,body.dropdown-open .control-dropdown.dropdown-menu>li.first-item:not(.disabled) a:active,body.dropdown-open .control-dropdown.dropdown-menu>li.first-item:not(.disabled) button:active{border-top-left-radius:8px;border-top-right-radius:8px}body.dropdown-open .control-dropdown.dropdown-menu>li.last-item:not(.disabled) a:hover,body.dropdown-open .control-dropdown.dropdown-menu>li.last-item:not(.disabled) button:hover,body.dropdown-open .control-dropdown.dropdown-menu>li.last-item:not(.disabled) a:focus,body.dropdown-open .control-dropdown.dropdown-menu>li.last-item:not(.disabled) button:focus,body.dropdown-open .control-dropdown.dropdown-menu>li.last-item:not(.disabled) a:active,body.dropdown-open .control-dropdown.dropdown-menu>li.last-item:not(.disabled) button:active{border-bottom-left-radius:8px;border-bottom-right-radius:8px}}.flash-message.static{color:#ffffff;font-size:14px;padding:10px 30px 10px 15px;word-wrap:break-word;text-align:left;border-radius:4px}.flash-message.static.success{background:var(--bs-success)}.flash-message.static.error{background:#cc3300}.flash-message.static.warning{background:#f0ad4e}.flash-message.static.info{background:#5fb6f5}.flash-message.static button{float:none;position:absolute;right:10px;top:12px;color:white;font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;opacity:1;outline:none}.flash-message.static button:before{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\e93e";color:white;font-size:16px;position:relative}.flash-message.static button:hover{opacity:1}.inspector-fields{min-width:220px;border-collapse:collapse;width:100%;table-layout:fixed;border-bottom-right-radius:2px;border-bottom-left-radius:2px}.inspector-fields td,.inspector-fields th{padding:5px 12px;font-size:12px;border-bottom:1px solid var(--oc-primary-border);text-align:left}.inspector-fields th{color:var(--oc-primary-color);width:45%}.inspector-fields td{color:var(--bs-body-color);width:55%}.inspector-fields tr:last-child td,.inspector-fields tr:last-child th{border-bottom:none}.inspector-fields tr:last-child td,.inspector-fields tr:last-child td input[type=text]{border-radius:0 0 2px 0}.inspector-fields tr.group{user-select:none}.inspector-fields tr.group th{font-weight:600;cursor:pointer}.inspector-fields tr.invalid th{color:#c03f31 !important}.inspector-fields tr.control-group{user-select:none}.inspector-fields tr.control-group th,.inspector-fields tr.control-group td{cursor:pointer}.inspector-fields tr.collapsed{display:none}.inspector-fields tr.expanded{display:table-row}.inspector-fields.has-groups th{padding-left:20px}.inspector-fields.has-groups tr.grouped th{padding-left:35px}.inspector-fields:not(:hover) td{border-left-color:transparent}.inspector-fields th{background:var(--oc-form-control-bg)}.inspector-fields td{font-weight:400;border-left:1px solid var(--oc-primary-border);text-overflow:ellipsis;white-space:nowrap;overflow:hidden;background:var(--oc-form-control-bg);transition:border-left-color .35s}.inspector-fields td.text input[type=text]{background:var(--oc-form-control-bg);color:var(--bs-body-color);text-overflow:ellipsis}.inspector-fields td.text input[type=text]::placeholder{font-weight:normal!important;color:#b5babd}.inspector-fields td.text.active{background:var(--oc-form-control-bg)}.inspector-fields td.autocomplete{padding:0;position:relative;overflow:visible}.inspector-fields td.autocomplete .autocomplete-container input[type=text]{padding:5px 12px}.inspector-fields td.autocomplete .autocomplete-container ul.dropdown-menu{background:var(--bs-modal-bg);font-size:12px;z-index:10000}.inspector-fields td.autocomplete .autocomplete-container ul.dropdown-menu li a{padding:5px 12px;white-space:normal;word-wrap:break-word}.inspector-fields td.autocomplete .autocomplete-container .loading-indicator span{margin-top:-12px;right:10px;left:auto}.inspector-fields td.trigger-cell{padding:0!important}.inspector-fields td.trigger-cell a.trigger{display:block;padding:5px 12px 7px 12px;overflow:hidden;min-height:29px;text-overflow:ellipsis;color:var(--oc-primary-color);text-decoration:none}.inspector-fields td.trigger-cell a.trigger.cell-placeholder{color:#b5babd}.inspector-fields td.trigger-cell a.trigger .loading-indicator{background-color:var(--oc-form-control-bg)}.inspector-fields td.trigger-cell a.trigger .loading-indicator span{margin-top:-12px;right:10px;left:auto}.inspector-fields td.dropdown{padding:0!important}.inspector-fields td select{width:90%}.inspector-fields td div.external-param-editor-container{position:relative;padding-right:25px}.inspector-fields td div.external-param-editor-container div.external-editor{bottom:0;margin:-5px -12px;right:30px;left:auto;top:0;position:absolute;transition:left .2s;transform:translateZ(0);will-change:transform}.inspector-fields td div.external-param-editor-container div.external-editor div.controls{position:absolute;width:100%;height:100%;left:0;top:0}.inspector-fields td div.external-param-editor-container div.external-editor div.controls a{position:absolute;left:0;top:0;display:inline-block;height:100%;width:30px;color:var(--oc-primary-color);outline:none}.inspector-fields td div.external-param-editor-container div.external-editor div.controls a i{display:inline-block;position:relative;left:7px;top:5px;font-size:1rem}.inspector-fields td div.external-param-editor-container div.external-editor div.controls input{position:absolute;display:block;border:none;width:100%;height:100%;left:0;top:0;padding-left:30px;padding-right:12px;background:transparent;color:var(--bs-body-color)}.inspector-fields td div.external-param-editor-container.editor-visible div.external-editor div.controls input{background:var(--oc-form-control-bg)}.inspector-fields td.active div.external-param-editor-container div.external-editor div.controls input{background:var(--bs-secondary-bg)}.inspector-fields td.dropdown div.external-param-editor-container div.external-editor,.inspector-fields td.trigger-cell div.external-param-editor-container div.external-editor{height:100%;margin:0;bottom:auto}.inspector-fields th{font-weight:600}.inspector-fields th>div{position:relative}.inspector-fields th>div>div{white-space:nowrap;padding-right:10px;text-overflow:ellipsis;overflow:hidden;width:100%}.inspector-fields th>div>div span.info{display:inline-block;position:absolute;right:3px;top:3px;font-size:14px;width:10px;height:12px;line-height:80%;color:#999}.inspector-fields th>div>div span.info:before{margin-left:3px}.inspector-fields th>div>div span.info:hover{color:var(--bs-emphasis-color)}.inspector-fields th>div a.expandControl{display:block;position:absolute;width:12px;height:12px;left:-15px;top:2px;text-indent:-100000em}.inspector-fields th>div a.expandControl span{position:absolute;display:inline-block;left:0;top:0;width:12px;height:12px}.inspector-fields th>div a.expandControl span:after{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f105";position:absolute;left:4px;top:-2px;width:12px;height:12px;font-size:13px;color:var(--oc-primary-color);text-indent:0}.inspector-fields th>div a.expandControl.expanded span:after{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f107";left:2px}.inspector-fields input[type=text]{display:block;width:100%;border:none;outline:none}.inspector-fields div.form-check{position:relative;top:1px;font-size:1rem}.inspector-fields .select2-container{width:100% !important}.inspector-fields .select2-container .select2-selection{height:29px;line-height:29px;padding:0 3px 0 12px;border:none !important;font-size:12px;-webkit-border-radius:0 !important;-moz-border-radius:0 !important;border-radius:0 !important;-webkit-box-shadow:none !important;box-shadow:none !important}.inspector-fields .select2-container .select2-selection.select2-default{font-weight:normal !important}.inspector-fields .select2-container .loading-indicator>span{top:15px}.inspector-fields .select2-container.select2-container--open{-webkit-border-radius:0 !important;-moz-border-radius:0 !important;border-radius:0 !important;border:none !important}.inspector-fields .select2-container .select2-selection__rendered{padding:0 22px 0 0;color:var(--bs-body-color)}.inspector-fields tr.changed td{font-weight:600}.inspector-fields tr.changed td input[type=text]{font-weight:600}.inspector-fields tr.changed td .select2-container .select2-selection{font-weight:600}div.control-popover.control-inspector>div{background:var(--oc-form-control-bg);border:none}div.control-popover.control-inspector>div:before,div.control-popover.control-inspector>div:after{display:none}div.control-popover.control-inspector>div>form{padding:5px 20px 10px}div.control-popover.control-inspector .popover-head{padding-left:20px;padding-right:20px}div.control-popover.control-inspector .inspector-fields th{padding-left:0}div.control-popover.hero>div{border-radius:8px}div.control-popover.hero .popover-head{border-top-right-radius:8px;border-top-left-radius:8px}div.control-popover.hero .popover-head h3{font-weight:normal;font-size:18px}div.control-popover.hero .popover-head .btn-close{top:16px;right:17px}div.control-popover.hero .inspector-fields th,div.control-popover.hero .inspector-fields td{padding:9px 12px;font-weight:600!important;font-size:13px}div.control-popover.hero .inspector-fields th{padding-left:0}div.control-popover.hero .inspector-fields td{font-weight:400!important}div.control-popover.hero .inspector-fields div.custom-select.select2-container .select2-selection{height:36px;line-height:36px}div.control-popover.inspector-temporary-placement{visibility:hidden;left:0!important;top:0!important}.inspector-columns-editor{min-height:400px;margin-bottom:20px;border-bottom:1px solid var(--oc-primary-border)}.inspector-columns-editor .items-column{width:250px}.inspector-columns-editor .inspector-wrapper{background:var(--oc-form-control-bg);border-left:2px solid var(--oc-primary-border)}.inspector-columns-editor .toolbar{padding:20px}.inspector-table-list{border-top:1px solid var(--bs-border-color);user-select:none}div.inspector-dictionary-container{border:1px solid var(--bs-border-color)}div.inspector-dictionary-container .values{height:300px}div.inspector-dictionary-container table.headers{width:100%;border:none}div.inspector-dictionary-container table.headers td{width:50%;padding:7px 5px;font-size:13px;text-transform:uppercase;font-weight:600;color:var(--oc-primary-color);background:var(--oc-primary-bg);border-bottom:1px solid var(--bs-border-color)}div.inspector-dictionary-container table.headers td:first-child{border-right:1px solid var(--bs-border-color)}div.inspector-dictionary-container table.inspector-dictionary-table{width:100%;border:none}div.inspector-dictionary-container table.inspector-dictionary-table tbody tr td{width:50%;padding:0!important;border-bottom:1px solid var(--bs-border-color)}div.inspector-dictionary-container table.inspector-dictionary-table tbody tr td div{border:1px solid transparent}div.inspector-dictionary-container table.inspector-dictionary-table tbody tr td.active div{border-color:var(--oc-accent)}div.inspector-dictionary-container table.inspector-dictionary-table tbody tr td input{width:100%;height:100%;display:block;outline:none;border:none;padding:7px 5px;box-shadow:none}div.inspector-dictionary-container table.inspector-dictionary-table tbody tr td input:focus{border:none;outline:none}div.inspector-dictionary-container table.inspector-dictionary-table tbody tr td:first-child{border-right:1px solid var(--bs-border-color)}div.inspector-dictionary-container table.inspector-dictionary-table tbody tr:last-child td{border-bottom:none}.inspector-header{background:var(--oc-popup-bg);padding:14px 16px;position:relative;color:var(--oc-primary-color);border-bottom:1px solid rgba(0,0,0,0.15)}.inspector-header h3{font-size:15px;font-weight:600;margin-top:0;margin-bottom:0;padding-right:15px;line-height:130%}.inspector-header p{font-size:12px;font-weight:normal;margin:5px 0 0 0}.inspector-header p:empty{display:none}.inspector-header span,.inspector-header a{text-decoration:none;position:absolute;top:12px;float:none;color:var(--bs-emphasis-color);cursor:pointer;line-height:1;opacity:.4}.inspector-header span:hover,.inspector-header a:hover{opacity:1;color:var(--bs-emphasis-color)}.inspector-header .detach{right:26px;line-height:22px}.inspector-header .close{right:11px;font-size:21px}.inspector-container:empty{display:none}.inspector-container .control-scrollpad{position:absolute}.inspector-field-comment:empty{display:none}ul.autocomplete.dropdown-menu.inspector-autocomplete{background:var(--bs-modal-bg);font-size:12px;z-index:10000}ul.autocomplete.dropdown-menu.inspector-autocomplete li a{padding:5px 12px;white-space:normal;word-wrap:break-word}.select2-dropdown.ocInspectorDropdown{font-size:12px;-webkit-border-radius:0 !important;-moz-border-radius:0 !important;border-radius:0 !important;border:none !important}.select2-dropdown.ocInspectorDropdown>.select2-results>.select2-results__options{font-size:12px}.select2-dropdown.ocInspectorDropdown>.select2-results>li>div{padding:5px 12px 5px}.select2-dropdown.ocInspectorDropdown>.select2-results li.select2-no-results{padding:5px 12px 5px}.select2-dropdown.ocInspectorDropdown>.select2-results li>i,.select2-dropdown.ocInspectorDropdown>.select2-results li>img{margin-left:6px}.select2-dropdown.ocInspectorDropdown .select2-search{min-height:26px;position:relative;border-bottom:1px solid var(--bs-border-color)}.select2-dropdown.ocInspectorDropdown .select2-search:after{position:absolute;font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f002";right:10px;top:10px;color:#95a5a6}.select2-dropdown.ocInspectorDropdown .select2-search input.select2-search__field{min-height:26px;background:transparent !important;font-size:13px;padding-left:4px;padding-right:20px;border:none}div.control-popover{position:absolute;background-clip:content-box;left:0;top:0;z-index:600;visibility:hidden}div.control-popover.fade,div.control-popover.show{visibility:visible}div.control-popover.fade>div{opacity:0;transition:all .3s,width 0s;transform:scale(.7)}div.control-popover.fade.show>div{opacity:1;transform:scale(1)}div.control-popover>div{position:relative;background:var(--oc-popup-bg);box-shadow:0 1px 6px rgba(0, 0, 0, 0.12), 0 1px 4px rgba(0, 0, 0, 0.24);border-radius:6px}div.control-popover>div:after,div.control-popover>div:before{position:absolute}div.control-popover>div:after{z-index:601}div.control-popover>div:before{z-index:600}div.control-popover.placement-bottom>div:after{content:'';display:block;width:0;height:0;border-left:7.5px solid transparent;border-right:7.5px solid transparent;border-bottom:8px solid var(--oc-popup-bg);left:15px;top:-8px}div.control-popover.placement-bottom>div:before{content:'';display:block;width:0;height:0;border-left:8.5px solid transparent;border-right:8.5px solid transparent;border-bottom:9px solid rgba(0,0,0,0.15);left:14px;top:-9px}div.control-popover.placement-bottom.placement-bottom-right>div:after{left:auto;right:15px}div.control-popover.placement-bottom.placement-bottom-right>div:before{left:auto;right:14px}div.control-popover.placement-top>div:after{content:'';display:block;width:0;height:0;border-left:7.5px solid transparent;border-right:7.5px solid transparent;border-top:8px solid var(--oc-popup-bg);border-bottom-width:0;left:15px;bottom:-8px}div.control-popover.placement-top>div:before{content:'';display:block;width:0;height:0;border-left:8.5px solid transparent;border-right:8.5px solid transparent;border-top:9px solid rgba(0,0,0,0.15);border-bottom-width:0;left:14px;bottom:-9px}div.control-popover.placement-left>div:after{content:'';display:block;width:0;height:0;border-top:7.5px solid transparent;border-bottom:7.5px solid transparent;border-left:8px solid var(--oc-popup-bg);right:-8px;top:7px}div.control-popover.placement-left>div:before{content:'';display:block;width:0;height:0;border-top:8.5px solid transparent;border-bottom:8.5px solid transparent;border-left:9px solid rgba(0,0,0,0.15);right:-9px;top:6px}div.control-popover.placement-right>div:after{content:'';display:block;width:0;height:0;border-top:7.5px solid transparent;border-bottom:7.5px solid transparent;border-right:8px solid var(--oc-popup-bg);left:-8px;top:7px}div.control-popover.placement-right>div:before{content:'';display:block;width:0;height:0;border-top:8.5px solid transparent;border-bottom:8.5px solid transparent;border-right:9px solid rgba(0,0,0,0.15);left:-9px;top:6px}div.control-popover div.popover-body{padding:15px}div.control-popover div.popover-body.form-container{padding-bottom:0}div.control-popover div.popover-footer{padding:0 20px 20px 20px}div.control-popover .popover-head{background:var(--oc-popup-bg);padding:14px 16px;position:relative;color:var(--oc-primary-color);border-top-right-radius:6px;border-top-left-radius:6px;border-bottom:1px solid rgba(0,0,0,0.15)}div.control-popover .popover-head:before{z-index:602;position:absolute}div.control-popover .popover-head h3{font-size:14px;font-weight:600;margin-top:0;margin-bottom:0;padding-right:15px;line-height:130%}div.control-popover .popover-head p{font-size:12px;font-weight:normal;margin:5px 0 0 0}div.control-popover .popover-head p:empty{display:none}div.control-popover .popover-head .btn-close,div.control-popover .popover-head .close{float:none;position:absolute;right:11px;top:12px;color:var(--oc-primary-color);outline:none;opacity:.4}div.control-popover .popover-head .btn-close:hover,div.control-popover .popover-head .close:hover{opacity:1}div.control-popover .popover-head .inspector-move-to-container{opacity:.4;position:absolute;top:14px;right:26px;float:none;color:var(--bs-emphasis-color);cursor:pointer;text-decoration:none;line-height:17px}div.control-popover .popover-head .inspector-move-to-container:hover{opacity:1;color:var(--bs-emphasis-color)}div.control-popover .popover-head .inspector-move-to-container:before{-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}div.control-popover.placement-bottom .popover-head:before{content:'';display:block;width:0;height:0;border-left:7.5px solid transparent;border-right:7.5px solid transparent;border-bottom:8px solid var(--oc-popup-bg);left:15px;top:-8px}div.control-popover.placement-left .popover-head:before{content:'';display:block;width:0;height:0;border-top:7.5px solid transparent;border-bottom:7.5px solid transparent;border-left:8px solid var(--oc-popup-bg);right:-8px;top:7px}div.control-popover.placement-right .popover-head:before{content:'';display:block;width:0;height:0;border-top:7.5px solid transparent;border-bottom:7.5px solid transparent;border-right:8px solid var(--oc-popup-bg);left:-8px;top:7px}div.control-popover.popover-danger>div{color:#fff;background-color:#ab2a1c}div.control-popover.popover-danger>div .popover-head p,div.control-popover.popover-danger>div .popover-head h3{color:#fff}div.control-popover.popover-danger>div .popover-head .btn-close{filter:var(--bs-btn-close-white-filter)}div.control-popover.popover-danger.placement-top>div:after{content:'';display:block;width:0;height:0;border-left:7.5px solid transparent;border-right:7.5px solid transparent;border-top:8px solid #ab2a1c;border-bottom-width:0}div.control-popover.popover-danger.placement-bottom>div:after,div.control-popover.popover-danger.placement-bottom .popover-head:before{content:'';display:block;width:0;height:0;border-left:7.5px solid transparent;border-right:7.5px solid transparent;border-bottom:8px solid #ab2a1c}div.control-popover.popover-danger.placement-left>div:after,div.control-popover.popover-danger.placement-left .popover-head:before{content:'';display:block;width:0;height:0;border-top:7.5px solid transparent;border-bottom:7.5px solid transparent;border-left:8px solid #ab2a1c}div.control-popover.popover-danger.placement-right>div:after,div.control-popover.popover-danger.placement-right .popover-head:before{content:'';display:block;width:0;height:0;border-top:7.5px solid transparent;border-bottom:7.5px solid transparent;border-right:8px solid #ab2a1c}div.control-popover.popover-danger .popover-head{color:#fff;background-color:#ab2a1c;border-bottom:2px solid rgba(255,255,255,0.15)}div.control-popover.popover-danger .popover-head .close{color:#fff;text-shadow:none}div.control-popover div.popover-fixed-height{height:300px}.popover-highlight{position:relative;z-index:598 !important}.popover-highlight:hover,.popover-highlight:active,.popover-highlight:focus{z-index:598 !important}div.popover-overlay-container{position:fixed;left:0;top:0;right:0;bottom:0;z-index:597;font-size:14px}div.popover-overlay-container>div.control-popover>div{border-radius:4px}div.popover-overlay{position:fixed;left:0;top:0;right:0;bottom:0;background:rgba(0, 0, 0, var(--bs-backdrop-opacity));z-index:597;display:none}div.popover-overlay.show{display:block}@media (max-width:768px){body.popover-open{overflow:hidden}body.popover-open .control-popover{overflow:auto;overflow-y:scroll;position:fixed;margin:0;padding:10px;width:100% !important;z-index:603;top:auto !important;right:0 !important;bottom:0 !important;left:0 !important}body.popover-open .control-popover>div{padding:0}body.popover-open .control-popover>div:before,body.popover-open .control-popover>div:after{display:none}body.popover-open .control-popover div.popover-fixed-height{height:100%;min-height:100%}body.popover-open .control-popover .popover-head:before{display:none}div.popover-overlay{display:block}}.modal{z-index:600}.modal-backdrop{z-index:490;background-color:rgba(0, 0, 0, var(--bs-backdrop-opacity))}.modal-content{border:var(--oc-popup-border);border-radius:8px;box-shadow:0 0 32px rgba(67, 86, 100, 0.2);background:var(--oc-popup-bg)}.modal-content.popup-shaking{-webkit-animation:popup-shake .82s cubic-bezier(.36, .07, .19, .97) both;animation:popup-shake .82s cubic-bezier(.36, .07, .19, .97) both;-webkit-transform:translate3d(0, 0, 0);-ms-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;-moz-perspective:1000px;perspective:1000px}.modal-content .modal-header{background:transparent;color:var(--bs-heading-color);border-top-right-radius:4px;border-top-left-radius:4px;padding:15px 20px;border-bottom:var(--oc-popup-border)}.modal-content .modal-header h4{font-weight:normal;font-size:18px}.modal-content .modal-header button.close{width:19px;height:19px;border-radius:20px;position:relative;top:5px;opacity:1;font-size:0;color:transparent}.modal-content .modal-header button.close:after{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\e93e";color:#35425b;font-size:16px;position:relative;top:-1px}.modal-content .modal-header button.close:hover:after{color:var(--bs-danger)}.modal-content .modal-header button.close:focus{outline:none;background-color:var(--oc-toolbar-hover-bg)}.modal-content .modal-body{padding:20px 20px 0 20px}.modal-content .modal-body>p:last-child{margin-bottom:20px}.modal-content .modal-body.modal-no-header{padding-top:20px}.modal-content .modal-body.modal-no-footer{padding-bottom:20px}.modal-content .modal-footer{background:transparent;border:none;margin-top:0;padding:0 20px 20px 20px;justify-content:flex-start}.modal-content .modal-footer>.pull-right{margin-left:auto}.modal-content .modal-footer>*:first-child{margin-left:0}.modal-content .modal-footer>*:last-child{margin-right:0}.modal-content .modal-footer .btn-link{padding-left:5px;padding-right:5px}.modal-dialog{max-width:598px}.modal-dialog.size-adaptive{max-width:100%;padding-right:50px;padding-left:50px}.modal-dialog.adaptive-height{height:100%;min-height:600px;margin-top:0;margin-bottom:0;padding-top:50px;padding-bottom:50px}.modal-dialog.adaptive-height .modal-content{height:100%}@media (min-width:768px){.modal-dialog.size-tiny{max-width:300px}.modal-dialog.size-small{max-width:400px}}@media (min-width:992px){.modal-dialog.size-large{max-width:750px}.modal-dialog.size-huge{max-width:900px}.modal-dialog.size-giant{max-width:982px}}@media (max-width:768px){.modal-dialog.size-adaptive{max-width:auto;padding:5px 0;margin:0}}.modal-dialog.size-400{max-width:400px}.modal-dialog.size-450{max-width:450px}.modal-dialog.size-500{max-width:500px}.modal-dialog.size-550{max-width:550px}.modal-dialog.size-600{max-width:600px}.modal-dialog.size-650{max-width:650px}.modal-dialog.size-700{max-width:700px}.modal-dialog.size-750{max-width:750px}.modal-dialog.size-800{max-width:800px}.modal-dialog.size-850{max-width:850px}.modal-dialog.size-900{max-width:900px}.modal-dialog.size-950{max-width:950px}.modal-dialog.size-1000{max-width:1000px}.modal-dialog.size-1050{max-width:1050px}.modal-dialog.size-1100{max-width:1100px}.modal-dialog.size-1200{max-width:1200px}.modal-dialog.size-1400{max-width:1400px}.modal-dialog.size-auto{max-width:auto;padding:5px 0;margin:0}.control-popup.fade:not(.show){pointer-events:none}.control-popup.fade .modal-dialog{opacity:0;-webkit-transition:all 0.3s, width 0s;transition:all 0.3s, width 0s;-webkit-transform:scale(0.7);-ms-transform:scale(0.7);transform:scale(0.7)}.control-popup.fade.show .modal-dialog{opacity:1;-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}.popup-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:490;background-color:rgba(0, 0, 0, var(--bs-backdrop-opacity));opacity:1}.popup-backdrop .popup-loading-indicator{background:var(--oc-popup-bg);display:block;width:100px;height:100px;position:absolute;top:130px;left:50%;margin-left:-50px;transition:all .3s,width 0s;transform:scale(.7);opacity:0}.popup-backdrop .popup-loading-indicator:after{--loader-size:50px;content:'';position:absolute;top:50%;left:50%;width:var(--loader-size);height:var(--loader-size);margin-top:calc(var(--loader-size) / -2);margin-left:calc(var(--loader-size) / -2);border:4px solid var(--oc-primary-border);border-top-color:var(--oc-accent);border-radius:50%;animation:spin .8s linear infinite}.popup-backdrop.loading .popup-loading-indicator{opacity:1;-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}@-moz-keyframes popup-shake{10%,90%{-moz-transform:translate3d(-1px, 0, 0)}20%,80%{-moz-transform:translate3d(2px, 0, 0)}30%,50%,70%{-moz-transform:translate3d(-4px, 0, 0)}40%,60%{-moz-transform:translate3d(4px, 0, 0)}}@-webkit-keyframes popup-shake{10%,90%{-webkit-transform:translate3d(-1px, 0, 0)}20%,80%{-webkit-transform:translate3d(2px, 0, 0)}30%,50%,70%{-webkit-transform:translate3d(-4px, 0, 0)}40%,60%{-webkit-transform:translate3d(4px, 0, 0)}}@keyframes popup-shake{10%,90%{transform:translate3d(-1px, 0, 0)}20%,80%{transform:translate3d(2px, 0, 0)}30%,50%,70%{transform:translate3d(-4px, 0, 0)}40%,60%{transform:translate3d(4px, 0, 0)}}.control-toolbar{font-size:0;padding:0 0 10px 0;position:relative;display:table;table-layout:fixed;width:100%}.control-toolbar .toolbar-divider{margin-right:1px;display:inline-block;height:1em;padding:10px 0;border-right:1px solid var(--oc-toolbar-border);border-left:1px solid var(--bs-border-color);width:1px;vertical-align:middle;margin-right:8px}.control-toolbar .toolbar-item{position:relative;white-space:nowrap;display:table-cell;vertical-align:top;padding-right:10px}.control-toolbar .toolbar-item:last-child,.control-toolbar .toolbar-item.last{padding-right:0}.control-toolbar .toolbar-item.toolbar-primary{position:relative}.control-toolbar .toolbar-item.toolbar-primary:after,.control-toolbar .toolbar-item.toolbar-primary:before{content:'';position:absolute;top:0;bottom:0;width:10px;z-index:2;opacity:0;transition:opacity .1s ease-out}.control-toolbar .toolbar-item.toolbar-primary:before{background:transparent linear-gradient(90deg, #000 0%, rgba(0,0,0,0) 100%);left:10px}.control-toolbar .toolbar-item.toolbar-primary:after{background:transparent linear-gradient(270deg, #000 0%, rgba(0,0,0,0) 100%);right:-10px}.control-toolbar .toolbar-item.toolbar-primary.scroll-before:before{opacity:.15}.control-toolbar .toolbar-item.toolbar-primary.scroll-after:after{opacity:.15}.control-toolbar .toolbar-item.toolbar-primary:before{left:-3px}.control-toolbar .toolbar-item.toolbar-primary:after{right:13px}.control-toolbar .toolbar-item.toolbar-setup{width:28px;vertical-align:middle}.control-toolbar .toolbar-item.toolbar-setup a{padding:8px 15px}.control-toolbar .toolbar-item.toolbar-setup a>span{display:block;color:var(--oc-toolbar-color);width:24px;height:24px;text-align:center;border-radius:3px}.control-toolbar .toolbar-item.toolbar-setup a>span:before{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\e90c";font-size:14px;line-height:24px;display:inline-block;vertical-align:middle}.control-toolbar .toolbar-item.toolbar-setup a:hover>span{color:var(--oc-toolbar-hover-color);background:var(--oc-toolbar-hover-bg)}.control-toolbar .btn,.control-toolbar .btn-group,.control-toolbar .dropdown,.control-toolbar .btn-container{white-space:nowrap;float:none;display:inline-block;margin-right:10px}.control-toolbar .btn:last-child,.control-toolbar .btn-group:last-child,.control-toolbar .dropdown:last-child,.control-toolbar .btn-container:last-child{margin-right:0}.control-toolbar .btn.standalone,.control-toolbar .btn-group.standalone,.control-toolbar .dropdown.standalone,.control-toolbar .btn-container.standalone{margin-right:15px}.control-toolbar .dropdown>.btn{margin-right:0}.control-toolbar .btn-group>.btn,.control-toolbar .btn-group>.dropdown{margin-right:0;display:inline-block;float:none}.control-toolbar .btn-group .dropdown>.btn{margin-right:0;border-bottom-right-radius:0;border-top-right-radius:0}.control-toolbar .btn-group .dropdown.last>.btn{border-bottom-right-radius:4px !important;border-top-right-radius:4px !important}.control-toolbar input.form-control[type=text]{height:auto;padding:7px 13px 6px}.control-toolbar.toolbar-padded{padding:20px}.control-toolbar.flex{display:flex;flex-direction:row}.control-toolbar.flex .toolbar-item{display:block;flex:1 1 auto}.control-toolbar.flex .toolbar-item.fix-width{flex:0 0 auto}.relation-toolbar .control-toolbar .btn,.control-toolbar.toolbar-form .btn{line-height:inherit;display:inline-block;color:inherit;padding:0 6px;min-width:40px;text-align:center;text-decoration:none !important;border-radius:4px;outline:none;box-shadow:none;-webkit-appearance:none;border:none;background:transparent;color:var(--oc-toolbar-color);font-size:14px;margin-right:8px;line-height:30px}.relation-toolbar .control-toolbar .btn.control-button,.control-toolbar.toolbar-form .btn.control-button{color:var(--oc-toolbar-color);font-size:14px;margin-right:8px;line-height:30px}.relation-toolbar .control-toolbar .btn.icon-only,.control-toolbar.toolbar-form .btn.icon-only{min-width:30px}.relation-toolbar .control-toolbar .btn[disabled],.control-toolbar.toolbar-form .btn[disabled]{cursor:default}.relation-toolbar .control-toolbar .btn[disabled]>i,.control-toolbar.toolbar-form .btn[disabled]>i,.relation-toolbar .control-toolbar .btn[disabled]>span,.control-toolbar.toolbar-form .btn[disabled]>span,.relation-toolbar .control-toolbar .btn[disabled]:after,.control-toolbar.toolbar-form .btn[disabled]:after{opacity:.5}.relation-toolbar .control-toolbar .btn i,.control-toolbar.toolbar-form .btn i{display:inline-block;position:relative;font-size:16px;top:1px}.relation-toolbar .control-toolbar .btn i+span.button-label,.control-toolbar.toolbar-form .btn i+span.button-label{margin-left:6px}.relation-toolbar .control-toolbar .btn:not([disabled]):hover,.control-toolbar.toolbar-form .btn:not([disabled]):hover{background:var(--oc-toolbar-hover-bg)}.relation-toolbar .control-toolbar .btn:not([disabled]):focus,.control-toolbar.toolbar-form .btn:not([disabled]):focus{background:var(--oc-toolbar-hover-bg)}.relation-toolbar .control-toolbar .btn.has-menu:after,.control-toolbar.toolbar-form .btn.has-menu:after{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\e90a";font-size:16px;vertical-align:middle;margin-left:0;margin-right:-3px;position:relative;top:-1px}[data-control~=toolbar]{white-space:nowrap;width:100%;overflow:hidden;padding:.3rem;margin:-0.3rem}@media (pointer:coarse){[data-control~=toolbar].is-native-drag{overflow:auto}}.clear-input-text{padding:0 5px;cursor:pointer;border:none;border-radius:100px;-webkit-appearance:none;font-size:21px;font-weight:bold;line-height:1;color:var(--bs-emphasis-color);text-shadow:0 1px 0 var(--bs-body-bg);font-family:sans-serif;display:inline-block;position:absolute;right:3px;top:0;bottom:0;margin:auto;background-color:var(--oc-form-control-bg);height:28px;width:26px;z-index:2}.clear-input-text>i{opacity:1;width:16px;height:16px;background-position:-16px -38px;display:block}.clear-input-text:hover,.clear-input-text:focus{text-decoration:none;cursor:pointer}.clear-input-text:focus{outline:none}.control-toolbar.form-toolbar{background-color:var(--oc-content-tab-content-bg);border-radius:6px}.control-toolbar.form-toolbar .btn-outline-secondary{--bs-btn-color:var(--bs-body-color)}.control-toolbar.form-toolbar .btn{--bs-btn-padding-x:.5rem;--bs-btn-padding-y:.4rem;border:none;font-size:14px;margin-right:8px;transition:all .15s ease-in-out}.control-toolbar.form-toolbar .btn:before{transition:color .15s ease-in-out}.control-toolbar.form-toolbar .btn,.control-toolbar.form-toolbar .btn:before{color:var(--bs-body-color)}.control-toolbar.form-toolbar .btn:hover,.control-toolbar.form-toolbar .btn:focus:hover,.control-toolbar.form-toolbar .btn:active,.control-toolbar.form-toolbar .btn:focus-visible,.control-toolbar.form-toolbar .btn:hover:before,.control-toolbar.form-toolbar .btn:focus:hover:before,.control-toolbar.form-toolbar .btn:active:before,.control-toolbar.form-toolbar .btn:focus-visible:before{color:#fff}.control-toolbar.editor-toolbar{padding:0;background:#ffffff;border-bottom-right-radius:0;border-bottom-left-radius:0;border-bottom:1px solid var(--oc-toolbar-border)}.control-toolbar.editor-toolbar .toolbar-item .btn,.control-toolbar.editor-toolbar .toolbar-item .btn-group,.control-toolbar.editor-toolbar .toolbar-item .dropdown{margin:0;padding:0}.control-toolbar.editor-toolbar .toolbar-item .btn{text-align:center;height:38px;width:38px;line-height:38px;color:#333;background:transparent;user-select:none;box-shadow:none;text-shadow:none;border-radius:0;font-size:14px}.control-toolbar.editor-toolbar .toolbar-item .btn>i{opacity:1}.control-toolbar.editor-toolbar .toolbar-item .btn:hover{outline:none;background-color:#ddd;color:#000}.control-toolbar.editor-toolbar .toolbar-item .btn.active,.control-toolbar.editor-toolbar .toolbar-item .btn:active{outline:none;background-color:#d6d6d6;color:#000}.control-toolbar.editor-toolbar .toolbar-item .btn.disabled,.control-toolbar.editor-toolbar .toolbar-item .btn[disabled]{opacity:1;color:#bdbdbd;cursor:default;background:transparent}.control-toolbar.editor-toolbar .toolbar-item .dropdown.open .btn{background-color:#d6d6d6;color:#000}.control-toolbar.editor-toolbar .toolbar-item .btn[class^="oc-icon-"]:before,.control-toolbar.editor-toolbar .toolbar-item .btn[class*=" oc-icon-"]:before{opacity:1;margin:0}.control-toolbar.editor-toolbar .toolbar-item .btn.oc-autumn-button{color:#c03f31}.control-toolbar.editor-toolbar .toolbar-item .btn.oc-autumn-button:hover{color:#000 !important}#layout-side-panel div.control-toolbar,.compact-toolbar div.control-toolbar,#layout-side-panel div.control-toolbar.toolbar-padded,.compact-toolbar div.control-toolbar.toolbar-padded{padding:0}#layout-side-panel div.control-toolbar.separator,.compact-toolbar div.control-toolbar.separator{border-bottom:1px solid var(--oc-toolbar-border)}#layout-side-panel div.control-toolbar .toolbar-item,.compact-toolbar div.control-toolbar .toolbar-item{padding-right:0}#layout-side-panel div.control-toolbar .btn,.compact-toolbar div.control-toolbar .btn{border-radius:0 !important;padding-top:12px;padding-bottom:13px;margin-right:0;box-shadow:none}#layout-side-panel div.control-toolbar input.form-control,.compact-toolbar div.control-toolbar input.form-control{border:none;padding:11px 13px 12px;height:auto;border-radius:0 !important}#layout-side-panel div.control-toolbar input.form-control.icon.search,.compact-toolbar div.control-toolbar input.form-control.icon.search{background-position:right -78px}#layout-side-panel div.control-toolbar div.loading-indicator-container.size-input-text .loading-indicator,.compact-toolbar div.control-toolbar div.loading-indicator-container.size-input-text .loading-indicator{top:6px}.tooltip{z-index:10700;font-size:13px}.form-check-input:checked,.form-check-input[type=checkbox]:indeterminate{background-color:var(--bs-primary);border-color:var(--bs-primary)}.backend-icon-background{background-image:url('../foundation/elements/backendicons/backend-icons.png');background-size:300px 300px;background-position:0 0}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){.backend-icon-background{background-image:url('../foundation/elements/backendicons/backend-icons@2x.png')}}.backend-icon-background.entity-small{position:relative;width:10px;height:10px}.backend-icon-background.entity-small.cms-page{background-position:-129px 0}.backend-icon-background.entity-small.cms-partial{background-position:-180px -20px}.backend-icon-background.entity-small.cms-content{background-position:-206px -20px}.backend-icon-background.entity-small.cms-layout{background-position:-193px -20px}.backend-icon-background.entity-small.cms-asset{background-position:-219px -20px}.backend-icon-background.entity-small.tailor-blueprint{background-position:-180px -60px}.backend-icon-background.entity-small.text{background-position:-142px 0}.backend-icon-background.monaco-document{position:relative;width:16px;height:16px}.backend-icon-background.monaco-document.php{background-position:-181px -38px}.backend-icon-background.monaco-document.html{background-position:-203px -38px}.backend-icon-background-pseudo:before{content:'';background-image:url('../foundation/elements/backendicons/backend-icons.png');background-size:300px 300px;background-position:0 0}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){.backend-icon-background-pseudo:before{background-image:url('../foundation/elements/backendicons/backend-icons@2x.png')}}[data-bs-theme="dark"] .backend-icon-background-pseudo:before{background-image:url('../foundation/elements/backendicons/backend-icons-dark.png')}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){[data-bs-theme="dark"] .backend-icon-background-pseudo:before{background-image:url('../foundation/elements/backendicons/backend-icons-dark@2x.png')}}body.breadcrumb-flush .control-breadcrumb,.control-breadcrumb.breadcrumb-flush{margin-bottom:0}.control-breadcrumb>ol>li>a{text-decoration:underline}body.compact-container .control-breadcrumb{margin-top:20px;margin-left:20px;margin-right:20px}body.compact-container .control-breadcrumb>ol{margin-bottom:0}body.slim-container .layout-container .control-breadcrumb{margin-left:20px;margin-right:20px}.btn{--bs-btn-padding-x:1.25rem;--bs-btn-padding-y:.5rem}.btn-lg,.btn-group-lg>.btn{--bs-btn-padding-y:.5rem;--bs-btn-padding-x:1.5rem;--bs-btn-font-size:1.15rem}.btn-sm,.btn-group-sm>.btn{--bs-btn-padding-y:.4rem;--bs-btn-padding-x:.85rem}.btn-default,.btn-primary,.btn-secondary,.btn-success,.btn-info,.btn-warning,.btn-danger{--bs-btn-box-shadow:rgba(20,19,78,0.32) 0 1px 1px 0, var(--bs-btn-bg) 0 0 0 1px, rgba(64,68,82,0.08) 0 2px 5px 0;box-shadow:var(--bs-btn-box-shadow);border:none}.btn-primary{--bs-btn-font-weight:600;--bs-btn-bg:var(--bs-primary);--bs-btn-hover-bg:var(--oc-primary-hover-bg);--bs-btn-active-bg:var(--oc-primary-active-bg)}.btn-secondary{--bs-btn-color:var(--bs-emphasis-color);--bs-btn-bg:var(--oc-secondary-bg);--bs-btn-hover-color:var(--bs-emphasis-color);--bs-btn-hover-bg:var(--oc-secondary-hover-bg);--bs-btn-active-color:var(--bs-emphasis-color);--bs-btn-active-bg:var(--oc-secondary-active-bg);--bs-btn-disabled-bg:var(--oc-secondary-bg);--bs-btn-disabled-color:var(--bs-body-color);--bs-btn-box-shadow:rgba(0,0,0,0.12) 0 1px 1px 0, rgba(64,68,82,0.16) 0 0 0 1px, rgba(64,68,82,0.08) 0 2px 5px 0}.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{--bs-btn-color:#fff;--bs-btn-hover-color:#fff;--bs-btn-active-color:#fff;--bs-btn-disabled-color:rgba(255,255,255,0.6)}.btn-success{--bs-btn-hover-bg:var(--bs-success)}.btn-info{--bs-btn-hover-bg:var(--bs-info)}.btn-warning{--bs-btn-hover-bg:var(--bs-warning)}.btn-default{--bs-btn-color:#fff;--bs-btn-bg:var(--bs-secondary);--bs-btn-border-color:var(--bs-secondary);--bs-btn-hover-color:#fff;--bs-btn-hover-bg:var(--oc-primary-hover-bg);--bs-btn-hover-border-color:var(--oc-primary-hover-bg);--bs-btn-focus-shadow-rgb:90, 92, 210;--bs-btn-active-color:#fff;--bs-btn-active-bg:var(--oc-primary-active-bg);--bs-btn-active-border-color:var(--oc-primary-active-bg);--bs-btn-active-shadow:inset 0 3px 5px rgba(0,0,0,0.125);--bs-btn-disabled-color:#f0f0f0;--bs-btn-disabled-bg:var(--bs-secondary);--bs-btn-disabled-border-color:var(--bs-secondary)}.btn-link.text-muted{text-decoration:underline}.btn-link.text-muted:hover,.btn-link.text-muted:focus-visible{color:#124364 !important}.input-group .btn{--bs-btn-box-shadow:none}.btn-outline-default,.btn-outline-primary,.btn-outline-success,.btn-outline-info,.btn-outline-warning,.btn-outline-danger{--bs-btn-hover-color:#fff;--bs-btn-active-color:#f0f0f0}.btn-outline-default{--bs-btn-hover-bg:var(--oc-brand-primary);--bs-btn-active-bg:var(--oc-brand-primary)}.button-separator{color:var(--bs-secondary-color);margin:0 10px;font-size:14px;padding-bottom:2px}.button-separator+.btn-link{margin-left:-5px}.btn[class^="oc-icon-"]:before,.btn[class*=" oc-icon-"]:before{font-size:14px;line-height:14px;position:relative;text-decoration:none}.btn i{font-size:14px;line-height:14px;position:relative;top:1px;text-decoration:none;margin-right:4px}.btn-group .btn{border-right:1px solid rgba(0,0,0,0.09);margin-left:0 !important}.btn-group .btn:last-child,.btn-group .btn.last{border-right:none}.btn-group .btn.last{border-bottom-right-radius:4px !important;border-top-right-radius:4px !important}.btn-group>.dropdown{float:left}.btn-group>.dropdown:not(:last-child, .last)>.btn{border-right:1px solid rgba(0,0,0,0.09);border-bottom-right-radius:0 !important;border-top-right-radius:0 !important}.btn-group>.dropdown:not(:first-child)>.btn{border-bottom-left-radius:0 !important;border-top-left-radius:0 !important}.btn-group>.dropdown.last .btn{border-right:none}.btn.offset-right,.btn-group.offset-right{margin-right:10px}.btn-icon{display:inline-block;height:36px;font-size:21px;background:transparent;border:none;outline:none;border-radius:.3rem;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.btn-icon:before{display:block;color:#bcc3c7}.btn-icon:focus-visible{box-shadow:0 0 0 .25rem rgba(53,66,91,0.1)}.btn-icon:hover:before{color:var(--bs-link-color)}.btn-icon.danger:hover:before{color:#c63e26}.btn-icon.pull-right:before{margin-right:0}.btn-icon.margin-left{margin-left:5px}.btn-icon.small{font-size:17px;height:17px;line-height:15px}.btn-icon.larger{font-size:21px;height:21px;line-height:17px}.btn-text{font-size:14px;color:var(--bs-secondary-color);vertical-align:middle;display:inline-block;padding:8px 0}.btn-text a,.btn-text .btn.btn-link{font-size:14px;color:var(--bs-secondary-color);text-decoration:underline;vertical-align:baseline}.btn-text a:hover,.btn-text .btn.btn-link:hover{color:var(--bs-link-color)}.btn-circle{width:30px;height:30px;text-align:center;padding-left:0;padding-right:0;font-size:12px;line-height:1.42857143;border-radius:30px}.btn-circle i{margin:0}.btn-circle.btn-lg{width:50px;height:50px;font-size:18px;line-height:1.33;border-radius:25px}.btn-circle.btn-xl{width:70px;height:70px;font-size:24px;line-height:1.33;border-radius:35px}div.scoreboard{position:relative;padding:0}div.scoreboard:after,div.scoreboard:before{display:none;position:absolute;top:50%;margin-top:-7px;height:9px;font-size:10px;color:#bbbbbb}div.scoreboard:before{left:-6px;font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f104"}div.scoreboard:after{right:-8px;font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f105"}div.scoreboard.scroll-before:before{display:block}div.scoreboard.scroll-after:after{display:block}div.scoreboard:before,div.scoreboard:after{margin-top:-10px}div.scoreboard:before{left:7px}div.scoreboard:after{right:10px}div.scoreboard div.scoreboard-item{display:inline-block;margin-right:40px;margin-bottom:20px;vertical-align:top}div.scoreboard div.scoreboard-item:last-child{margin-right:0}div.scoreboard div.scoreboard-item.thumbnail-value{margin-right:20px}div.scoreboard div.scoreboard-item.thumbnail-value img{height:72px}div.scoreboard div.scoreboard-item span.scoreboard-colorpicker{width:0;height:0;background-color:transparent;border-right-color:var(--background-color);border-bottom-color:var(--background-color);border-top-color:var(--foreground-color);border-left-color:var(--foreground-color);border-width:10px;border-style:solid;border-radius:20px;display:inline-block}div.scoreboard div.scoreboard-item span.scoreboard-colorpicker:after{content:"";top:-10px;left:-10px;width:20px;height:20px;position:absolute;box-shadow:inset 0 0 0 1px rgba(var(--bs-body-bg-rgb), .2);border-radius:20px}div.scoreboard .control-chart{height:67px}div.scoreboard .control-chart ul{margin-left:77px;top:-2px}div.scoreboard .control-chart ul li{padding-left:18px}div.scoreboard .control-chart ul li>i{margin-left:-18px}div.scoreboard .control-chart .canvas+ul{margin-left:0}div.scoreboard .scoreboard-offset{padding-left:20px}body.slim-container div.scoreboard{padding:0 20px}.title-value h4{font-size:1.1rem;line-height:1;font-weight:normal;margin:0;color:var(--bs-heading-color)}.title-value p{color:var(--bs-body-color);margin:0;font-size:28px}.title-value p i,.title-value p:before{color:var(--oc-color-neutral);font-size:22px;position:relative;top:-2px}.title-value p.success{color:var(--oc-color-positive)}.title-value p.danger{color:var(--oc-color-negative)}.title-value p.negative:after,.title-value p.positive:after{font-size:17px;vertical-align:middle;position:relative;top:-3px;left:5px}.title-value p.negative{color:var(--oc-color-negative)}.title-value p.negative:after{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f103"}.title-value p.positive{color:var(--oc-color-positive)}.title-value p.positive:after{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f102"}.title-value p.description{color:var(--bs-secondary-color);line-height:100%;font-size:13px}.loading-indicator{padding:20px 20px 20px 60px;color:#999999;font-size:14px;font-weight:600;background:var(--bs-body-bg);text-align:left;z-index:10}.loading-indicator>span{--loader-size:40px;position:absolute;width:var(--loader-size);height:var(--loader-size);top:50%;margin-top:calc(var(--loader-size) / -2);left:0;display:block;border:3px solid var(--oc-primary-border);border-top-color:var(--oc-accent);border-radius:50%;animation:spin .8s linear infinite}.loading-indicator.is-transparent{background-color:transparent}.loading-indicator-container{position:relative;min-height:40px}.loading-indicator-container .loading-indicator{position:absolute;left:-1px;right:-1px;top:-1px;bottom:-1px;padding-top:0}.loading-indicator-container .loading-indicator>div{position:absolute;top:50%;margin-top:-0.65em}.form-buttons>.loading-indicator-container{padding:.3rem;margin:-0.3rem}.loading-indicator.is-opaque>span,.loading-indicator-container.is-opaque .loading-indicator>span{background-color:var(--bs-body-bg);border-color:var(--oc-primary-border);border-top-color:var(--oc-accent)}.loading-indicator-container.size-small{min-height:20px}.loading-indicator.size-small,.loading-indicator-container.size-small .loading-indicator{padding:16px 16px 16px 30px;font-size:11px}.loading-indicator.size-small>span,.loading-indicator-container.size-small .loading-indicator>span{--loader-size:20px;border-width:2px}.loading-indicator.indicator-center,.loading-indicator-container.indicator-center .loading-indicator{padding:20px}.loading-indicator.indicator-center>span,.loading-indicator-container.indicator-center .loading-indicator>span{left:50%;margin-left:calc(var(--loader-size) / -2);margin-top:calc(var(--loader-size) / -2)}.loading-indicator.indicator-center>div,.loading-indicator-container.indicator-center .loading-indicator>div{text-align:center;position:relative;margin-top:30px}.loading-indicator.indicator-inset,.loading-indicator-container.indicator-inset .loading-indicator{padding-left:80px}.loading-indicator.indicator-inset>span,.loading-indicator-container.indicator-inset .loading-indicator>span{left:20px}.loading-indicator-container.size-form-field,.loading-indicator-container.size-input-text{min-height:0}.loading-indicator-container.size-form-field .loading-indicator,.loading-indicator-container.size-input-text .loading-indicator{background-color:transparent;padding:0;margin:0}.loading-indicator-container.size-form-field .loading-indicator>span,.loading-indicator-container.size-input-text .loading-indicator>span{--loader-size:23px;padding:0;margin:0;left:auto;right:7px;top:6px;border-width:2px}.loading-indicator-container.size-form-field .loading-indicator>span{--loader-size:20px;top:0;right:0}.layout{display:table;table-layout:fixed;height:100%;width:100%}.layout>.layout-row{display:table-row;vertical-align:top;height:100%}.layout>.layout-row>.layout-cell{display:table-cell;vertical-align:top;height:100%}.layout>.layout-row>.layout-cell .layout-relative{position:relative;height:100%}.layout>.layout-row>.layout-cell .layout-absolute{position:absolute;height:100%;width:100%}.layout>.layout-row>.layout-cell.min-size{width:0}.layout>.layout-row>.layout-cell.min-height{height:0}.layout>.layout-row>.layout-cell.center{text-align:center}.layout>.layout-row>.layout-cell.middle{vertical-align:middle}.layout>.layout-row>.layout-cell .layout-relative{position:relative;height:100%}.layout>.layout-row>.layout-cell .layout-absolute{position:absolute;height:100%;width:100%}.layout>.layout-row>.layout-cell.min-size{width:0}.layout>.layout-row>.layout-cell.min-height{height:0}.layout>.layout-row>.layout-cell.center{text-align:center}.layout>.layout-row>.layout-cell.middle{vertical-align:middle}.layout>.layout-row.min-size{height:.1px}.layout>.layout-cell{display:table-cell;vertical-align:top;height:100%}.layout>.layout-cell .layout-relative{position:relative;height:100%}.layout>.layout-cell .layout-absolute{position:absolute;height:100%;width:100%}.layout>.layout-cell.min-size{width:0}.layout>.layout-cell.min-height{height:0}.layout>.layout-cell.center{text-align:center}.layout>.layout-cell.middle{vertical-align:middle}@media (max-width:768px){.hide-on-small{display:none !important}}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:var(--bs-emphasis-color);text-shadow:0 1px 0 var(--bs-body-bg);font-family:sans-serif;opacity:.2}.close:hover,.close:focus{color:var(--bs-emphasis-color);text-decoration:none;cursor:pointer;opacity:.5}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.custom-checkbox.nolabel label,.custom-radio.nolabel label{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.custom-checkbox,.custom-radio{padding-left:23px;margin-top:0}.custom-checkbox input[type=radio],.custom-radio input[type=radio],.custom-checkbox input[type=checkbox],.custom-radio input[type=checkbox]{position:absolute;overflow:hidden;clip:rect(0 0 0 0);width:1px;margin:-1px;padding:0;border:0;opacity:0}.custom-checkbox label,.custom-radio label{display:inline-block;cursor:pointer;position:relative;padding-left:20px;margin-right:15px;margin-left:-20px;font-size:1rem;user-select:none}.custom-checkbox label:before,.custom-radio label:before{content:"";display:inline-block;text-align:center;color:#FFFFFF;width:16px;height:16px;margin-right:15px;position:absolute;left:-3px;top:1px;background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.custom-checkbox label:hover:before,.custom-radio label:hover:before{border-color:var(--oc-border-focus)}.custom-checkbox label:active:before,.custom-radio label:active:before{border-color:#b0bdcd;border-width:2px}.custom-checkbox input[type=radio]+label:before,.custom-radio input[type=radio]+label:before{background-position:-19px -19px}.custom-checkbox input[type=radio]:checked+label:before,.custom-radio input[type=radio]:checked+label:before{background-position:0 -19px}.custom-checkbox input[type=radio][data-radio-color=green]:checked+label:before,.custom-radio input[type=radio][data-radio-color=green]:checked+label:before{background-position:-59px -19px}.custom-checkbox input[type=radio][data-radio-color=red]:checked+label:before,.custom-radio input[type=radio][data-radio-color=red]:checked+label:before{background-position:-79px -19px}.custom-checkbox input[type=checkbox]+label:before,.custom-radio input[type=checkbox]+label:before{background-position:-19px 0}.custom-checkbox input[type=checkbox]:checked+label:before,.custom-radio input[type=checkbox]:checked+label:before{background-position:0 0}.custom-checkbox input[type=checkbox]:indeterminate+label:before,.custom-radio input[type=checkbox]:indeterminate+label:before{background-position:-79px 0}.custom-checkbox input:disabled+label,.custom-radio input:disabled+label{cursor:not-allowed}.custom-checkbox input[type=checkbox]:disabled:checked+label:before,.custom-radio input[type=checkbox]:disabled:checked+label:before{background-position:-39px 0}.custom-checkbox input[type=radio]:disabled:checked+label:before,.custom-radio input[type=radio]:disabled:checked+label:before{background-position:-39px -19px}.custom-checkbox:focus,.custom-radio:focus{outline:none}.custom-checkbox:focus label:before,.custom-radio:focus label:before{box-shadow:0 0 0px 2px rgba(0,0,0,0.15)}.custom-checkbox p.form-text,.custom-radio p.form-text{margin-bottom:17px}.custom-radio label:before{-webkit-border-radius:16px;-moz-border-radius:16px;border-radius:16px}.custom-checkbox label:before{-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.inline-options .field-checkboxlist:not(.is-scrollable) .field-checkboxlist-inner{padding:10px 15px 15px 15px}.inline-options .field-checkboxlist:not(.is-scrollable) .custom-checkbox{display:inline-block;margin:0}.inline-options .field-checkboxlist:not(.is-scrollable) .custom-checkbox label{margin-bottom:0 !important;padding-top:10px}.inline-options .field-checkboxlist:not(.is-scrollable) .custom-checkbox label:before{top:10px}.inline-options.radio-field>label{display:block}.inline-options.radio-field .custom-radio{display:inline-block;margin-bottom:0}.switch-field .field-switch{padding-left:75px;float:left}.switch-field .field-switch>label{margin-top:3px}.custom-switch{display:block;width:65px;height:26px;position:relative;text-transform:uppercase;border:none;cursor:pointer;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.custom-switch *{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.custom-switch.disabled{opacity:.5}.custom-switch .slide-button{z-index:9;display:block;position:absolute;right:42px;top:3px;width:20px;height:20px;background-color:#f6f6f6;-webkit-border-radius:20px;-moz-border-radius:20px;border-radius:20px;-webkit-transition:all .1s;transition:all .1s}.custom-switch label,.custom-switch>span{line-height:23px;vertical-align:middle}.custom-switch label{z-index:8;width:100%;display:block;position:relative}.custom-switch input{z-index:10;position:absolute;left:0;top:0;opacity:0}.custom-switch input:checked~.slide-button{right:4px}.custom-switch input:checked~span{background-color:var(--bs-success)}.custom-switch input:checked~span span:first-of-type{color:#FFFFFF;display:block}.custom-switch input:checked~span span:last-of-type{color:#666666;display:none}.custom-switch input[disabled]~span{background-color:#bdc3c7 !important}.custom-switch input[disabled]~span,.custom-switch input[disabled]~span+a{cursor:default}.custom-switch>span{display:block;height:100%;position:absolute;left:0;width:100%;background-color:#72809d;font-size:12px;font-weight:600;user-select:none;-webkit-border-radius:20px;-moz-border-radius:20px;border-radius:20px}.custom-switch>span span{z-index:10;display:block;position:absolute;top:1px;left:-1px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.custom-switch>span span:last-child{left:28px;color:#FFFFFF;display:block}.custom-switch>span span:first-of-type{padding-left:13px;display:none;color:#666666}.oc-circle-thin:before,.icon-circle-thin:before{content:"\f10c"}.oc-undo:before,.icon-undo:before{content:"\e929"}.oc-redo:before,.icon-redo:before{content:"\e928"}.oc-icon-arrow-circle-o-down:before,.icon-arrow-circle-o-down:before{content:"\f0ab"}.oc-icon-arrow-circle-o-left:before,.icon-arrow-circle-o-left:before{content:"\f0a8"}.oc-icon-arrow-circle-o-right:before,.icon-arrow-circle-o-right:before{content:"\f0a9"}.oc-icon-arrow-circle-o-up:before,.icon-arrow-circle-o-up:before{content:"\f0aa"}.oc-icon-caret-square-o-down:before,.icon-caret-square-o-down:before{content:"\f150"}.oc-icon-caret-square-o-left:before,.icon-caret-square-o-left:before{content:"\f191"}.oc-icon-caret-square-o-right:before,.icon-caret-square-o-right:before{content:"\f152"}.oc-icon-caret-square-o-up:before,.icon-caret-square-o-up:before{content:"\f151"}.oc-icon-circle-o-notch:before,.icon-circle-o-notch:before{content:"\f1ce"}.oc-icon-hand-o-down:before,.icon-hand-o-down:before{content:"\f0a7"}.oc-icon-hand-o-left:before,.icon-hand-o-left:before{content:"\f0a5"}.oc-icon-hand-o-right:before,.icon-hand-o-right:before{content:"\f0a4"}.oc-icon-hand-o-up:before,.icon-hand-o-up:before{content:"\f0a6"}.oc-icon-thumbs-o-down:before,.icon-thumbs-o-down:before{content:"\f16a"}.oc-icon-thumbs-o-up:before,.icon-thumbs-o-up:before{content:"\f08c"}.oc-icon-address-book-o:before,.icon-address-book-o:before{content:"\f2ba"}.oc-icon-address-card-o:before,.icon-address-card-o:before{content:"\f2bc"}.oc-icon-bar-chart-o:before,.icon-bar-chart-o:before{content:"\f080"}.oc-icon-bell-o:before,.icon-bell-o:before{content:"\f0f3"}.oc-icon-bell-slash-o:before,.icon-bell-slash-o:before{content:"\f1f6"}.oc-icon-bookmark-o:before,.icon-bookmark-o:before{content:"\f02e"}.oc-icon-building-o:before,.icon-building-o:before{content:"\f1ad"}.oc-icon-calendar-check-o:before,.icon-calendar-check-o:before{content:"\f274"}.oc-icon-calendar-minus-o:before,.icon-calendar-minus-o:before{content:"\f272"}.oc-icon-calendar-o:before,.icon-calendar-o:before{content:"\f073"}.oc-icon-calendar-plus-o:before,.icon-calendar-plus-o:before{content:"\f271"}.oc-icon-calendar-times-o:before,.icon-calendar-times-o:before{content:"\f273"}.oc-icon-check-circle-o:before,.icon-check-circle-o:before{content:"\f058"}.oc-icon-check-square-o:before,.icon-check-square-o:before{content:"\f14a"}.oc-icon-circle-o:before,.icon-circle-o:before{content:"\f10c"}.oc-icon-clock-o:before,.icon-clock-o:before{content:"\f017"}.oc-icon-comment-o:before,.icon-comment-o:before{content:"\f075"}.oc-icon-commenting-o:before,.icon-commenting-o:before{content:"\f27b"}.oc-icon-comments-o:before,.icon-comments-o:before{content:"\f086"}.oc-icon-dot-circle-o:before,.icon-dot-circle-o:before{content:"\f192"}.oc-icon-drivers-license-o:before,.icon-drivers-license-o:before{content:"\f2c3"}.oc-icon-envelope-o:before,.icon-envelope-o:before{content:"\f0e0"}.oc-icon-envelope-open-o:before,.icon-envelope-open-o:before{content:"\f2b7"}.oc-icon-file-archive-o:before,.icon-file-archive-o:before{content:"\f1c6"}.oc-icon-file-audio-o:before,.icon-file-audio-o:before{content:"\f1c7"}.oc-icon-file-code-o:before,.icon-file-code-o:before{content:"\f1c9"}.oc-icon-file-excel-o:before,.icon-file-excel-o:before{content:"\f1c3"}.oc-icon-file-image-o:before,.icon-file-image-o:before{content:"\f1c5"}.oc-icon-file-movie-o:before,.icon-file-movie-o:before{content:"\f1c8"}.oc-icon-file-o:before,.icon-file-o:before{content:"\f15c"}.oc-icon-file-pdf-o:before,.icon-file-pdf-o:before{content:"\f1c1"}.oc-icon-file-photo-o:before,.icon-file-photo-o:before{content:"\f1ca"}.oc-icon-file-picture-o:before,.icon-file-picture-o:before{content:"\f1cc"}.oc-icon-file-powerpoint-o:before,.icon-file-powerpoint-o:before{content:"\f1c4"}.oc-icon-file-sound-o:before,.icon-file-sound-o:before{content:"\f1cd"}.oc-icon-file-text-o:before,.icon-file-text-o:before{content:"\f15d"}.oc-icon-file-video-o:before,.icon-file-video-o:before{content:"\f1cf"}.oc-icon-file-word-o:before,.icon-file-word-o:before{content:"\f1c2"}.oc-icon-file-zip-o:before,.icon-file-zip-o:before{content:"\f1d0"}.oc-icon-files-o:before,.icon-files-o:before{content:"\f0c6"}.oc-icon-flag-o:before,.icon-flag-o:before{content:"\f024"}.oc-icon-floppy-o:before,.icon-floppy-o:before{content:"\f0c7"}.oc-icon-folder-o:before,.icon-folder-o:before{content:"\f114"}.oc-icon-folder-open-o:before,.icon-folder-open-o:before{content:"\f115"}.oc-icon-frown-o:before,.icon-frown-o:before{content:"\f119"}.oc-icon-futbol-o:before,.icon-futbol-o:before{content:"\f1e3"}.oc-icon-hand-grab-o:before,.icon-hand-grab-o:before{content:"\f255"}.oc-icon-hand-lizard-o:before,.icon-hand-lizard-o:before{content:"\f258"}.oc-icon-hand-paper-o:before,.icon-hand-paper-o:before{content:"\f256"}.oc-icon-hand-peace-o:before,.icon-hand-peace-o:before{content:"\f25b"}.oc-icon-hand-pointer-o:before,.icon-hand-pointer-o:before{content:"\f25a"}.oc-icon-hand-rock-o:before,.icon-hand-rock-o:before{content:"\f257"}.oc-icon-hand-scissors-o:before,.icon-hand-scissors-o:before{content:"\f259"}.oc-icon-hand-spock-o:before,.icon-hand-spock-o:before{content:"\f25c"}.oc-icon-hand-stop-o:before,.icon-hand-stop-o:before{content:"\f25d"}.oc-icon-handshake-o:before,.icon-handshake-o:before{content:"\f2b8"}.oc-icon-hdd-o:before,.icon-hdd-o:before{content:"\f0a0"}.oc-icon-heart-o:before,.icon-heart-o:before{content:"\f004"}.oc-icon-hospital-o:before,.icon-hospital-o:before{content:"\f0f8"}.oc-icon-hourglass-o:before,.icon-hourglass-o:before{content:"\f250"}.oc-icon-id-card-o:before,.icon-id-card-o:before{content:"\f2c6"}.oc-icon-keyboard-o:before,.icon-keyboard-o:before{content:"\f11c"}.oc-icon-lemon-o:before,.icon-lemon-o:before{content:"\f094"}.oc-icon-lightbulb-o:before,.icon-lightbulb-o:before{content:"\f0eb"}.oc-icon-map-o:before,.icon-map-o:before{content:"\f278"}.oc-icon-meh-o:before,.icon-meh-o:before{content:"\f11a"}.oc-icon-minus-square-o:before,.icon-minus-square-o:before{content:"\f147"}.oc-icon-moon-o:before,.icon-moon-o:before{content:"\f186"}.oc-icon-newspaper-o:before,.icon-newspaper-o:before{content:"\f1ea"}.oc-icon-paper-plane-o:before,.icon-paper-plane-o:before{content:"\f1dd"}.oc-icon-pause-circle-o:before,.icon-pause-circle-o:before{content:"\f28c"}.oc-icon-pencil-square-o:before,.icon-pencil-square-o:before{content:"\f14b"}.oc-icon-picture-o:before,.icon-picture-o:before{content:"\f03f"}.oc-icon-play-circle-o:before,.icon-play-circle-o:before{content:"\f144"}.oc-icon-plus-square-o:before,.icon-plus-square-o:before{content:"\f196"}.oc-icon-question-circle-o:before,.icon-question-circle-o:before{content:"\f059"}.oc-icon-send-o:before,.icon-send-o:before{content:"\f1e7"}.oc-icon-share-square-o:before,.icon-share-square-o:before{content:"\f14d"}.oc-icon-smile-o:before,.icon-smile-o:before{content:"\f118"}.oc-icon-snowflake-o:before,.icon-snowflake-o:before{content:"\f2dc"}.oc-icon-soccer-ball-o:before,.icon-soccer-ball-o:before{content:"\f1ef"}.oc-icon-square-o:before,.icon-square-o:before{content:"\f0d2"}.oc-icon-star-half-o:before,.icon-star-half-o:before{content:"\f12f"}.oc-icon-star-o:before,.icon-star-o:before{content:"\f006"}.oc-icon-sticky-note-o:before,.icon-sticky-note-o:before{content:"\f26d"}.oc-icon-stop-circle-o:before,.icon-stop-circle-o:before{content:"\f28e"}.oc-icon-sun-o:before,.icon-sun-o:before{content:"\f189"}.oc-icon-times-circle-o:before,.icon-times-circle-o:before{content:"\f057"}.oc-icon-times-rectangle-o:before,.icon-times-rectangle-o:before{content:"\f2de"}.oc-icon-trash-o:before,.icon-trash-o:before{content:"\f1f8"}.oc-icon-user-circle-o:before,.icon-user-circle-o:before{content:"\f2be"}.oc-icon-user-o:before,.icon-user-o:before{content:"\f007"}.oc-icon-vcard-o:before,.icon-vcard-o:before{content:"\f2df"}.oc-icon-window-close-o:before,.icon-window-close-o:before{content:"\f2e1"}.storm-icon{background-size:300px 63px;background-image:url('../foundation/migrate/images/storm-icons.png')}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){.storm-icon{background-image:url('../foundation/migrate/images/storm-icons@2x.png')}}.storm-icon-pseudo:before{content:'';background-size:300px 63px;background-image:url('../foundation/migrate/images/storm-icons.png')}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){.storm-icon-pseudo:before{background-image:url('../foundation/migrate/images/storm-icons@2x.png')}}.control-breadcrumb ul{padding:0 0 20px 0;margin:0;font-size:0}.control-breadcrumb ul li{font-size:14px;list-style:none;margin:0;padding:0;display:inline-block;position:relative;color:var(--bs-link-color)}.control-breadcrumb ul li a{display:inline-block;color:var(--bs-link-color);text-decoration:underline}.control-breadcrumb ul li a:hover{color:var(--bs-link-color)}.control-breadcrumb ul li:after{content:var(--bs-breadcrumb-divider, "/");position:relative;display:inline-block;margin:0 5px;color:var(--bs-secondary-color)}.control-breadcrumb ul li:last-child{background-color:transparent;color:var(--bs-secondary-color)}.control-breadcrumb ul li:last-child:after{display:none}body.breadcrumb-flush .control-breadcrumb,.control-breadcrumb.breadcrumb-flush{margin-bottom:0}body.compact-container .control-breadcrumb{margin-top:20px;margin-left:20px;margin-right:20px}body.compact-container .control-breadcrumb>ul{padding-bottom:0}body.slim-container .control-breadcrumb{margin-left:20px;margin-right:20px}.modal-content .modal-header.flex-row-reverse{justify-content:space-between}@font-face{font-family:'octo-icon-migrate';src:url('../foundation/migrate/vendor/octoicons/octo-icon.eot?symvb&v=1.0.1');src:url('../foundation/migrate/vendor/octoicons/octo-icon.eot?symvb#iefix&v=1.0.1') format('embedded-opentype'),url('../foundation/migrate/vendor/octoicons/octo-icon.ttf?symvb&v=1.0.1') format('truetype'),url('../foundation/migrate/vendor/octoicons/octo-icon.woff?symvb&v=1.0.1') format('woff'),url('../foundation/migrate/vendor/octoicons/octo-icon.svg?symvb#octo-icon&v=1.0.1') format('svg');font-weight:normal;font-style:normal;font-display:block}[class^="octo-icon-"],[class*=" octo-icon-"]{font-family:'octo-icon-migrate' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.octo-icon-size-20{font-size:20px}.octo-icon-earth:before{content:"\eb55"}.octo-icon-language-letters:before{content:"\eb56"}.octo-icon-database-flash:before{content:"\eb54"}.octo-icon-collapse:before{content:"\eb26"}.octo-icon-music:before{content:"\e964"}.octo-icon-envelope:before{content:"\e965"}.octo-icon-heart:before{content:"\e966"}.octo-icon-star:before{content:"\e967"}.octo-icon-user:before{content:"\e968"}.octo-icon-film:before{content:"\e969"}.octo-icon-check:before{content:"\e96a"}.octo-icon-remove:before{content:"\e96b"}.octo-icon-close:before{content:"\e96c"}.octo-icon-search-minus:before{content:"\e96d"}.octo-icon-search-plus:before{content:"\e96e"}.octo-icon-power-off:before{content:"\e96f"}.octo-icon-signal:before{content:"\e970"}.octo-icon-gear:before{content:"\e971"}.octo-icon-cog:before{content:"\e972"}.octo-icon-home:before{content:"\e973"}.octo-icon-file:before{content:"\e974"}.octo-icon-clock:before{content:"\e975"}.octo-icon-road:before{content:"\e976"}.octo-icon-arrow-circle-o-down:before{content:"\e977"}.octo-icon-arrow-circle-o-up:before{content:"\e978"}.octo-icon-inbox:before{content:"\e979"}.octo-icon-play-circle:before{content:"\e97a"}.octo-icon-rotate-right:before{content:"\e97b"}.octo-icon-repeat:before{content:"\e97c"}.octo-icon-refresh:before{content:"\e97d"}.octo-icon-list-alt:before{content:"\e97e"}.octo-icon-headphones:before{content:"\e97f"}.octo-icon-volume-off:before{content:"\e980"}.octo-icon-volume-down:before{content:"\e981"}.octo-icon-volume-up:before{content:"\e982"}.octo-icon-qrcode:before{content:"\e983"}.octo-icon-barcode:before{content:"\e984"}.octo-icon-tags:before{content:"\e985"}.octo-icon-book:before{content:"\e986"}.octo-icon-bookmark:before{content:"\e987"}.octo-icon-print:before{content:"\e988"}.octo-icon-camera:before{content:"\e989"}.octo-icon-font:before{content:"\e98a"}.octo-icon-text-height:before{content:"\e98b"}.octo-icon-text-width:before{content:"\e98c"}.octo-icon-video-camera:before{content:"\e98d"}.octo-icon-photo:before{content:"\e98e"}.octo-icon-image:before{content:"\e98f"}.octo-icon-picture:before{content:"\e990"}.octo-icon-pencil:before{content:"\e991"}.octo-icon-map-marker:before{content:"\e992"}.octo-icon-adjust:before{content:"\e993"}.octo-icon-edit:before{content:"\e994"}.octo-icon-pencil-square:before{content:"\e995"}.octo-icon-share-square:before{content:"\e996"}.octo-icon-check-square:before{content:"\e997"}.octo-icon-arrows:before{content:"\e998"}.octo-icon-pause:before{content:"\e999"}.octo-icon-stop:before{content:"\e99a"}.octo-icon-eject:before{content:"\e99b"}.octo-icon-chevron-right:before{content:"\e99c"}.octo-icon-plus-circle:before{content:"\e99d"}.octo-icon-minus-circle:before{content:"\e99e"}.octo-icon-times-circle:before{content:"\e99f"}.octo-icon-check-circle:before{content:"\e9a0"}.octo-icon-question-circle:before{content:"\e9a1"}.octo-icon-info-circle:before{content:"\e9a2"}.octo-icon-crosshairs:before{content:"\e9a3"}.octo-icon-times-circle-o:before{content:"\e9a4"}.octo-icon-check-circle-o:before{content:"\e9a5"}.octo-icon-ban:before{content:"\e9a6"}.octo-icon-arrow-left:before{content:"\e9a7"}.octo-icon-arrow-right:before{content:"\e9a8"}.octo-icon-arrow-up:before{content:"\e9a9"}.octo-icon-arrow-down:before{content:"\e9aa"}.octo-icon-mail-forward:before{content:"\e9ab"}.octo-icon-share:before{content:"\e9ac"}.octo-icon-expand:before{content:"\e9ad"}.octo-icon-compress:before{content:"\e9ae"}.octo-icon-minus:before{content:"\e9af"}.octo-icon-asterisk:before{content:"\e9b0"}.octo-icon-exclamation-circle:before{content:"\e9b1"}.octo-icon-gift:before{content:"\e9b2"}.octo-icon-leaf:before{content:"\e9b3"}.octo-icon-fire:before{content:"\e9b4"}.octo-icon-eye:before{content:"\e9b5"}.octo-icon-eye-slash:before{content:"\e9b6"}.octo-icon-calendar:before{content:"\e9b7"}.octo-icon-comment:before{content:"\e9b8"}.octo-icon-chevron-down:before{content:"\e9b9"}.octo-icon-retweet:before{content:"\e9ba"}.octo-icon-shopping-cart:before{content:"\e9bb"}.octo-icon-folder:before{content:"\e9bc"}.octo-icon-folder-open:before{content:"\e9bd"}.octo-icon-arrows-v:before{content:"\e9be"}.octo-icon-arrows-h:before{content:"\e9bf"}.octo-icon-bar-chart-o:before{content:"\e9c0"}.octo-icon-bar-chart:before{content:"\e9c1"}.octo-icon-twitter-square:before{content:"\e9c2"}.octo-icon-facebook-square:before{content:"\e9c3"}.octo-icon-camera-retro:before{content:"\e9c4"}.octo-icon-key:before{content:"\e9c5"}.octo-icon-cogs:before{content:"\e9c6"}.octo-icon-comments:before{content:"\e9c7"}.octo-icon-thumbs-o-down:before{content:"\e9c8"}.octo-icon-heart-o:before{content:"\e9c9"}.octo-icon-sign-out:before{content:"\e9ca"}.octo-icon-linkedin-square:before{content:"\e9cb"}.octo-icon-thumb-tack:before{content:"\e9cc"}.octo-icon-external-link:before{content:"\e9cd"}.octo-icon-sign-in:before{content:"\e9ce"}.octo-icon-trophy:before{content:"\e9cf"}.octo-icon-upload:before{content:"\e9d0"}.octo-icon-lemon-o:before{content:"\e9d1"}.octo-icon-phone:before{content:"\e9d2"}.octo-icon-square-o:before{content:"\e9d3"}.octo-icon-bookmark-o:before{content:"\e9d4"}.octo-icon-phone-square:before{content:"\e9d5"}.octo-icon-twitter:before{content:"\e9d6"}.octo-icon-facebook-f:before{content:"\e9d7"}.octo-icon-facebook:before{content:"\e9d8"}.octo-icon-unlock:before{content:"\e9d9"}.octo-icon-credit-card:before{content:"\e9da"}.octo-icon-feed:before{content:"\e9db"}.octo-icon-rss:before{content:"\e9dc"}.octo-icon-hdd-o:before{content:"\e9dd"}.octo-icon-certificate:before{content:"\e9de"}.octo-icon-arrow-circle-left:before{content:"\e9df"}.octo-icon-arrow-circle-down:before{content:"\e9e0"}.octo-icon-wrench:before{content:"\e9e1"}.octo-icon-tasks:before{content:"\e9e2"}.octo-icon-filter:before{content:"\e9e3"}.octo-icon-briefcase:before{content:"\e9e4"}.octo-icon-arrows-alt:before{content:"\e9e5"}.octo-icon-group:before{content:"\e9e6"}.octo-icon-chain:before{content:"\e9e7"}.octo-icon-flask:before{content:"\e9e8"}.octo-icon-cut:before{content:"\e9e9"}.octo-icon-scissors:before{content:"\e9ea"}.octo-icon-copy:before{content:"\e9eb"}.octo-icon-files-o:before{content:"\e9ec"}.octo-icon-square:before{content:"\e9ed"}.octo-icon-navicon:before{content:"\e9ee"}.octo-icon-reorder:before{content:"\e9ef"}.octo-icon-bars:before{content:"\e9f0"}.octo-icon-list-ul:before{content:"\e9f1"}.octo-icon-list-ol:before{content:"\e9f2"}.octo-icon-strikethrough:before{content:"\e9f3"}.octo-icon-magic:before{content:"\e9f4"}.octo-icon-truck:before{content:"\e9f5"}.octo-icon-pinterest:before{content:"\e9f6"}.octo-icon-pinterest-square:before{content:"\e9f7"}.octo-icon-google-plus-square:before{content:"\e9f8"}.octo-icon-google-plus:before{content:"\e9f9"}.octo-icon-caret-down:before{content:"\e9fa"}.octo-icon-caret-up:before{content:"\e9fb"}.octo-icon-caret-left:before{content:"\e9fc"}.octo-icon-caret-right:before{content:"\e9fd"}.octo-icon-columns:before{content:"\e9fe"}.octo-icon-sort:before{content:"\e9ff"}.octo-icon-sort-down:before{content:"\ea00"}.octo-icon-sort-desc:before{content:"\ea01"}.octo-icon-sort-up:before{content:"\ea02"}.octo-icon-sort-asc:before{content:"\ea03"}.octo-icon-linkedin:before{content:"\ea04"}.octo-icon-rotate-left:before{content:"\ea05"}.octo-icon-legal:before{content:"\ea06"}.octo-icon-gavel:before{content:"\ea07"}.octo-icon-dashboard:before{content:"\ea08"}.octo-icon-tachometer:before{content:"\ea09"}.octo-icon-comment-o:before{content:"\ea0a"}.octo-icon-comments-o:before{content:"\ea0b"}.octo-icon-sitemap:before{content:"\ea0c"}.octo-icon-umbrella:before{content:"\ea0d"}.octo-icon-clipboard:before{content:"\ea0e"}.octo-icon-lightbulb-o:before{content:"\ea0f"}.octo-icon-exchange:before{content:"\ea10"}.octo-icon-user-md:before{content:"\ea11"}.octo-icon-stethoscope:before{content:"\ea12"}.octo-icon-suitcase:before{content:"\ea13"}.octo-icon-bell-o:before{content:"\ea14"}.octo-icon-coffee:before{content:"\ea15"}.octo-icon-file-text-o:before{content:"\ea16"}.octo-icon-building-o:before{content:"\ea17"}.octo-icon-hospital-o:before{content:"\ea18"}.octo-icon-ambulance:before{content:"\ea19"}.octo-icon-medkit:before{content:"\ea1a"}.octo-icon-fighter-jet:before{content:"\ea1b"}.octo-icon-beer:before{content:"\ea1c"}.octo-icon-h-square:before{content:"\ea1d"}.octo-icon-plus-square:before{content:"\ea1e"}.octo-icon-angle-double-left:before{content:"\ea1f"}.octo-icon-angle-double-right:before{content:"\ea20"}.octo-icon-angle-double-up:before{content:"\ea21"}.octo-icon-angle-double-down:before{content:"\ea22"}.octo-icon-angle-left:before{content:"\ea23"}.octo-icon-angle-right:before{content:"\ea24"}.octo-icon-desktop:before{content:"\ea25"}.octo-icon-laptop:before{content:"\ea26"}.octo-icon-tablet:before{content:"\ea27"}.octo-icon-mobile-phone:before{content:"\ea28"}.octo-icon-mobile:before{content:"\ea29"}.octo-icon-circle-o:before{content:"\ea2a"}.octo-icon-quote-left:before{content:"\ea2b"}.octo-icon-quote-right:before{content:"\ea2c"}.octo-icon-spinner:before{content:"\ea2d"}.octo-icon-circle:before{content:"\ea2e"}.octo-icon-mail-reply:before{content:"\ea2f"}.octo-icon-reply:before{content:"\ea30"}.octo-icon-folder-o:before{content:"\ea31"}.octo-icon-folder-open-o:before{content:"\ea32"}.octo-icon-smile-o:before{content:"\ea33"}.octo-icon-frown-o:before{content:"\ea34"}.octo-icon-meh-o:before{content:"\ea35"}.octo-icon-keyboard-o:before{content:"\ea36"}.octo-icon-flag-checkered:before{content:"\ea37"}.octo-icon-terminal:before{content:"\ea38"}.octo-icon-code:before{content:"\ea39"}.octo-icon-mail-reply-all:before{content:"\ea3a"}.octo-icon-reply-all:before{content:"\ea3b"}.octo-icon-location-arrow:before{content:"\ea3c"}.octo-icon-crop:before{content:"\ea3d"}.octo-icon-chain-broken:before{content:"\ea3e"}.octo-icon-question:before{content:"\ea3f"}.octo-icon-exclamation:before{content:"\ea40"}.octo-icon-superscript:before{content:"\ea41"}.octo-icon-subscript:before{content:"\ea42"}.octo-icon-puzzle-piece:before{content:"\ea43"}.octo-icon-microphone:before{content:"\ea44"}.octo-icon-microphone-slash:before{content:"\ea45"}.octo-icon-shield:before{content:"\ea46"}.octo-icon-calendar-o:before{content:"\ea47"}.octo-icon-fire-extinguisher:before{content:"\ea48"}.octo-icon-rocket:before{content:"\ea49"}.octo-icon-chevron-circle-left:before{content:"\ea4a"}.octo-icon-chevron-circle-right:before{content:"\ea4b"}.octo-icon-chevron-circle-up:before{content:"\ea4c"}.octo-icon-chevron-circle-down:before{content:"\ea4d"}.octo-icon-html5:before{content:"\ea4e"}.octo-icon-css3:before{content:"\ea4f"}.octo-icon-anchor:before{content:"\ea50"}.octo-icon-unlock-alt:before{content:"\ea51"}.octo-icon-bullseye:before{content:"\ea52"}.octo-icon-rss-square:before{content:"\ea53"}.octo-icon-minus-square:before{content:"\ea54"}.octo-icon-minus-square-o:before{content:"\ea55"}.octo-icon-level-down:before{content:"\ea56"}.octo-icon-toggle-down:before{content:"\ea57"}.octo-icon-caret-square-o-down:before{content:"\ea58"}.octo-icon-toggle-up:before{content:"\ea59"}.octo-icon-caret-square-o-up:before{content:"\ea5a"}.octo-icon-caret-square-o-right:before{content:"\ea5b"}.octo-icon-euro:before{content:"\ea5c"}.octo-icon-sort-numeric-asc:before{content:"\ea5d"}.octo-icon-sort-numeric-desc:before{content:"\ea5e"}.octo-icon-eur:before{content:"\ea5f"}.octo-icon-gbp:before{content:"\ea60"}.octo-icon-dollar:before{content:"\ea61"}.octo-icon-usd:before{content:"\ea62"}.octo-icon-bitcoin:before{content:"\ea63"}.octo-icon-btc:before{content:"\ea64"}.octo-icon-file-text:before{content:"\ea65"}.octo-icon-thumbs-up:before{content:"\ea66"}.octo-icon-thumbs-down:before{content:"\ea67"}.octo-icon-youtube-square:before{content:"\ea68"}.octo-icon-youtube:before{content:"\ea69"}.octo-icon-xing:before{content:"\ea6a"}.octo-icon-xing-square:before{content:"\ea6b"}.octo-icon-youtube-play:before{content:"\ea6c"}.octo-icon-dropbox:before{content:"\ea6d"}.octo-icon-stack-overflow:before{content:"\ea6e"}.octo-icon-instagram:before{content:"\ea6f"}.octo-icon-flickr:before{content:"\ea70"}.octo-icon-tumblr:before{content:"\ea71"}.octo-icon-tumblr-square:before{content:"\ea72"}.octo-icon-long-arrow-down:before{content:"\ea73"}.octo-icon-long-arrow-up:before{content:"\ea74"}.octo-icon-long-arrow-left:before{content:"\ea75"}.octo-icon-long-arrow-right:before{content:"\ea76"}.octo-icon-apple:before{content:"\ea77"}.octo-icon-windows:before{content:"\ea78"}.octo-icon-android:before{content:"\ea79"}.octo-icon-dribbble:before{content:"\ea7a"}.octo-icon-foursquare:before{content:"\ea7b"}.octo-icon-female:before{content:"\ea7c"}.octo-icon-male:before{content:"\ea7d"}.octo-icon-sun-o:before{content:"\ea7e"}.octo-icon-moon-o:before{content:"\ea7f"}.octo-icon-archive:before{content:"\ea80"}.octo-icon-bug:before{content:"\ea81"}.octo-icon-vk:before{content:"\ea82"}.octo-icon-weibo:before{content:"\ea83"}.octo-icon-renren:before{content:"\ea84"}.octo-icon-arrow-circle-o-right:before{content:"\ea85"}.octo-icon-arrow-circle-o-left:before{content:"\ea86"}.octo-icon-caret-square-o-left:before{content:"\ea87"}.octo-icon-dot-circle-o:before{content:"\ea88"}.octo-icon-wheelchair:before{content:"\ea89"}.octo-icon-plus-square-o:before{content:"\ea8a"}.octo-icon-space-shuttle:before{content:"\ea8b"}.octo-icon-envelope-square:before{content:"\ea8c"}.octo-icon-institution:before{content:"\ea8d"}.octo-icon-bank:before{content:"\ea8e"}.octo-icon-university:before{content:"\ea8f"}.octo-icon-mortar-board:before{content:"\ea90"}.octo-icon-yahoo:before{content:"\ea91"}.octo-icon-reddit:before{content:"\ea92"}.octo-icon-reddit-square:before{content:"\ea93"}.octo-icon-delicious:before{content:"\ea94"}.octo-icon-digg:before{content:"\ea95"}.octo-icon-drupal:before{content:"\ea96"}.octo-icon-language:before{content:"\ea97"}.octo-icon-building:before{content:"\ea98"}.octo-icon-paw:before{content:"\ea99"}.octo-icon-spoon:before{content:"\ea9a"}.octo-icon-steam:before{content:"\ea9b"}.octo-icon-steam-square:before{content:"\ea9c"}.octo-icon-automobile:before{content:"\ea9d"}.octo-icon-car:before{content:"\ea9e"}.octo-icon-cab:before{content:"\ea9f"}.octo-icon-taxi:before{content:"\eaa0"}.octo-icon-tree:before{content:"\eaa1"}.octo-icon-database:before{content:"\eaa2"}.octo-icon-file-pdf-o:before{content:"\eaa3"}.octo-icon-file-word-o:before{content:"\eaa4"}.octo-icon-file-excel-o:before{content:"\eaa5"}.octo-icon-file-powerpoint-o:before{content:"\eaa6"}.octo-icon-file-photo-o:before{content:"\eaa7"}.octo-icon-file-picture-o:before{content:"\eaa8"}.octo-icon-file-image-o:before{content:"\eaa9"}.octo-icon-file-zip-o:before{content:"\eaaa"}.octo-icon-file-archive-o:before{content:"\eaab"}.octo-icon-file-sound-o:before{content:"\eaac"}.octo-icon-file-audio-o:before{content:"\eaad"}.octo-icon-file-video-o:before{content:"\eaae"}.octo-icon-file-code-o:before{content:"\eaaf"}.octo-icon-vine:before{content:"\eab0"}.octo-icon-life-bouy:before{content:"\eab1"}.octo-icon-life-buoy:before{content:"\eab2"}.octo-icon-life-saver:before{content:"\eab3"}.octo-icon-support:before{content:"\eab4"}.octo-icon-life-ring:before{content:"\eab5"}.octo-icon-ra:before{content:"\eab6"}.octo-icon-empire:before{content:"\eab7"}.octo-icon-tencent-weibo:before{content:"\eab8"}.octo-icon-send:before{content:"\eab9"}.octo-icon-paper-plane:before{content:"\eaba"}.octo-icon-send-o:before{content:"\eabb"}.octo-icon-paper-plane-o:before{content:"\eabc"}.octo-icon-history:before{content:"\eabd"}.octo-icon-circle-thin:before{content:"\eabe"}.octo-icon-paragraph:before{content:"\eabf"}.octo-icon-sliders:before{content:"\eac0"}.octo-icon-share-alt:before{content:"\eac1"}.octo-icon-share-alt-square:before{content:"\eac2"}.octo-icon-bomb:before{content:"\eac3"}.octo-icon-soccer-ball-o:before{content:"\eac4"}.octo-icon-futbol-o:before{content:"\eac5"}.octo-icon-tty:before{content:"\eac6"}.octo-icon-binoculars:before{content:"\eac7"}.octo-icon-twitch:before{content:"\eac8"}.octo-icon-yelp:before{content:"\eac9"}.octo-icon-newspaper-o:before{content:"\eaca"}.octo-icon-wifi:before{content:"\eacb"}.octo-icon-calculator:before{content:"\eacc"}.octo-icon-paypal:before{content:"\eacd"}.octo-icon-cc-visa:before{content:"\eace"}.octo-icon-cc-mastercard:before{content:"\eacf"}.octo-icon-cc-discover:before{content:"\ead0"}.octo-icon-cc-amex:before{content:"\ead1"}.octo-icon-cc-paypal:before{content:"\ead2"}.octo-icon-cc-stripe:before{content:"\ead3"}.octo-icon-bell-slash:before{content:"\ead4"}.octo-icon-bell-slash-o:before{content:"\ead5"}.octo-icon-at:before{content:"\ead6"}.octo-icon-paint-brush:before{content:"\ead7"}.octo-icon-birthday-cake:before{content:"\ead8"}.octo-icon-area-chart:before{content:"\ead9"}.octo-icon-pie-chart:before{content:"\eada"}.octo-icon-line-chart:before{content:"\eadb"}.octo-icon-lastfm:before{content:"\eadc"}.octo-icon-lastfm-square:before{content:"\eadd"}.octo-icon-toggle-off:before{content:"\eade"}.octo-icon-toggle-on:before{content:"\eadf"}.octo-icon-bicycle:before{content:"\eae0"}.octo-icon-bus:before{content:"\eae1"}.octo-icon-cc:before{content:"\eae2"}.octo-icon-cart-plus:before{content:"\eae3"}.octo-icon-cart-arrow-down:before{content:"\eae4"}.octo-icon-diamond:before{content:"\eae5"}.octo-icon-ship:before{content:"\eae6"}.octo-icon-street-view:before{content:"\eae7"}.octo-icon-heartbeat:before{content:"\eae8"}.octo-icon-venus:before{content:"\eae9"}.octo-icon-mars:before{content:"\eaea"}.octo-icon-transgender:before{content:"\eaeb"}.octo-icon-transgender-alt:before{content:"\eaec"}.octo-icon-facebook-official:before{content:"\eaed"}.octo-icon-pinterest-p:before{content:"\eaee"}.octo-icon-whatsapp:before{content:"\eaef"}.octo-icon-server:before{content:"\eaf0"}.octo-icon-user-plus:before{content:"\eaf1"}.octo-icon-hotel:before{content:"\eaf2"}.octo-icon-bed:before{content:"\eaf3"}.octo-icon-train:before{content:"\eaf4"}.octo-icon-subway:before{content:"\eaf5"}.octo-icon-battery-4:before{content:"\eaf6"}.octo-icon-battery:before{content:"\eaf7"}.octo-icon-battery-full:before{content:"\eaf8"}.octo-icon-battery-3:before{content:"\eaf9"}.octo-icon-battery-three-quarters:before{content:"\eafa"}.octo-icon-battery-2:before{content:"\eafb"}.octo-icon-battery-half:before{content:"\eafc"}.octo-icon-battery-1:before{content:"\eafd"}.octo-icon-battery-quarter:before{content:"\eafe"}.octo-icon-battery-0:before{content:"\eaff"}.octo-icon-battery-empty:before{content:"\eb00"}.octo-icon-mouse-pointer:before{content:"\eb01"}.octo-icon-i-cursor:before{content:"\eb02"}.octo-icon-clone:before{content:"\eb03"}.octo-icon-balance-scale:before{content:"\eb04"}.octo-icon-hourglass-1:before{content:"\eb05"}.octo-icon-hourglass-start:before{content:"\eb06"}.octo-icon-hourglass-half:before{content:"\eb07"}.octo-icon-odnoklassniki:before{content:"\eb08"}.octo-icon-odnoklassniki-square:before{content:"\eb09"}.octo-icon-wikipedia-w:before{content:"\eb0a"}.octo-icon-tv:before{content:"\eb0b"}.octo-icon-television:before{content:"\eb0c"}.octo-icon-px:before{content:"\eb0d"}.octo-icon-amazon:before{content:"\eb0e"}.octo-icon-calendar-plus-o:before{content:"\eb0f"}.octo-icon-calendar-minus-o:before{content:"\eb10"}.octo-icon-calendar-times-o:before{content:"\eb11"}.octo-icon-calendar-check-o:before{content:"\eb12"}.octo-icon-industry:before{content:"\eb13"}.octo-icon-map-signs:before{content:"\eb14"}.octo-icon-commenting:before{content:"\eb15"}.octo-icon-black-tie:before{content:"\eb16"}.octo-icon-reddit-alien:before{content:"\eb17"}.octo-icon-credit-card-alt:before{content:"\eb18"}.octo-icon-scribd:before{content:"\eb19"}.octo-icon-usb:before{content:"\eb1a"}.octo-icon-pause-circle:before{content:"\eb1b"}.octo-icon-pause-circle-o:before{content:"\eb1c"}.octo-icon-stop-circle:before{content:"\eb1d"}.octo-icon-stop-circle-o:before{content:"\eb1e"}.octo-icon-shopping-bag:before{content:"\eb1f"}.octo-icon-shopping-basket:before{content:"\eb20"}.octo-icon-hashtag:before{content:"\eb21"}.octo-icon-bluetooth-b:before{content:"\eb22"}.octo-icon-wheelchair-alt:before{content:"\eb23"}.octo-icon-question-circle-o:before{content:"\eb24"}.octo-icon-blind:before{content:"\eb25"}.octo-icon-volume-control-phone:before{content:"\eb26"}.octo-icon-braille:before{content:"\eb27"}.octo-icon-snapchat:before{content:"\eb28"}.octo-icon-snapchat-ghost:before{content:"\eb29"}.octo-icon-snapchat-square:before{content:"\eb2a"}.octo-icon-google-plus-circle:before{content:"\eb2b"}.octo-icon-google-plus-official:before{content:"\eb2c"}.octo-icon-handshake-o:before{content:"\eb2d"}.octo-icon-envelope-open:before{content:"\eb2e"}.octo-icon-envelope-open-o:before{content:"\eb2f"}.octo-icon-address-book:before{content:"\eb30"}.octo-icon-address-book-o:before{content:"\eb31"}.octo-icon-vcard:before{content:"\eb32"}.octo-icon-address-card:before{content:"\eb33"}.octo-icon-vcard-o:before{content:"\eb34"}.octo-icon-address-card-o:before{content:"\eb35"}.octo-icon-user-circle:before{content:"\eb36"}.octo-icon-user-circle-o:before{content:"\eb37"}.octo-icon-user-o:before{content:"\eb38"}.octo-icon-id-badge:before{content:"\eb39"}.octo-icon-drivers-license:before{content:"\eb3a"}.octo-icon-id-card:before{content:"\eb3b"}.octo-icon-drivers-license-o:before{content:"\eb3c"}.octo-icon-id-card-o:before{content:"\eb3d"}.octo-icon-quora:before{content:"\eb3e"}.octo-icon-thermometer-4:before{content:"\eb3f"}.octo-icon-thermometer:before{content:"\eb40"}.octo-icon-thermometer-3:before{content:"\eb41"}.octo-icon-thermometer-full:before{content:"\eb42"}.octo-icon-thermometer-three-quarters:before{content:"\eb43"}.octo-icon-thermometer-half:before{content:"\eb44"}.octo-icon-thermometer-2:before{content:"\eb45"}.octo-icon-thermometer-1:before{content:"\eb46"}.octo-icon-thermometer-quarter:before{content:"\eb47"}.octo-icon-thermometer-0:before{content:"\eb48"}.octo-icon-thermometer-empty:before{content:"\eb49"}.octo-icon-shower:before{content:"\eb4a"}.octo-icon-window-maximize:before{content:"\eb4b"}.octo-icon-window-minimize:before{content:"\eb4c"}.octo-icon-window-restore:before{content:"\eb4d"}.octo-icon-window-close:before{content:"\eb4e"}.octo-icon-etsy:before{content:"\eb4f"}.octo-icon-imdb:before{content:"\eb50"}.octo-icon-microchip:before{content:"\eb51"}.octo-icon-snowflake-o:before{content:"\eb52"}.octo-icon-meetup:before{content:"\eb53"}.octo-icon-add-bold:before{content:"\e962"}.octo-icon-layers-grid-add:before{content:"\e963"}.octo-icon-common-file-star:before{content:"\e961"}.octo-icon-set-parent:before{content:"\e960"}.octo-icon-common-file-remove:before{content:"\e95c"}.octo-icon-common-file-sync:before{content:"\e95d"}.octo-icon-harddrive-upload:before{content:"\e95e"}.octo-icon-common-file-upload:before{content:"\e95f"}.octo-icon-list-reorder:before{content:"\e95b"}.octo-icon-keyboard-return:before{content:"\e95a"}.octo-icon-calendar-add:before{content:"\e951"}.octo-icon-calendar-3:before{content:"\e952"}.octo-icon-calendar-disable:before{content:"\e953"}.octo-icon-calendar-check:before{content:"\e954"}.octo-icon-calendar-clock:before{content:"\e955"}.octo-icon-notes-edit-active .path1:before{content:"\e956";color:#ff9c09}.octo-icon-notes-edit-active .path2:before{content:"\e957";margin-left:-1em;color:#536061}.octo-icon-notes-edit:before{content:"\e958"}.octo-icon-calendar-check-1:before{content:"\e959"}.octo-icon-user-actions-key:before{content:"\e950"}.octo-icon-id-card-1:before{content:"\e94e"}.octo-icon-user-group:before{content:"\e94f"}.octo-icon-translate:before{content:"\e94c"}.octo-icon-globe:before{content:"\e94d"}.octo-icon-code-snippet:before{content:"\e94b"}.octo-icon-log-settings:before{content:"\e941"}.octo-icon-lock:before{content:"\e949"}.octo-icon-users:before{content:"\e94a"}.octo-icon-power:before{content:"\e942"}.octo-icon-paint-brush-1:before{content:"\e943"}.octo-icon-mail-templates:before{content:"\e947"}.octo-icon-mail-messages:before{content:"\e945"}.octo-icon-mail-settings:before{content:"\e946"}.octo-icon-mail-branding:before{content:"\e944"}.octo-icon-download:before{content:"\e948"}.octo-icon-plus:before{content:"\e940"}.octo-icon-cross:before{content:"\e93e"}.octo-icon-callout-danger:before{content:"\e93d"}.octo-icon-callout-success:before{content:"\e93c"}.octo-icon-callout-info:before{content:"\e93f"}.octo-icon-add-above:before{content:"\e93a"}.octo-icon-add-below:before{content:"\e93b"}.octo-icon-check-multi:before{content:"\e939"}.octo-icon-unlink:before{content:"\e938"}.octo-icon-list-add:before{content:"\e935"}.octo-icon-list-remove:before{content:"\e936"}.octo-icon-create:before{content:"\e937"}.octo-icon-preview:before{content:"\e933"}.octo-icon-window-split:before{content:"\e934"}.octo-icon-eraser:before{content:"\e92e"}.octo-icon-text-code-block:before{content:"\e932"}.octo-icon-text-h1:before{content:"\e92f"}.octo-icon-text-h3:before{content:"\e930"}.octo-icon-text-h2:before{content:"\e931"}.octo-icon-text-insert-table:before{content:"\e915"}.octo-icon-text-colors:before{content:"\e91e"}.octo-icon-text-emoticons:before{content:"\e91f"}.octo-icon-text-inline-style:before{content:"\e924"}.octo-icon-volume:before{content:"\e925"}.octo-icon-text-video:before{content:"\e926"}.octo-icon-edit-code:before{content:"\e92b"}.octo-icon-text-subscript:before{content:"\e920"}.octo-icon-text-superscript:before{content:"\e921"}.octo-icon-link:before{content:"\e91b"}.octo-icon-text-strikethrough:before{content:"\e91d"}.octo-icon-text-increase-indent:before{content:"\e922"}.octo-icon-text-decrease-indent:before{content:"\e923"}.octo-icon-text-image:before{content:"\e927"}.octo-icon-redo:before{content:"\e928"}.octo-icon-undo:before{content:"\e929"}.octo-icon-attachment:before{content:"\e92a"}.octo-icon-cursor-arrow:before{content:"\e92c"}.octo-icon-text-clear-formatting:before{content:"\e92d"}.octo-icon-quote:before{content:"\e919"}.octo-icon-text-format-ul:before{content:"\e90c"}.octo-icon-text-format-ol:before{content:"\e90d"}.octo-icon-text-justify:before{content:"\e916"}.octo-icon-magic-wand:before{content:"\e918"}.octo-icon-horizontal-line:before{content:"\e91a"}.octo-icon-bold:before{content:"\e90f"}.octo-icon-text-left:before{content:"\e910"}.octo-icon-text-right:before{content:"\e911"}.octo-icon-text-center:before{content:"\e912"}.octo-icon-italic:before{content:"\e913"}.octo-icon-underline:before{content:"\e914"}.octo-icon-info:before{content:"\e917"}.octo-icon-components:before{content:"\e91c"}.octo-icon-fullscreen-collapse:before{content:"\e90e"}.octo-icon-angle-down:before{content:"\e90a"}.octo-icon-angle-up:before{content:"\e90b"}.octo-icon-search:before{content:"\e909"}.octo-icon-settings:before{content:"\e904"}.octo-icon-delete:before{content:"\e905"}.octo-icon-fullscreen:before{content:"\e906"}.octo-icon-save:before{content:"\e907"}.octo-icon-search-code:before{content:"\e908"}.octo-icon-exit:before{content:"\e901"}.octo-icon-app-window:before{content:"\e902"}.octo-icon-user-account:before{content:"\e903"}.octo-icon-triangle-down:before{content:"\e900"}.octo-icon-location-target:before{content:"\1f32b"}.list-hidden-drag-ghost{opacity:0 !important}table.table.data tr:hover .list-reorder span.list-reorder-handle{display:block}table.table.data tbody.tree-drag-mode,table.table.data tbody.tree-drag-mode *{cursor:move!important}table.table.data tbody.tree-drag-mode td.list-reorder span.list-reorder-handle{display:none}table.table.data tbody.tree-drag-mode tr.sortable-chosen td.list-reorder span.list-reorder-handle{display:block}table.table.data tbody.tree-drag-updated td.list-reorder span.list-reorder-handle{display:none !important}table.table.data th.list-reorder,table.table.data td.list-reorder{width:24px;min-width:24px;padding-left:0;padding-right:0}table.table.data td.list-reorder{vertical-align:middle;position:relative}table.table.data td.list-reorder span.list-reorder-handle{display:none;background:var(--oc-toolbar-bg);text-align:center;cursor:move;left:-6px;width:24px;height:24px;position:absolute;top:calc(50% - 12px);border-radius:3px;text-decoration:none}table.table.data td.list-reorder span.list-reorder-handle i{font-size:24px;top:2px}table.table.data td.list-reorder span.list-reorder-handle:active{background:var(--oc-toolbar-hover-bg)}table.table.data td.list-cell-tree{vertical-align:middle;position:relative;-webkit-transition:padding .3s ease;transition:padding .3s ease}table.table.data td.list-cell-tree a.tree-expand-collapse{width:24px;height:24px;display:block;position:absolute;top:calc(50% - 12px);border-radius:3px;text-decoration:none}table.table.data td.list-cell-tree a.tree-expand-collapse{margin-left:-25px}table.table.data td.list-cell-tree a.tree-expand-collapse>span{position:absolute;display:inline-block;left:0;top:0;width:24px;height:24px}table.table.data td.list-cell-tree a.tree-expand-collapse>span:after{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f105";position:absolute;left:calc(100% - 15px);top:5px;width:24px;height:24px;font-size:15px;color:var(--bs-body-color);text-indent:0}table.table.data td.list-cell-tree a.tree-expand-collapse.is-expanded span:after{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f107";left:calc(100% - 17px)}.control-list.list-checkboxes table.table.data th.list-reorder,.control-list.list-checkboxes table.table.data td.list-reorder{width:18px}.control-list.list-checkboxes table.table.data td.list-reorder span.list-reorder-handle{left:-13px}tr.rowlink:not(.nolink) td{cursor:pointer}tr.rowlink:not(.nolink) td.nolink{cursor:auto}a.rowlink{color:inherit;font:inherit;text-decoration:inherit}.control-list table.table.data thead td,.control-list table.table.data thead th{border:none}.control-list .table>:not(:first-child){border-top:none}.control-list .table>:not(caption)>*>*{border-bottom-width:0}table.table.data{font-size:14px;border-bottom:1px solid var(--bs-border-color)}table.table.data.no-offset-bottom{margin-bottom:0 !important}table.table.data thead{background:var(--bs-tertiary-bg)}table.table.data thead td,table.table.data thead th{border-width:1px;border-top:1px solid var(--bs-border-color) !important;border-bottom:1px solid var(--bs-border-color) !important;border-color:var(--bs-border-color);padding:0;font-weight:600;font-size:14px;white-space:nowrap;background-color:inherit}table.table.data thead td>a,table.table.data thead th>a,table.table.data thead td>span,table.table.data thead th>span{display:block;padding:10px 15px;color:var(--bs-body-color);text-decoration:none}table.table.data thead td>a:hover,table.table.data thead th>a:hover,table.table.data thead td>span:hover,table.table.data thead th>span:hover{color:var(--bs-emphasis-color)}table.table.data thead td.explicit-left-padding>a,table.table.data thead th.explicit-left-padding>a,table.table.data thead td.explicit-left-padding>span,table.table.data thead th.explicit-left-padding>span{padding-left:0}table.table.data thead td.sort-desc>span:after,table.table.data thead th.sort-desc>span:after,table.table.data thead td.sort-desc>a:after,table.table.data thead th.sort-desc>a:after{font-size:14px;line-height:14px;display:inline-block;margin-left:6px;vertical-align:baseline;opacity:.4;font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f107"}table.table.data thead td.sort-desc>span:hover:after,table.table.data thead th.sort-desc>span:hover:after,table.table.data thead td.sort-desc>a:hover:after,table.table.data thead th.sort-desc>a:hover:after{opacity:.8}table.table.data thead td.sort-asc>span:after,table.table.data thead th.sort-asc>span:after,table.table.data thead td.sort-asc>a:after,table.table.data thead th.sort-asc>a:after{font-size:14px;line-height:14px;display:inline-block;margin-left:6px;vertical-align:baseline;opacity:.4;font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f106"}table.table.data thead td.sort-asc>span:hover:after,table.table.data thead th.sort-asc>span:hover:after,table.table.data thead td.sort-asc>a:hover:after,table.table.data thead th.sort-asc>a:hover:after{opacity:.8}table.table.data thead td.has-tooltip>a,table.table.data thead th.has-tooltip>a,table.table.data thead td.has-tooltip>span,table.table.data thead th.has-tooltip>span{padding-left:20px;padding-right:10px}table.table.data thead td.has-tooltip .list-tooltip,table.table.data thead th.has-tooltip .list-tooltip{float:left;position:relative;left:-5px}table.table.data thead td.active,table.table.data thead th.active{text-decoration:underline;color:var(--bs-emphasis-color)}table.table.data thead td.active:hover,table.table.data thead th.active:hover{text-decoration:none}table.table.data thead td.active>span:after,table.table.data thead th.active>span:after,table.table.data thead td.active>a:after,table.table.data thead th.active>a:after{text-decoration:none;color:#c63e26;opacity:1 !important}table.table.data thead tr th:last-child a{padding-right:25px}table.table.data thead .list-checkbox .custom-checkbox{top:-22px}table.table.data tbody tr{background-color:var(--bs-table-bg)}table.table.data tbody tr:nth-child(even) td,table.table.data tbody tr:nth-child(even) th{background-color:var(--bs-table-striped-bg)}table.table.data tbody td,table.table.data tbody th{padding:10px 15px;color:var(--bs-table-color);border-top:1px solid var(--bs-table-border-color)}table.table.data tbody td strong,table.table.data tbody th strong{font-weight:600}table.table.data tbody td div.progress,table.table.data tbody th div.progress{position:relative;overflow:visible;height:auto;margin-bottom:0;background-color:transparent;border-radius:0;box-shadow:none}table.table.data tbody td div.progress div.bar,table.table.data tbody th div.progress div.bar{position:absolute;left:-15px;top:-11px;bottom:-11px;background:#0181b9;opacity:.3}table.table.data tbody td div.progress a,table.table.data tbody th div.progress a{position:relative}table.table.data tbody tr:first-child th,table.table.data tbody tr:first-child td{border-top:none}table.table.data tbody tr.active:not(.no-data) td,table.table.data tbody tr.selected:not(.no-data) td{color:var(--bs-table-hover-color);background-color:var(--bs-table-hover-bg)}table.table.data tbody:not(.tree-drag-mode) tr.rowlink:not(.nolink):not(.active):hover td,table.table.data tbody:not(.tree-drag-mode) tr:not(.no-data):not(.active).selected td{color:var(--bs-table-hover-color);background-color:var(--bs-table-hover-bg)}table.table.data tbody:not(.tree-drag-mode) tr.rowlink:not(.nolink).active:hover td,table.table.data tbody:not(.tree-drag-mode) tr:not(.no-data).active.selected td{background:var(--oc-table-active-bg)}table.table.data tbody tr.sortable-chosen{position:relative;z-index:1}table.table.data tbody tr.sortable-chosen td{color:var(--bs-table-hover-color);background-color:var(--bs-table-hover-bg)}table.table.data tbody tr:focus{outline:none}table.table.data tbody tr.hidden td,table.table.data tbody tr.hidden th,table.table.data tbody tr.hidden td a,table.table.data tbody tr.hidden th a{display:none}table.table.data tbody tr.strike td,table.table.data tbody tr.strike th,table.table.data tbody tr.strike td a,table.table.data tbody tr.strike th a{text-decoration:line-through}table.table.data tbody tr.frozen td,table.table.data tbody tr.frozen th,table.table.data tbody tr.frozen td a,table.table.data tbody tr.frozen th a{color:#337ab7}table.table.data tbody tr.processing td,table.table.data tbody tr.processing th,table.table.data tbody tr.processing td a,table.table.data tbody tr.processing th a{color:#666666}table.table.data tbody tr.negative td,table.table.data tbody tr.negative th,table.table.data tbody tr.negative td a,table.table.data tbody tr.negative th a{color:#b2341c}table.table.data tbody tr.positive td,table.table.data tbody tr.positive th,table.table.data tbody tr.positive td a,table.table.data tbody tr.positive th a{color:#278731}table.table.data tbody tr.disabled td,table.table.data tbody tr.deleted td,table.table.data tbody tr.disabled th,table.table.data tbody tr.deleted th,table.table.data tbody tr.disabled td a,table.table.data tbody tr.deleted td a,table.table.data tbody tr.disabled th a,table.table.data tbody tr.deleted th a{color:#888888}table.table.data tbody tr.new td,table.table.data tbody tr.important td,table.table.data tbody tr.new th,table.table.data tbody tr.important th,table.table.data tbody tr.new td a,table.table.data tbody tr.important td a,table.table.data tbody tr.new th a,table.table.data tbody tr.important th a{font-weight:600}table.table.data tbody tr.safe td,table.table.data tbody tr.special td,table.table.data tbody tr.safe th,table.table.data tbody tr.special th,table.table.data tbody tr.safe td a,table.table.data tbody tr.special td a,table.table.data tbody tr.safe th a,table.table.data tbody tr.special th a{color:#98a7a8}table.table.data tbody td.column-break-word{word-wrap:break-word;word-break:break-all}table.table.data tbody td.column-single-line{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}table.table.data tbody td.column-slim{padding-left:0;padding-right:0}table.table.data tbody td.column-compact{padding:0}table.table.data tbody td.column-button{padding:5px}table.table.data tbody.icons td i[class^="icon-"]{display:inline-block;margin-right:7px;font-size:15px;color:#95a5a6;position:relative;top:1px}table.table.data tbody.clickable{cursor:pointer;user-select:none}table.table.data.no-active-indicator tbody tr td:first-child{border-left:none}table.table.data tfoot a{color:var(--bs-table-color);text-decoration:none}table.table.data tfoot td,table.table.data tfoot th{border-color:var(--bs-border-color);padding:10px 15px}table.table.data th.list-cell-type-switch,table.table.data td.list-cell-type-switch{text-align:center}table.table.data th.list-cell-type-number,table.table.data td.list-cell-type-number{text-align:right}table.table.data th.list-cell-align-left,table.table.data td.list-cell-align-left{text-align:left}table.table.data th.list-cell-align-right,table.table.data td.list-cell-align-right{text-align:right}table.table.data th.list-cell-align-center,table.table.data td.list-cell-align-center{text-align:center}table.table.data th.list-cell-type-selectable,table.table.data td.list-cell-type-selectable{white-space:nowrap}table.table.data th.list-cell-type-colorpicker,table.table.data td.list-cell-type-colorpicker{width:20px}table.table.data .list-badge{display:inline-block;position:relative;top:0;margin:0 5px 0 0;padding:1px 0 0 0;font-size:10px;width:15px;height:15px;text-align:center;border-radius:4px;color:#fff}table.table.data .list-badge>i{position:relative;top:-1px}table.table.data .list-badge.badge-default{background:#98A0A0}table.table.data .list-badge.badge-primary{background:var(--bs-primary)}table.table.data .list-badge.badge-success{background:var(--bs-success)}table.table.data .list-badge.badge-info{background:var(--bs-info)}table.table.data .list-badge.badge-warning{background:var(--bs-warning)}table.table.data .list-badge.badge-danger{background:var(--bs-danger)}table.table.data .list-switch{border-radius:100px;display:inline-block;width:20px;height:20px;text-align:center}table.table.data .list-switch.is-true{background-color:var(--bs-success);color:#fff}table.table.data .list-switch.is-false{background-color:var(--bs-secondary-bg);color:var(--bs-body-color)}table.table.data ul.list-link-list{list-style:none;padding:0;margin:0}table.table.data ul.list-link-list>li{display:inline-block}table.table.data ul.list-link-list>li:after{content:', '}table.table.data ul.list-link-list>li:last-child{padding-right:0}table.table.data ul.list-link-list>li:last-child:after{content:''}table.table.data ul.list-link-list>li a{text-decoration:underline}table.table.data .list-image-thumbs{margin-top:-7px;margin-bottom:-7px}table.table.data .list-image-thumb{display:inline-block;position:relative;margin-top:1px;margin-bottom:1px}table.table.data .list-image-thumb:after{content:'';display:block;position:absolute;box-shadow:inset 0 0 0 1px rgba(127,140,141,0.25);top:0;bottom:0;right:0;left:0;z-index:2;border-radius:4px}table.table.data .list-image-thumb>img{border-radius:4px}table.table.data .list-image-thumb.is-default-size{width:34px;height:34px}table.table.data .list-image-thumb.is-default-size>img{height:100%;width:100%;object-fit:cover}table.table.data .list-file-items{margin-top:-7px;margin-bottom:-7px}table.table.data .list-file-item{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;margin:1px;background:var(--oc-toolbar-bg);border:1px solid var(--oc-toolbar-border);border-radius:4px;color:var(--bs-secondary-color);text-decoration:none}table.table.data .list-file-item>i{font-size:18px}table.table.data .list-selectable{background:var(--oc-toolbar-bg);border:1px solid var(--oc-toolbar-border);border-radius:6px;display:inline-block;padding:0 8px;height:28px;line-height:26px;margin-top:-10px;margin-bottom:-10px;white-space:nowrap}table.table.data .list-selectable i{display:inline-block;margin-right:3px;margin-left:3px;text-align:center}table.table.data .list-colorpicker{width:0;height:0;background-color:transparent;border-right-color:var(--background-color);border-top-color:var(--background-color);border-bottom-color:var(--background-color);border-left-color:var(--background-color);border-width:10px;border-style:solid;border-radius:20px;display:inline-block;position:relative;margin-top:-5px;margin-bottom:-5px}table.table.data .list-colorpicker:after{content:"";top:-10px;left:-10px;width:20px;height:20px;position:absolute;box-shadow:inset 0 0 0 1px rgba(53,66,91,0.15);border-radius:20px}table.table.data .list-colorpicker.is-twotone{border-top-color:var(--foreground-color);border-left-color:var(--foreground-color)}table.table.data .list-checkbox{width:42px;vertical-align:top;border-right:1px solid var(--bs-table-border-color)}table.table.data tbody tr td.list-checkbox{padding-left:15px}table.table.data thead tr th.list-checkbox{padding:10.5px 0 0 15px}.list-preview{padding:0;margin-bottom:20px;background:white;border:1px solid var(--bs-border-color)}.list-preview .list-header:first-child{padding-top:20px}.list-preview .control-list:last-child{margin-bottom:0}.list-preview .control-list:last-child>table{border-bottom:none}.list-flush table.table.data thead tr th{border-top:none !important}.list-with-sidebar{display:flex}.list-with-sidebar .sidebar-area{flex-shrink:0;flex-basis:180px;width:180px}.list-with-sidebar .sidebar-list{flex-grow:1;width:0}@media (max-width:991px){.list-with-sidebar{display:block}.list-with-sidebar .sidebar-area{width:auto;flex-shrink:inherit;flex-basis:inherit;margin:0 20px 20px}.list-with-sidebar .sidebar-list{flex-grow:inherit;width:auto}}.control-list{margin-bottom:20px}.control-list p.no-data{padding:18px 20px;margin:0 20px;color:var(--bs-secondary-color);font-size:14px;text-align:center;border-radius:4px}.control-list table.table.data{margin-bottom:0}.control-list table.table.data .list-setup{width:48px;vertical-align:middle}.control-list table.table.data .list-setup a{padding:8px 15px}.control-list table.table.data .list-setup a>span{display:block;color:var(--oc-toolbar-color);background:var(--oc-toolbar-bg);width:24px;height:24px;text-align:center;border-radius:3px}.control-list table.table.data .list-setup a>span:before{font-size:14px;line-height:14px;font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f0ca";display:inline-block;vertical-align:middle}.control-list table.table.data .list-setup a:hover>span{color:var(--oc-toolbar-hover-color);background:var(--oc-toolbar-hover-bg)}.control-list table.table.data .list-setup.setup-show-structure a>span:before{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f0f0"}.control-list.list-rowlink tbody td a:not(.btn,.dropdown-item),.control-list.list-rowlink tbody th a:not(.btn,.dropdown-item){color:var(--bs-table-color)}.control-list.list-rowlink tbody td a:not(.btn,.dropdown-item):hover,.control-list.list-rowlink tbody th a:not(.btn,.dropdown-item):hover{text-decoration:none}.list-widget-container{border-radius:6px;border:1px solid var(--bs-border-color);overflow:hidden}.list-widget-container>.control-filter{background-color:var(--oc-form-control-bg);border-top:none}.list-widget-container>.control-filter>.filter-group>.filter-scope{padding:10px}.list-widget-container>.list-widget table.table.data thead th{border-top:none !important}.list-widget-container>.list-widget .control-list table.table.data{border-bottom:none}.list-widget-container{margin:0 20px 20px}.list-widget-container>.list-widget>.control-list{margin-bottom:0}@media (max-width:767px){.list-widget-container{margin:0 0 20px;border-radius:0;border:none}}.list-header{background-color:transparent;padding:0 20px 1px 20px}.list-header h3{font-size:14px;color:#7e8c8d;text-transform:uppercase;font-weight:600;margin-top:0;margin-bottom:15px}.list-footer{padding:5px 10px;border-top:1px solid var(--bs-border-color)}.list-footer a{color:var(--bs-table-color);text-decoration:none}.list-footer .list-pagination{display:flex}.list-footer .list-pagination .list-pagination-summary{padding:.475rem 5px;font-size:.9rem}.list-footer .list-pagination .list-pagination-links ul.pagination{margin-bottom:0}.list-footer .list-pagination .list-pagination-links ul.pagination .page-link{border-radius:4px;border-color:transparent;box-shadow:none}.list-footer .list-pagination .list-pagination-links ul.pagination li.disabled .page-link{color:#98a7a8;background-color:transparent}@media (max-width:992px){.list-footer .list-pagination{flex-direction:column-reverse}.list-footer .list-pagination .list-pagination-links{margin-bottom:.5rem}}.report-widget .table-container{margin:-15px}.report-widget .table-container table.table.data{margin-bottom:0}.report-widget .table-container table.table.data thead tr th{border-top:none !important}.report-widget .table-container table.table.data tbody tr:nth-child(even) td,.report-widget .table-container table.table.data tbody tr:nth-child(even) th{background-color:transparent}.list-scrollable-container{touch-action:auto;position:relative}.list-scrollable-container>.list-scrollable{position:relative}.list-scrollable-container>.list-scrollable:after,.list-scrollable-container>.list-scrollable:before{content:'';position:absolute;top:0;bottom:0;width:10px;z-index:2;opacity:0;transition:opacity .1s ease-out}.list-scrollable-container>.list-scrollable:before{background:transparent linear-gradient(90deg, #000 0%, rgba(0,0,0,0) 100%);left:10px}.list-scrollable-container>.list-scrollable:after{background:transparent linear-gradient(270deg, #000 0%, rgba(0,0,0,0) 100%);right:-10px}.list-scrollable-container>.list-scrollable.scroll-before:before{opacity:.15}.list-scrollable-container>.list-scrollable.scroll-after:after{opacity:.15}.list-scrollable-container>.list-scrollable:after,.list-scrollable-container>.list-scrollable:before{bottom:auto;height:40.5px}.list-scrollable-container>.list-scrollable:before{left:0}.list-scrollable-container>.list-scrollable:after{right:0}.list-scrollable-container>.list-scrollable.scroll-after th,.list-scrollable-container>.list-scrollable.scroll-before th{cursor:move}.list-scrollable-container>.list-scrollable.scroll-after th a,.list-scrollable-container>.list-scrollable.scroll-before th a{cursor:pointer}.list-scrollable-container>.list-scrollable>.list-content{overflow-x:auto}.control-filter .form-check .form-check-input{position:relative;top:1px}.control-filter .form-check label{font-size:14px;color:var(--bs-secondary-color)}.control-filter>.filter-group>.filter-scope.scope-checkbox.form-check,.control-filter>.filter-group>.filter-scope.form-check{padding-left:29px}.control-filter>.filter-group>.filter-scope.scope-checkbox.form-check,.control-filter>.filter-group>.filter-scope.form-check,.control-filter>.filter-group>.filter-scope.scope-checkbox.form-check label,.control-filter>.filter-group>.filter-scope.form-check label{margin-bottom:0}.control-filter>.filter-group>.filter-scope.scope-checkbox.form-check:after,.control-filter>.filter-group>.filter-scope.form-check:after{content:''}.control-filter>.filter-group>.filter-scope.scope-checkbox:hover.form-check label,.control-filter>.filter-group>.filter-scope:hover.form-check label,.control-filter>.filter-group>.filter-scope.scope-checkbox.active.form-check label,.control-filter>.filter-group>.filter-scope.active.form-check label{color:var(--bs-emphasis-color)}.control-filter>.filter-group>.filter-scope.scope-dropdown,.control-filter>.filter-group>.filter-scope.dropdown{padding:8px 0 4px}.control-filter>.filter-group>.filter-scope.scope-dropdown:after,.control-filter>.filter-group>.filter-scope.dropdown:after{content:''}.control-filter>.filter-group>.filter-scope.scope-dropdown .select2-container,.control-filter>.filter-group>.filter-scope.dropdown .select2-container{width:100%;display:inline-block;position:relative;top:-2px}.control-filter>.filter-group>.filter-scope.scope-dropdown .select2-container .select2-selection,.control-filter>.filter-group>.filter-scope.dropdown .select2-container .select2-selection{height:29px;line-height:29px;padding:0 3px 0 12px;border:none;font-size:12px;border-radius:0;box-shadow:none;background-color:transparent}.control-filter>.filter-group>.filter-scope.scope-dropdown .select2-container .select2-selection.select2-default,.control-filter>.filter-group>.filter-scope.dropdown .select2-container .select2-selection.select2-default{font-weight:normal}.control-filter>.filter-group>.filter-scope.scope-dropdown .select2-container .select2-selection:focus-visible,.control-filter>.filter-group>.filter-scope.dropdown .select2-container .select2-selection:focus-visible{outline:auto 1px}.control-filter>.filter-group>.filter-scope.scope-dropdown .select2-container .loading-indicator>span,.control-filter>.filter-group>.filter-scope.dropdown .select2-container .loading-indicator>span{top:15px}.control-filter>.filter-group>.filter-scope.scope-dropdown .select2-container.select2-container--open,.control-filter>.filter-group>.filter-scope.dropdown .select2-container.select2-container--open{border-radius:0;border:none}.control-filter>.filter-group>.filter-scope.scope-dropdown .select2-container .select2-selection__arrow,.control-filter>.filter-group>.filter-scope.dropdown .select2-container .select2-selection__arrow{font-size:14px;top:1px;right:10px}.control-filter>.filter-group>.filter-scope.scope-dropdown .select2-container .select2-selection__rendered,.control-filter>.filter-group>.filter-scope.dropdown .select2-container .select2-selection__rendered{padding:0 28px 0 0;font-size:14px;color:var(--bs-secondary-color)}.control-filter>.filter-group>.filter-scope.scope-dropdown:hover .select2-selection__arrow,.control-filter>.filter-group>.filter-scope.dropdown:hover .select2-selection__arrow,.control-filter>.filter-group>.filter-scope.scope-dropdown.active .select2-selection__arrow,.control-filter>.filter-group>.filter-scope.dropdown.active .select2-selection__arrow,.control-filter>.filter-group>.filter-scope.scope-dropdown:hover .select2-selection__rendered,.control-filter>.filter-group>.filter-scope.dropdown:hover .select2-selection__rendered,.control-filter>.filter-group>.filter-scope.scope-dropdown.active .select2-selection__rendered,.control-filter>.filter-group>.filter-scope.dropdown.active .select2-selection__rendered{color:var(--bs-emphasis-color)}.control-filter>.filter-group>.filter-scope.scope-inline{padding:0;display:flex;align-items:center}.control-filter>.filter-group>.filter-scope.scope-inline:after{content:''}.control-filter-popover .filter-search{min-height:36px;position:relative}.control-filter-popover .filter-search input{height:auto;border:none;border-bottom:1px solid var(--bs-border-color);border-bottom-right-radius:0;border-bottom-left-radius:0;box-shadow:none;background:transparent !important;padding-top:6px;padding-bottom:6px;padding-right:35px}.control-filter-popover .filter-search .close{display:none}.control-filter-popover .filter-search .filter-items,.control-filter-popover .filter-search .filter-active-items{color:var(--bs-secondary-color);font-size:13px}.control-filter-popover .filter-search .filter-items ul,.control-filter-popover .filter-search .filter-active-items ul,.control-filter-popover .filter-search .filter-items li,.control-filter-popover .filter-search .filter-active-items li{list-style-type:none;margin:0;padding:0}.control-filter-popover .filter-search .filter-items li,.control-filter-popover .filter-search .filter-active-items li{transition:color .6s,background-color .3s}.control-filter-popover .filter-search .filter-items a,.control-filter-popover .filter-search .filter-active-items a{text-decoration:none;color:var(--bs-secondary-color);display:block;padding:7px 9px}.control-filter-popover .filter-search .filter-items a:before,.control-filter-popover .filter-search .filter-active-items a:before{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;margin-right:4px;position:relative;top:1px;display:inline-block;vertical-align:baseline;text-align:center;border-radius:100px}.control-filter-popover .filter-search .filter-items a:hover,.control-filter-popover .filter-search .filter-active-items a:hover{background-color:var(--oc-selection);color:white}.control-filter-popover .filter-search .filter-items{max-height:200px;overflow:auto}.control-filter-popover .filter-search .filter-items a:before{content:"\f067"}.control-filter-popover .filter-search .filter-items li.loading{padding:7px}.control-filter-popover .filter-search .filter-items li.loading>span{--loader-size:20px;display:block;width:var(--loader-size);height:var(--loader-size);border:2px solid var(--oc-primary-border);border-top-color:var(--oc-accent);border-radius:50%;animation:spin .8s linear infinite}.control-filter-popover .filter-search .filter-items li.animate-enter{animation:fadeInUp .3s}.control-filter-popover .filter-search .filter-buttons{border-top:none}.control-filter-popover .filter-search .filter-active-items{border-top:1px solid var(--bs-border-color)}.control-filter-popover .filter-search .filter-active-items a{font-weight:600;color:var(--bs-body-color)}.control-filter-popover .filter-search .filter-active-items a:before{background-color:var(--bs-secondary-bg);color:var(--bs-body-color);content:"\e93e";left:-1px}.control-filter-popover .filter-search .filter-active-items a:hover:before{color:#c63e26;background-color:white}.control-filter-popover .filter-search .filter-active-items li.animate-enter{animation:fadeInDown .3s}.control-filter-popover .filter-search .filter-active-items li:last-child{border-bottom:1px solid var(--bs-border-color)}.control-filter-popover .filter-box .filter-facet{padding:10px 5px;display:flex}.control-filter-popover .filter-box .filter-facet .facet-item{white-space:nowrap;padding-left:5px;padding-right:5px;line-height:1.8}.control-filter-popover .filter-box .filter-facet .facet-item>span{font-size:12px;color:var(--bs-secondary-color)}.control-filter-popover .filter-box .filter-facet .facet-item.is-grow{flex-grow:1}.control-filter-popover .filter-box .filter-facet+.filter-facet{padding-top:0}.control-filter{padding:0 10px;color:var(--bs-secondary-color);background-color:var(--bs-body-bg);border-top:1px solid var(--bs-border-color);border-bottom:1px solid var(--bs-border-color);font-size:14px}.control-filter a{text-decoration:none;color:var(--bs-secondary-color)}.control-filter>.filter-setup{padding:6px 0}.control-filter>.filter-setup>a>span{margin-left:-2px;margin-right:-2px;display:block;color:var(--oc-toolbar-color);width:24px;height:24px;text-align:center;border-radius:3px}.control-filter>.filter-setup>a>span>i{position:relative;top:2px;font-size:20px}.control-filter>.filter-setup>a:hover>span{color:var(--oc-toolbar-hover-color);background:var(--oc-toolbar-hover-bg)}.control-filter>.filter-group{display:inline-block;vertical-align:middle}.control-filter>.filter-group>.filter-scope{padding:8px;display:block}.control-filter>.filter-group>.filter-scope .filter-label{margin-right:5px}.control-filter>.filter-group>.filter-scope .filter-setting{display:inline-block;margin-right:5px;-webkit-transition:color .6s;transition:color .6s}.control-filter>.filter-group>.filter-scope.loading-indicator-container.in-progress{pointer-events:none;cursor:default}.control-filter>.filter-group>.filter-scope.loading-indicator-container.in-progress .loading-indicator{background:transparent}.control-filter>.filter-group>.filter-scope.loading-indicator-container.in-progress .loading-indicator>span{left:unset;right:0;top:10px;background-color:var(--bs-body-bg);border-radius:50%;margin-top:0;width:20px;height:20px;background-size:15px 15px}.control-filter>.filter-group>.filter-scope:after{font-size:14px;font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f107"}.control-filter>.filter-group>.filter-scope.active .filter-setting{padding-left:5px;padding-right:5px;color:#FFF;background-color:var(--bs-success);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-transition:color 1s, background-color 1s;transition:color 1s, background-color 1s}.control-filter>.filter-group>.filter-scope:hover,.control-filter>.filter-group>.filter-scope.active{color:var(--bs-emphasis-color)}.control-filter>.filter-group>.filter-scope:hover .filter-label,.control-filter>.filter-group>.filter-scope.active .filter-label{color:var(--bs-emphasis-color)}.control-filter>.filter-group>.filter-scope:hover.active .filter-setting,.control-filter>.filter-group>.filter-scope.active.active .filter-setting{background-color:var(--bs-success)}.control-filter>.filter-group>.filter-has-popover{display:inline-block;padding:10px}.control-filter>.filter-group>.filter-has-popover .filter-setting{display:inline-block;-webkit-transition:color .6s;transition:color .6s}.control-filter>.filter-group>.filter-has-popover:after{font-size:14px;font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f107"}.control-filter>.filter-group>.filter-has-popover.active .filter-setting{padding-left:5px;padding-right:5px;color:#FFF;background-color:var(--bs-success);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;transition:color 1s,background-color 1s}.control-filter>.filter-group>.filter-has-popover:hover{color:#000}.control-filter>.filter-group>.filter-has-popover:hover .filter-label{color:var(--bs-secondary-color)}.control-filter>.filter-group>.filter-has-popover:hover.active .filter-setting{background-color:#79c035}.control-filter.is-loading{cursor:wait}.control-filter.is-loading>.filter-group{pointer-events:none;opacity:.7}.control-filter.is-loading>.filter-setup>a{opacity:0}.control-filter.is-loading>.filter-setup:after{--loader-size:20px;content:'';position:absolute;left:50%;top:50%;width:var(--loader-size);height:var(--loader-size);margin-top:calc(var(--loader-size) / -2);margin-left:calc(var(--loader-size) / -2);border:2px solid var(--oc-primary-border);border-top-color:var(--oc-accent);border-radius:50%;animation:spin .8s linear infinite}.control-filter-popover{min-width:275px}.control-filter-popover>.loading-indicator-container>.loading-indicator{border-radius:4px}.control-filter-popover>.loading-indicator-container>.loading-indicator>span{left:10px}.control-filter-popover input[type="number"]{-moz-appearance:textfield}.control-filter-popover input[type="number"]::-webkit-inner-spin-button,.control-filter-popover input[type="number"]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.control-filter-popover .filter-buttons{border-top:1px solid var(--bs-border-color);padding:10px;display:flex;border-bottom-left-radius:5px;border-bottom-right-radius:5px}.control-filter-popover .filter-buttons>div{flex-grow:1}.control-filter-popover .filter-buttons>.btn{text-align:center}.form-preview{padding:20px;margin-bottom:20px;background:var(--oc-primary-bg);border:1px solid var(--oc-primary-border)}.form-preview>.form-group:last-child{padding-bottom:0}.form-preview>.form-group:last-child .form-check{margin-bottom:0}.form-preview.form-flush{border-top:none}.form-preview .control-tabs.primary-tabs>ul.nav-tabs>li a>span.title:before,.form-preview .control-tabs.primary-tabs>div>ul.nav-tabs>li a>span.title:before,.form-preview .control-tabs.primary-tabs>div>div>ul.nav-tabs>li a>span.title:before,.form-preview .control-tabs.primary-tabs>ul.nav-tabs>li a>span.title:after,.form-preview .control-tabs.primary-tabs>div>ul.nav-tabs>li a>span.title:after,.form-preview .control-tabs.primary-tabs>div>div>ul.nav-tabs>li a>span.title:after{background:var(--oc-primary-bg)}.form-preview .control-tabs.primary-tabs>ul.nav-tabs>li.active a:before,.form-preview .control-tabs.primary-tabs>div>ul.nav-tabs>li.active a:before,.form-preview .control-tabs.primary-tabs>div>div>ul.nav-tabs>li.active a:before{background-color:var(--oc-primary-bg)}.form-elements:before,.form-tabless-fields:before,.form-elements:after,.form-tabless-fields:after{content:" ";display:table}.form-elements:after,.form-tabless-fields:after{clear:both}.form-elements:before,.form-tabless-fields:before,.form-elements:after,.form-tabless-fields:after{content:" ";display:table}.form-elements:after,.form-tabless-fields:after{clear:both}.form-control{position:relative;-webkit-appearance:none}.form-control:focus{border:1px solid var(--oc-border-focus);box-shadow:none}.form-control.growable,.form-control.is-growable{width:110px}.form-control.growable:focus,.form-control.is-growable:focus,.form-control.growable:active,.form-control.is-growable:active{width:200px !important}@media (max-width:576px){.form-control.growable,.form-control.is-growable{width:40px;text-indent:-999px}.form-control.growable:focus,.form-control.is-growable:focus,.form-control.growable:active,.form-control.is-growable:active{text-indent:0;width:100px !important}.form-control.growable.icon,.form-control.is-growable.icon{padding-right:0 !important}}.form-control.is-searchable{border-radius:100px}.search-input-container{position:relative}.search-input-container:before{position:absolute;width:16px;height:16px;background-position:0 -38px;z-index:1;right:8px;top:11px}.form-group{position:relative;box-sizing:border-box}.form-group:empty{display:none}.form-group,.form-group.layout-item{padding-bottom:20px;margin-bottom:0}.form-group.is-required>label:after{background-color:var(--bs-danger);width:5px;height:5px;margin-left:3px;vertical-align:super;font-size:60%;content:"";display:inline-block;border-radius:8px}.form-group .form-translatable{display:inline-block;position:relative;height:20px;width:25px;border-radius:5px;background-color:var(--oc-input-translatable-bg);color:var(--oc-input-translatable-color);margin-bottom:-5px;margin-left:2px}.form-group .form-translatable i{font-size:16px;position:absolute;top:2px;left:4px;cursor:pointer}.form-group .form-translatable.no-label{position:absolute;top:1px;right:0;left:auto;z-index:2}.form-group.span-full{width:100%;float:left}.form-group.span-left{float:left;width:48.5%;clear:left}.form-group.span-right{float:right;width:48.5%;clear:right}.form-group.clear-full{clear:both}.form-group.clear-left{clear:left}.form-group.clear-right{clear:right}.form-group.layout-relative{padding-bottom:0}.form-group.checkbox-field{padding-bottom:5px}.form-group.checkbox-field>.form-check{margin-bottom:10px}.form-group.radio-field>.form-check{margin-bottom:10px}.form-group.number-field>.form-control{text-align:right}.form-group.radio-align{padding-left:23px;margin-top:-20px}.form-group.checkbox-align{padding-left:23px;margin-top:-5px}.form-group.field-align-above{margin-top:-5px}.form-group.field-slim.span-left,.form-group.field-slim.span-right{width:50%}.form-group.field-indent{padding-left:23px}.form-group.input-sidebar-control{padding-right:35px}.form-group.input-sidebar-control .sidebar-control{position:absolute;right:8px;top:34px;font-size:16px;color:#C4C4C4}.form-group.input-sidebar-control .sidebar-control:hover,.form-group.input-sidebar-control .sidebar-control:focus{text-decoration:none;color:var(--bs-link-color);outline:none}.form-horizontal .col-horizontal{width:155px}.form-horizontal .form-group.checkbox-field{padding-bottom:20px}.form-group-preview .form-control{background-color:var(--oc-form-control-disabled-bg);height:auto;min-height:38px;word-break:break-word;box-shadow:none}.form-group-preview .custom-checkbox label,.form-group-preview .custom-radio label{cursor:default}.col-form-label,.form-label{font-weight:600}.form-text{margin-bottom:0}.form-text.before-field{margin-top:-0.135rem;margin-bottom:.385rem}.control-disabled .form-text{opacity:.5}.input-with-icon{position:relative}.input-with-icon>.icon{position:absolute;z-index:10;padding:10px;pointer-events:none;color:#bdbdbd;font-size:15px;margin-top:1px}.input-with-icon.right-align>.icon{right:0}.input-with-icon.right-align input{padding-right:32px !important}.input-with-icon.left-align>.icon{left:0}.input-with-icon.left-align input{padding-left:32px !important}.input-with-icon.size-sm>.icon{margin-top:0;padding:5px 8px}.input-no-spinner{-moz-appearance:textfield}.input-no-spinner::-webkit-inner-spin-button,.input-no-spinner::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.field-section{border-bottom:1px solid var(--bs-border-color);padding-top:3px;padding-bottom:7px}.field-section>h4{font-weight:400;font-size:1.3rem}.field-section>p:first-child,.field-section>h4:first-child{margin:0}.field-section.is-collapsible{cursor:pointer}.field-section.is-collapsible>h4:before{display:inline-block;vertical-align:baseline;font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f077";font-size:12px;margin:2px 8px 0;float:right;color:rgba(0,0,0,0.4);-webkit-transition:all .3s;transition:all .3s;-webkit-transform:scale(1, 1);transform:scale(1, 1)}.field-section.is-collapsible:hover{border-bottom:1px solid #b0bdcd}.field-section.is-collapsible:hover>h4:before{color:inherit}.field-action{border-bottom:1px solid var(--bs-border-color);padding:10px 0 5px;margin-top:-20px}.field-action h5{margin-bottom:.35rem;margin-top:.75rem;font-weight:400;font-size:1rem}.field-action p{color:var(--bs-secondary-color);margin-bottom:0;font-size:.9rem}.field-action .field-action-button{margin-top:15px;margin-bottom:10px}@media (min-width:992px){.field-action .field-action-button{text-align:right}}.form-group.section-field.collapsed .field-section.is-collapsible>h4:before{-webkit-transform:scale(1, -1);transform:scale(1, -1)}.field-horizontalrule hr{height:1px;background:#000;margin:0;opacity:.1}.field-textarea{resize:vertical}.field-textarea.size-tiny{min-height:50px}.field-textarea.size-small{min-height:100px}.field-textarea.size-large{min-height:200px}.field-textarea.size-huge{min-height:250px}.field-textarea.size-giant{min-height:350px}.field-checkboxlist .field-checkboxlist-inner{border-radius:4px;background:var(--oc-form-control-bg);border:1px solid var(--bs-border-color)}.field-checkboxlist:not(.is-scrollable) .field-checkboxlist-inner{padding:15px 15px 2px 15px}.field-checkboxlist .checkboxlist-controls{padding:3px;font-size:0;white-space:nowrap;background:var(--oc-form-control-bg);border-top:1px solid var(--bs-border-color);border-left:1px solid var(--bs-border-color);border-right:1px solid var(--bs-border-color);border-top-left-radius:4px;border-top-right-radius:4px}.field-checkboxlist .checkboxlist-controls+.field-checkboxlist-inner{border-top-left-radius:0;border-top-right-radius:0;border-top-color:var(--oc-toolbar-border)}.field-checkboxlist .form-check{margin-bottom:10px}.field-checkboxlist .checkboxlist-group{margin:0;padding-left:1.1rem;list-style:none}.field-checkboxlist .checkboxlist-group:not(:has(*)),.field-checkboxlist .checkboxlist-group-item:not(:has(*)){display:none}.field-checkboxlist-scrollable{height:300px}.field-checkboxlist-scrollable .form-check{margin-left:15px;margin-top:15px;margin-bottom:10px}.field-checkboxlist-scrollable .form-check~.form-check{margin-top:0}.form-group.inline-options .field-checkboxlist:not(.is-scrollable) .field-checkboxlist-inner{padding:15px 15px 5px 15px}.form-group.inline-options .form-check{display:inline-block;margin-right:15px}.form-buttons{padding-bottom:20px;font-size:0;width:100%}.form-buttons:before,.form-buttons:after{content:" ";display:table}.form-buttons:after{clear:both}.form-buttons:before,.form-buttons:after{content:" ";display:table}.form-buttons:after{clear:both}.form-buttons.is-horizontal{padding-left:155px}.form-buttons .btn{margin-right:7px}.form-buttons .btn.no-margin-right{margin-right:0}.form-buttons .btn-group{margin-right:7px}.form-buttons .btn-group .btn{margin-right:0}.form-buttons .pull-right{margin-right:0;margin-left:7px}.form-buttons.buttons-offset{padding-left:20px}body.slim-container .form-buttons{padding:0 20px 20px}body .modal-footer .form-buttons{padding:0}@media (max-width:769px){.form-group.span-left,.form-group.span-right{width:100%;clear:none}}body.compact-container .form-fatal-error{padding:1rem 20px 0 20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border:none}.select2-dropdown{z-index:10400}[data-control~=toolbar] .form-control{display:inline-block;margin-right:15px}[data-control~=toolbar] input[type=text].form-control,[data-control~=toolbar] label{position:relative;top:5px}[data-control~=toolbar] label{margin-right:7px}[data-control~=toolbar] label.standalone{margin-right:15px}[data-control~=toolbar] .select2-container{display:inline-block;width:auto;height:36px;margin-right:15px}[data-control~=toolbar] .select2-container .select2-selection__rendered{line-height:17px}[data-control~=toolbar] .select2-container .select2-selection--single{height:36px}[data-control~=toolbar] select.form-control.custom-select{display:none}.control-simplelist{padding:20px 20px 2px 20px;margin-bottom:20px;border:1px solid var(--bs-border-color);background:var(--oc-form-control-bg);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.control-simplelist ul{padding-left:15px}.control-simplelist.form-control ul{margin-bottom:0}.control-simplelist.form-control li{padding-top:5px;padding-bottom:5px}.control-simplelist.with-icons ul,.control-simplelist.with-checkboxes ul,.control-simplelist.is-divided ul,.control-simplelist.is-selectable ul{list-style-type:none;padding-left:0}.control-simplelist.with-icons li>i{margin-right:5px}.control-simplelist.with-checkboxes li div.custom-checkbox,.control-simplelist.with-checkboxes li div.form-check{display:inline-block}.control-simplelist.with-checkboxes li:first-child{margin-top:0}.control-simplelist.with-checkboxes li:last-child div.custom-checkbox,.control-simplelist.with-checkboxes li:last-child div.form-check{margin-bottom:0}.control-simplelist.with-checkboxes li:last-child div.custom-checkbox label,.control-simplelist.with-checkboxes li:last-child div.form-check label{margin-bottom:5px}.control-simplelist.is-sortable li{position:relative;padding:2px 0 2px 24px}.control-simplelist.is-sortable li .drag-handle{width:24px;height:24px;cursor:move;text-align:center;color:var(--oc-toolbar-color);background:var(--oc-toolbar-bg);border-radius:3px;text-decoration:none;position:absolute;top:1px;left:-6px}.control-simplelist.is-sortable li .drag-handle>i{font-size:24px}.control-simplelist.is-sortable li.sortable-chosen.sortable-ghost{position:relative}.control-simplelist.is-sortable li.sortable-chosen.sortable-ghost .drag-handle{background:var(--oc-toolbar-hover-bg)}.control-simplelist.is-scrollable{height:200px}.control-simplelist.is-scrollable.size-tiny{min-height:250px}.control-simplelist.is-scrollable.size-small{min-height:300px}.control-simplelist.is-scrollable.size-large{min-height:400px}.control-simplelist.is-scrollable.size-huge{min-height:450px}.control-simplelist.is-scrollable.size-giant{min-height:550px}.control-simplelist.is-divided,.control-simplelist.is-selectable,.control-simplelist.is-selectable-box{padding:0}.control-simplelist.is-divided li .heading,.control-simplelist.is-selectable li .heading,.control-simplelist.is-selectable-box li .heading{font-size:14px;font-weight:600;margin-top:10px}.control-simplelist.is-divided li,.control-simplelist.is-selectable li{padding:5px 10px;border-bottom:1px solid var(--bs-border-color)}.control-simplelist.is-divided li:last-child,.control-simplelist.is-selectable li:last-child{border-bottom:none}.control-simplelist.is-selectable li a{padding:5px 10px;margin:-5px -10px;display:block;color:var(--bs-body-color)}.control-simplelist.is-selectable li:hover{background:var(--bs-primary);cursor:pointer}.control-simplelist.is-selectable li:hover,.control-simplelist.is-selectable li:hover a,.control-simplelist.is-selectable li:hover .heading,.control-simplelist.is-selectable li:hover .description{color:white}.control-simplelist.is-selectable li:hover a{text-decoration:none}.control-simplelist.is-selectable li.active a{background:#f0f0f0}.control-simplelist.is-selectable li.active a:hover{background:var(--bs-primary)}.control-simplelist.is-selectable-box{padding-top:15px;margin-bottom:0;background:transparent}.control-simplelist.is-selectable-box.is-flush{padding:0;margin-left:-8px}.control-simplelist.is-selectable-box.is-flush ul{padding-left:0}.control-simplelist.is-selectable-box li{width:155px;margin:8px;display:inline-block;text-align:center;vertical-align:top}.control-simplelist.is-selectable-box li a{text-decoration:none;display:block;color:var(--bs-body-color)}.control-simplelist.is-selectable-box li a .box{display:block;width:155px;height:155px;border:3px solid rgba(0,0,0,0.1);position:relative;background:var(--oc-form-control-bg);-webkit-transition:border .3s ease;transition:border .3s ease}.control-simplelist.is-selectable-box li a .image{display:block;width:56px;height:56px;position:absolute;top:50%;left:50%;margin-top:-28px;margin-left:-28px}.control-simplelist.is-selectable-box li a .image>i{font-size:56px;color:rgba(0,0,0,0.25)}.control-simplelist.is-selectable-box li a .heading{margin:7px 0;padding:0}.control-simplelist.is-selectable-box li a .description{font-size:12px}.control-simplelist.is-selectable-box li a:hover .box{border-color:rgba(0,0,0,0.2)}.control-simplelist.is-selectable-box li a:hover .image>i{color:rgba(0,0,0,0.45)}.control-simplelist.is-selectable-box li.active a .box{border-color:var(--oc-selection)}.control-simplelist.is-selectable-box li.active a .image>i{color:rgba(0,0,0,0.45)}.list-preview .control-simplelist.is-selectable ul{margin-bottom:0}.drag-noselect{user-select:none}.control-scrollbar{position:relative;overflow:hidden;height:100%}.control-scrollbar>.scrollbar-scrollbar{position:absolute;z-index:100}.control-scrollbar>.scrollbar-scrollbar .scrollbar-track{background-color:transparent;position:relative;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.control-scrollbar>.scrollbar-scrollbar .scrollbar-track .scrollbar-thumb{background-color:rgba(0,0,0,0.35);-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;cursor:pointer;overflow:hidden;position:absolute}.control-scrollbar>.scrollbar-scrollbar.disabled{display:none !important}.control-scrollbar.vertical>.scrollbar-scrollbar{right:0;margin-right:5px;width:6px}.control-scrollbar.vertical>.scrollbar-scrollbar .scrollbar-track{height:100%;width:6px}.control-scrollbar.vertical>.scrollbar-scrollbar .scrollbar-track .scrollbar-thumb{height:20px;width:6px;top:0;left:0}.control-scrollbar.vertical>.scrollbar-scrollbar:active,.control-scrollbar.vertical>.scrollbar-scrollbar:hover{width:8px;-webkit-transition:width .3s;transition:width .3s}.control-scrollbar.vertical>.scrollbar-scrollbar:active .scrollbar-track,.control-scrollbar.vertical>.scrollbar-scrollbar:hover .scrollbar-track,.control-scrollbar.vertical>.scrollbar-scrollbar:active .scrollbar-thumb,.control-scrollbar.vertical>.scrollbar-scrollbar:hover .scrollbar-thumb{width:8px;-webkit-transition:width .3s;transition:width .3s}.control-scrollbar.horizontal>.scrollbar-scrollbar{margin:0 0 5px;clear:both;height:6px}.control-scrollbar.horizontal>.scrollbar-scrollbar .scrollbar-track{width:100%;height:6px}.control-scrollbar.horizontal>.scrollbar-scrollbar .scrollbar-track .scrollbar-thumb{height:6px;margin:2px 0;left:0;top:0}.control-scrollbar.horizontal>.scrollbar-scrollbar:active,.control-scrollbar.horizontal>.scrollbar-scrollbar:hover{height:8px;-webkit-transition:height .3s;transition:height .3s}.control-scrollbar.horizontal>.scrollbar-scrollbar:active .scrollbar-track,.control-scrollbar.horizontal>.scrollbar-scrollbar:hover .scrollbar-track,.control-scrollbar.horizontal>.scrollbar-scrollbar:active .scrollbar-thumb,.control-scrollbar.horizontal>.scrollbar-scrollbar:hover .scrollbar-thumb{height:8px;-webkit-transition:height .3s;transition:height .3s}@media (pointer:coarse){.control-scrollbar{overflow:auto}}.no-touch .control-scrollbar>.scrollbar-scrollbar{opacity:0;-webkit-transition:opacity .3s;transition:opacity .3s}.no-touch .control-scrollbar:active>.scrollbar-scrollbar,.no-touch .control-scrollbar:hover>.scrollbar-scrollbar{opacity:1}.scrollable-panel-container{position:relative}.scrollable-panel-container:before{content:'';position:absolute;top:0;left:0;width:100%;height:0;background:transparent url(../images/scrollable-panel-shadow.png) repeat-x left top;transition:height .1s,width .1s}.scrollable-panel-container.scrolled:before{height:9px}.scrollable-panel-container.horizontal:before{top:0;width:0;height:100%}.scrollable-panel-container.horizontal.scrolled:before:before{width:9px;background:transparent url(../images/scrollable-panel-shadow-horizontal.png) repeat-x left top}.scrollable-panel-container .scrollable{position:absolute;left:0;top:0;height:100%;width:100%}.control-filelist p.no-data{padding:22px 0;margin:0;color:#666666;font-size:14px;text-align:center;font-weight:normal;border-radius:4px}.control-filelist ul{padding:0;margin:0}.control-filelist ul li{font-weight:normal;line-height:150%;position:relative;list-style:none}.control-filelist ul li a:hover{background:var(--oc-toolbar-hover-bg)}.control-filelist ul li.active>a{background:var(--oc-selection);position:relative}.control-filelist ul li.active>a span.title,.control-filelist ul li.active>a span.description{color:#ffffff}.control-filelist ul li a{display:block;padding:10px 45px 10px 20px;outline:none}.control-filelist ul li a:hover,.control-filelist ul li a:focus,.control-filelist ul li a:active{text-decoration:none}.control-filelist ul li a span{display:block}.control-filelist ul li a span.title{font-weight:normal;color:var(--oc-toolbar-color);font-size:14px}.control-filelist ul li a span.description{color:var(--oc-primary-color);font-size:12px;white-space:nowrap;font-weight:normal;overflow:hidden;text-overflow:ellipsis}.control-filelist ul li a span.description strong{color:var(--oc-toolbar-color);font-weight:normal}.control-filelist ul li.group>h4,.control-filelist ul li.group>div.group>h4{font-weight:normal;font-size:14px;margin-top:0;margin-bottom:0;position:relative}.control-filelist ul li.group>h4 a,.control-filelist ul li.group>div.group>h4 a{padding:10px 20px 10px 53px;color:var(--oc-toolbar-color);position:relative;outline:none}.control-filelist ul li.group>h4 a:hover,.control-filelist ul li.group>div.group>h4 a:hover{background:transparent}.control-filelist ul li.group>h4 a:before,.control-filelist ul li.group>div.group>h4 a:before,.control-filelist ul li.group>h4 a:after,.control-filelist ul li.group>div.group>h4 a:after{width:10px;height:10px;display:block;position:absolute;top:1px}.control-filelist ul li.group>h4 a:after,.control-filelist ul li.group>div.group>h4 a:after{left:33px;top:9px;font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f114";color:#a1aab1;font-size:16px}.control-filelist ul li.group>h4 a:before,.control-filelist ul li.group>div.group>h4 a:before{left:20px;top:9px;color:#cfcfcf;font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f0da";transform:rotate(90deg) translate(5px, 0);transition:all .1s ease}.control-filelist ul li.group>ul>li>a{padding-left:52px}.control-filelist ul li.group>ul>li.group{padding-left:20px}.control-filelist ul li.group>ul>li.group>ul>li>a{padding-left:324px;margin-left:-270px}.control-filelist ul li.group>ul>li.group>ul>li.group>ul>li>a{padding-left:297px;margin-left:-243px}.control-filelist ul li.group>ul>li.group>ul>li.group>ul>li.group>ul>li>a{padding-left:270px;margin-left:-216px}.control-filelist ul li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li>a{padding-left:243px;margin-left:-189px}.control-filelist ul li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li>a{padding-left:216px;margin-left:-162px}.control-filelist ul li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li>a{padding-left:189px;margin-left:-135px}.control-filelist ul li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li>a{padding-left:162px;margin-left:-108px}.control-filelist ul li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li>a{padding-left:135px;margin-left:-81px}.control-filelist ul li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li>a{padding-left:108px;margin-left:-54px}.control-filelist ul li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li.group>ul>li>a{padding-left:81px;margin-left:-27px}.control-filelist ul li.group[data-status=collapsed]>h4 a:before,.control-filelist ul li.group[data-status=collapsed]>div.group>h4 a:before{transform:rotate(0deg) translate(3px, 0)}.control-filelist ul li.group[data-status=collapsed]>ul,.control-filelist ul li.group[data-status=collapsed]>div.subitems{display:none}.control-filelist ul li>div.controls{position:absolute;right:19px;top:6px}.control-filelist ul li>div.controls .dropdown{width:14px;height:21px}.control-filelist ul li>div.controls .dropdown.open a.control{display:block!important}.control-filelist ul li>div.controls .dropdown.open a.control:before{visibility:visible;display:block}.control-filelist ul li>div.controls a.control{color:var(--oc-toolbar-color);font-size:14px;visibility:hidden;overflow:hidden;width:14px;height:21px;display:none;text-decoration:none;cursor:pointer;padding:0;opacity:.5}.control-filelist ul li>div.controls a.control:before{visibility:visible;display:block;margin-right:0}.control-filelist ul li>div.controls a.control:hover{opacity:1}.control-filelist ul li:hover>div.controls,.control-filelist ul li:hover>a.control{display:block!important}.control-filelist ul li:hover>div.controls>a.control,.control-filelist ul li:hover>a.control>a.control{display:block!important}.control-filelist ul li .checkbox{position:absolute;top:-5px;right:0}.control-filelist ul li .checkbox label{margin-right:0}.control-filelist ul li .checkbox label:before{border-color:#cccccc}.control-filelist.single-line ul li a span.title{text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.control-filelist.filelist-hero ul li{background:var(--oc-primary-bg);border-bottom:none}.control-filelist.filelist-hero ul li>a{padding:11px 45px 10px 50px;font-size:13px;border-bottom:1px solid var(--oc-toolbar-border)}.control-filelist.filelist-hero ul li>a span.title{font-size:14px;font-weight:normal;color:var(--oc-primary-color)}.control-filelist.filelist-hero ul li>a span.description{font-size:13px}.control-filelist.filelist-hero ul li>a .list-icon{position:absolute;left:14px;top:9px;font-size:22px;color:#b7c0c2}.control-filelist.filelist-hero ul li>a .list-description{position:absolute;right:12px;top:50%;transform:translateY(-50%);font-size:16px;color:var(--oc-primary-color);cursor:help;opacity:.7;transition:opacity .15s ease}.control-filelist.filelist-hero ul li>a .list-description:hover{opacity:1}.control-filelist.filelist-hero ul li>a:hover{background:var(--oc-dropdown-hover-bg);border-bottom:1px solid var(--oc-dropdown-hover-bg) !important}.control-filelist.filelist-hero ul li>a:hover span.title,.control-filelist.filelist-hero ul li>a:hover span.description{color:var(--oc-dropdown-hover-color) !important}.control-filelist.filelist-hero ul li>a:hover .list-icon{color:var(--oc-dropdown-hover-color) !important}.control-filelist.filelist-hero ul li>a:hover .list-description{color:var(--oc-dropdown-hover-color)}.control-filelist.filelist-hero ul li>a:active{background:var(--oc-dropdown-active-bg);border-bottom:1px solid var(--oc-dropdown-active-bg) !important}.control-filelist.filelist-hero ul li>a:active span.title,.control-filelist.filelist-hero ul li>a:active span.description{color:var(--oc-dropdown-hover-color) !important}.control-filelist.filelist-hero ul li>a:active .list-icon{color:var(--oc-dropdown-hover-color) !important}.control-filelist.filelist-hero ul li>a:active .list-description{color:var(--oc-dropdown-hover-color)}.control-filelist.filelist-hero ul li .checkbox{top:-4px;right:0}.control-filelist.filelist-hero ul li.no-description .list-icon{top:10px}.control-filelist.filelist-hero ul li.has-description>a{padding-right:40px}.control-filelist.filelist-hero ul li.active>a{border-bottom:1px solid var(--oc-selection)}.control-filelist.filelist-hero ul li.active>a:after{display:none}.control-filelist.filelist-hero ul li.active>a>span.borders:before{content:' ';position:absolute;width:100%;height:1px;display:block;left:0;background-color:var(--oc-selection)}.control-filelist.filelist-hero ul li.active>a>span.borders:before{top:-1px}.control-filelist.filelist-hero ul li.active>a:hover>span.borders:before{background-color:var(--oc-dropdown-hover-bg)}.control-filelist.filelist-hero ul li.active>a:active>span.borders:before{background-color:var(--oc-dropdown-active-bg)}.control-filelist.filelist-hero ul li.active>a span.title,.control-filelist.filelist-hero ul li.active>a span.description{color:var(--oc-dropdown-hover-color) !important}.control-filelist.filelist-hero ul li.active>a .list-icon{color:var(--oc-dropdown-hover-color) !important}.control-filelist.filelist-hero ul li>h4{padding-top:7px;padding-bottom:6px;border-bottom:1px solid var(--oc-toolbar-border)}.control-filelist.filelist-hero ul li>div.controls{display:none;position:absolute;right:16px;top:15px}.control-filelist.filelist-hero ul li>div.controls>a.control{width:16px;height:23px;background:transparent;overflow:hidden;display:inline-block;color:var(--oc-dropdown-hover-color) !important;padding:0}.control-filelist.filelist-hero ul li>div.controls>a.control:before{font-size:17px}.control-filelist.filelist-hero ul li:hover>div.controls{display:block}.control-filelist.filelist-hero ul li.separator{position:relative;border-bottom:1px solid var(--bs-border-color);padding:12px 15px 13px 15px}.control-filelist.filelist-hero ul li.separator:before{z-index:31;content:'';display:block;width:0;height:0;border-left:9.5px solid transparent;border-right:9.5px solid transparent;border-top:11px solid var(--oc-primary-bg);border-bottom-width:0;position:absolute;left:13px;bottom:-8px}.control-filelist.filelist-hero ul li.separator:after{z-index:30;content:'';display:block;width:0;height:0;border-left:8.5px solid transparent;border-right:8.5px solid transparent;border-top:9px solid var(--bs-border-color);border-bottom-width:0;position:absolute;left:14px;bottom:-9px}.control-filelist.filelist-hero ul li.separator h5{color:var(--bs-heading-color);font-size:14px;margin:0;font-weight:normal;padding:0}.control-filelist.filelist-hero ul>li.group>ul>li>a{padding-left:66px}.control-filelist.filelist-hero.single-level ul li:hover{background:var(--oc-dropdown-hover-bg)}.control-filelist.filelist-hero.single-level ul li:hover>a{background:var(--oc-dropdown-hover-bg);border-bottom:1px solid var(--oc-dropdown-hover-bg) !important}.control-filelist.filelist-hero.single-level ul li:hover>a span.title,.control-filelist.filelist-hero.single-level ul li:hover>a span.description{color:var(--oc-dropdown-hover-color) !important}.control-filelist.filelist-hero.single-level ul li:hover>a .list-icon{color:var(--oc-dropdown-hover-color) !important}.control-filelist.filelist-hero.single-level ul li:active{background:var(--oc-dropdown-active-bg)}.control-filelist.filelist-hero.single-level ul li:active>a{background:var(--oc-dropdown-active-bg);border-bottom:1px solid var(--oc-dropdown-active-bg) !important}.control-filelist.filelist-hero.single-level ul li:active>a span.title,.control-filelist.filelist-hero.single-level ul li:active>a span.description{color:var(--oc-dropdown-hover-color) !important}.control-filelist.filelist-hero.single-level ul li:active>a .list-icon{color:var(--oc-dropdown-hover-color) !important}.control-filelist.snippet-list li>a{position:relative;color:var(--oc-toolbar-color)}.control-filelist.snippet-list li>a:before{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\e94b";font-size:19.5px;position:absolute;width:17px;height:19px;left:18px;top:12px}.control-filelist.snippet-list li>a:hover:before{color:var(--oc-dropdown-hover-color)}.control-filelist.snippet-list li.group ul li>a:before{left:34px}.oc-progress-bar{background-color:var(--oc-accent, #0076ff)}.control-scrollpanel{position:relative;background:var(--bs-tertiary-bg)}.control-scrollpanel .control-scrollbar.vertical>.scrollbar-scrollbar{right:0}.tooltip .tooltip-inner{text-align:left;padding:5px 8px}.tooltip.show{opacity:1}.status-indicator{width:10px;height:10px;border-radius:20px;display:inline-block;margin-right:3px;border:1px solid rgba(var(--bs-body-bg-rgb), .9)}.oc-logo-white{background-image:url(../images/october-logo-white.svg);background-position:50% 50%;background-repeat:no-repeat;background-size:contain}.oc-logo,.october-cms-logo-grey{background-image:url(../images/october-logo.svg);background-position:50% 50%;background-repeat:no-repeat;background-size:contain}.layout.control-tabs.oc-logo-transparent:not(.has-tabs),.flex-layout-column.oc-logo-transparent:not(.has-tabs),.layout-cell.oc-logo-transparent{background-size:auto 38px;background-repeat:no-repeat;background-image:url(../images/october-logo.svg);background-position:50% 50%;position:relative}.report-widget{padding:15px;background:var(--oc-popup-bg);box-sizing:border-box;border-radius:6px;font-size:14px}.report-widget h3{font-size:14px;color:#7e8c8d;text-transform:uppercase;font-weight:600;margin-top:0;margin-bottom:30px}.report-widget .height-100{height:100px}.report-widget .height-200{height:200px}.report-widget .height-300{height:300px}.report-widget .height-400{height:400px}.report-widget .height-500{height:500px}.report-widget p.report-description{margin-bottom:0;margin-top:15px;font-size:12px;line-height:190%;color:#7e8c8d}.report-widget a:not(.btn){color:#7e8c8d;text-decoration:none}.report-widget a:not(.btn):hover{color:var(--bs-link-color);text-decoration:none}.report-widget p.flash-message.static{margin-bottom:0}.report-widget .icon-circle.success,.report-widget .icon-circle-full.success{color:var(--bs-success)}.report-widget .icon-circle.primary,.report-widget .icon-circle-full.primary{color:var(--bs-primary)}.report-widget .icon-circle.warning,.report-widget .icon-circle-full.warning{color:var(--bs-warning)}.report-widget .icon-circle.danger,.report-widget .icon-circle-full.danger{color:var(--bs-danger)}.report-widget .icon-circle.info,.report-widget .icon-circle-full.info{color:var(--bs-info)}.control-treelist ol{padding:0;margin:0;list-style:none}.control-treelist ol ol{margin:0;margin-left:15px;padding-left:15px;border-left:1px solid #dbdee0}.control-treelist>ol>li>div.record:before{display:none}.control-treelist li{margin:0;padding:0}.control-treelist li>div.record{margin:0;font-size:14px;margin-bottom:5px;position:relative;display:block}.control-treelist li>div.record:before{color:#bdc3c7;font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f10c";font-size:6px;position:absolute;left:-18px;top:11px}.control-treelist li>div.record>a.move{display:inline-block;padding:7px 0 7px 10px;text-decoration:none;color:#bdc3c7}.control-treelist li>div.record>a.move:hover{color:rgba(215,225,234,0.7)}.control-treelist li>div.record>a.move:before{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f0c9"}.control-treelist li>div.record>span{color:var(--bs-table-color);display:inline-block;padding:7px 15px 7px 5px}.control-treelist li.dragged{position:absolute;z-index:2000;width:auto !important;height:auto !important}.control-treelist li.dragged>div.record{opacity:.5;background:rgba(215,225,234,0.7) !important}.control-treelist li.dragged>div.record>a.move:before,.control-treelist li.dragged>div.record>span{color:white}.control-treelist li.dragged>div.record:before{display:none}.control-treelist li.placeholder{display:inline-block;position:relative;background:rgba(215,225,234,0.7) !important;height:25px;margin-bottom:5px}.control-treelist li.placeholder:before{display:block;position:absolute;font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f053";color:#d35714;left:-10px;top:8px;z-index:2000}html .sidenav-tree{background:var(--oc-settings-bg)}.sidenav-tree{width:300px;flex-shrink:0;border-right:1px solid var(--oc-primary-border)}.sidenav-tree .sidenav-tree-scroll-canvas{position:absolute;top:0;bottom:0;left:0;right:0}.sidenav-tree .settings-search-toolbar-item{display:block;background:var(--oc-form-control-bg);position:relative}.sidenav-tree .settings-search-toolbar-item:before{display:block;width:15px;height:15px;position:absolute;background-position:-238px 0;top:13px;right:15px;font-size:0}.sidenav-tree .settings-search-toolbar-item input.form-control{border:none;outline:none;padding:1px 35px 1px 10px;border-bottom:1px solid var(--oc-primary-border);height:40px;background:transparent;border-radius:0}.sidenav-tree .settings-search-toolbar-item input.form-control.search{background-position:right -78px}.sidenav-tree ul{padding:0;margin:0;list-style:none}.sidenav-tree div.scrollbar-thumb{background:rgba(0,0,0,0.2) !important}.sidenav-tree ul.top-level>li[data-status=collapsed]>div.group h3:before{transform:rotate(0deg) translate(2px, -2px)}.sidenav-tree ul.top-level>li[data-status=collapsed]>div.group:before,.sidenav-tree ul.top-level>li[data-status=collapsed]>div.group:after{display:none}.sidenav-tree ul.top-level>li[data-status=collapsed] ul{display:none}.sidenav-tree ul.top-level>li>div.group{position:relative;position:sticky;top:0;z-index:1}.sidenav-tree ul.top-level>li>div.group h3{user-select:none;font-size:14px;font-weight:600;padding:9px 15px 7px 25px;margin:0;position:relative;cursor:pointer}.sidenav-tree ul.top-level>li>div.group h3:before{display:block;position:absolute;width:10px;height:10px;left:10px;top:10px;font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f105";transform:rotate(90deg) translate(5px, -3px);transition:all .1s ease;font-size:16px}.sidenav-tree ul.top-level>li>ul li{padding:5px}.sidenav-tree ul.top-level>li>ul li a{display:block;position:relative;padding:10px 5px 15px 44px;text-decoration:none !important;border-radius:6px}.sidenav-tree ul.top-level>li>ul li a i{position:absolute;left:11px;top:11px;font-size:22px}.sidenav-tree ul.top-level>li>ul li a span{display:block;line-height:150%}.sidenav-tree ul.top-level>li>ul li a span.header{margin-bottom:3px}.sidenav-tree ul.top-level>li>ul li a span.description{font-size:.875em}.sidenav-tree ul.top-level>li>ul li a:hover,.sidenav-tree ul.top-level>li>ul li.active a{opacity:1;text-decoration:none}.system-home-link{display:block;padding:13px 15px;background:var(--bs-secondary);color:white;font-size:14px;line-height:14px;text-decoration:none}.system-home-link i{display:inline-block;margin-right:10px}.system-home-link i:before{content:'';width:24px;height:11px;display:inline-block;background-image:url('../foundation/elements/backendicons/backend-icons.png');background-size:300px 300px;background-position:-52px 0}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){.system-home-link i:before{background-image:url('../foundation/elements/backendicons/backend-icons@2x.png')}}.system-home-link:hover{color:white;text-decoration:none}.system-home-link.back-link-other{display:none;margin-bottom:20px}@media (max-width:768px){.system-home-link.back-link-other{display:block}}.layout-container .system-home-link.back-link-other{margin:-20px -20px 20px -20px}body.slim-container .layout-container .system-home-link.back-link-other{margin:-20px 0 20px 0}body.compact-container .layout-container .system-home-link.back-link-other{margin:0}@media (max-width:768px){.sidenav-tree{border-right:none;width:100%;height:auto;display:none !important}body.has-sidenav-tree #layout-body{display:block}}body.sidenav-tree-expanded #layout-body{display:none !important}body.sidenav-tree-expanded .sidenav-tree{display:block !important;margin:0 auto;border-right:none;border-left:none;flex-shrink:inherit;width:100%;padding:30px 10px 50px 10px}body.sidenav-tree-expanded .sidenav-tree .scrollbar-scrollbar{display:none !important}body.sidenav-tree-expanded .sidenav-tree .system-home-link{display:none !important}body.sidenav-tree-expanded .sidenav-tree .sidenav-tree-scroll-canvas{position:relative}body.sidenav-tree-expanded .sidenav-tree ul.top-level>li>div.group{margin-top:10px}body.sidenav-tree-expanded .sidenav-tree ul.top-level>li>div.group h3{background:transparent}body.sidenav-tree-expanded .sidenav-tree ul.top-level>li>ul{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:flex-start;align-items:stretch;align-content:stretch}body.sidenav-tree-expanded .sidenav-tree ul.top-level>li>ul>li{display:inline-block;padding:10px;width:350px}body.sidenav-tree-expanded .sidenav-tree ul.top-level>li>ul>li a{background:var(--oc-settings-item);padding-top:12px;padding-right:15px;padding-bottom:15px;padding-left:54px;min-height:100px;height:100%;overflow:hidden}body.sidenav-tree-expanded .sidenav-tree ul.top-level>li>ul>li a i{top:15px;left:12px;font-size:28px}body.sidenav-tree-expanded .sidenav-tree ul.top-level>li>ul>li a:hover{background:var(--oc-settings-hover-bg)}body.sidenav-tree-expanded .settings-search-toolbar-item{margin:0px 10px 10px 10px;border-radius:20px !important}body.sidenav-tree-expanded .settings-search-toolbar-item input.form-control{border:1px solid var(--bs-border-color);border-radius:20px !important;padding-left:15px;line-height:40px;height:40px}body.sidenav-tree-expanded .settings-search-toolbar-item input.form-control:focus{border-color:var(--oc-border-focus)}.sidenav-tree ul.top-level>li>div.group h3{color:var(--oc-settings-color)}.sidenav-tree ul.top-level>li>div.group h3:before{color:var(--oc-settings-color)}.sidenav-tree ul.top-level>li>ul li a{color:var(--oc-settings-color)}.sidenav-tree ul.top-level>li>ul li a span.header{color:var(--oc-settings-color)}.sidenav-tree ul.top-level>li>ul li a span.description{color:var(--oc-settings-color);opacity:.7}.sidenav-tree ul.top-level>li>ul li a:hover{background:var(--oc-settings-hover-bg);color:var(--oc-settings-color)}.sidenav-tree ul.top-level>li>ul li a:hover span.header{color:var(--oc-settings-color)}.sidenav-tree ul.top-level>li>ul li a:hover span.description{color:var(--oc-settings-color);opacity:1}.sidenav-tree ul.top-level>li>ul li.active a{color:var(--oc-settings-active-color);background:var(--oc-settings-active-bg)}.sidenav-tree ul.top-level>li>ul li.active a span.header{color:var(--oc-settings-active-color)}.sidenav-tree ul.top-level>li>ul li.active a span.description{color:var(--oc-settings-active-color);opacity:1}@media (max-width:768px){.sidenav-tree-expanded #layout-body{display:none !important}}@media (max-width:767px){body.sidenav-tree-expanded .sidenav-tree{width:100%}body.sidenav-tree-expanded .sidenav-tree ul.top-level>li>ul>li{width:100%}}body:not(.sidenav-tree-expanded) .sidenav-tree .is-inactive-group{display:none}body:not(.sidenav-tree-expanded) .sidenav-tree.is-searching .is-inactive-group{display:block}div.panel,div.media-panel{padding:20px}div.panel.no-padding,div.media-panel.no-padding{padding:0}div.panel.padding-top,div.media-panel.padding-top{padding-top:20px}div.panel.padding-less,div.media-panel.padding-less{padding:15px}div.panel.transparent,div.media-panel.transparent{background:transparent}div.panel.border-left,div.media-panel.border-left{border-left:1px solid var(--oc-primary-border)}div.panel.border-right,div.media-panel.border-right{border-right:1px solid var(--oc-primary-border)}div.panel.border-bottom,div.media-panel.border-bottom{border-bottom:1px solid var(--oc-primary-border)}div.panel.border-top,div.media-panel.border-top{border-top:1px solid var(--oc-primary-border)}div.panel h3.section,div.media-panel h3.section,div.panel>label,div.media-panel>label{color:var(--oc-primary-color);font-size:13px;font-weight:600}div.panel>label,div.media-panel>label{margin-bottom:5px}div.panel .nav.selector-group,div.media-panel .nav.selector-group{margin:0 -20px 20px -20px}.nav.selector-group{font-size:13px;letter-spacing:.01em;margin-bottom:20px}.nav.selector-group li{padding:0 3px;margin:0}.nav.selector-group li a{padding:7px 20px 7px 23px;color:var(--oc-toolbar-color);border-radius:4px}.nav.selector-group li a:hover{background-color:var(--oc-toolbar-hover-bg)}.nav.selector-group li.active a{background:var(--oc-selection);padding-left:20px;color:white}.nav.selector-group li i[class^="icon-"]{font-size:17px;margin-right:6px;position:relative;top:1px}ul.tree-path{list-style:none;padding:0;margin-bottom:0}ul.tree-path li{display:inline-block;margin-right:1px;font-size:13px}ul.tree-path li:after{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f105";display:inline-block;font-size:13px;margin-left:5px;position:relative;top:1px;color:#95a5a6}ul.tree-path li:last-child a{cursor:default}ul.tree-path li:last-child:after{display:none}ul.tree-path li.go-up{font-size:12px;margin-right:7px}ul.tree-path li.go-up a{color:#95a5a6}ul.tree-path li.go-up a:hover{color:var(--bs-link-color)}ul.tree-path li.go-up:after{display:none}ul.tree-path li.root a{font-weight:600;color:var(--oc-primary-color)}ul.tree-path li a{color:#95a5a6}ul.tree-path li a:hover{text-decoration:none}table.name-value-list{border-collapse:collapse;font-size:13px}table.name-value-list th,table.name-value-list td{padding:4px 0 4px 0;vertical-align:top}table.name-value-list tr:first-child th,table.name-value-list tr:first-child td{padding-top:0}table.name-value-list th{font-weight:600;color:var(--oc-primary-color);padding-right:15px;text-transform:uppercase}table.name-value-list td{color:var(--bs-body-color);word-wrap:break-word}.scrollpad-scrollbar-size-tester{width:50px;height:50px;overflow-y:scroll;position:absolute;top:-200px;left:-200px}.scrollpad-scrollbar-size-tester div{height:100px}.scrollpad-scrollbar-size-tester::-webkit-scrollbar{width:0;height:0}div.control-scrollpad{position:relative;width:100%;height:100%;overflow:hidden}div.control-scrollpad>div{overflow:hidden;overflow-y:scroll;height:100%}div.control-scrollpad>div::-webkit-scrollbar{width:0;height:0}div.control-scrollpad[data-direction=horizontal]>div{overflow-x:scroll;overflow-y:hidden;width:100%}div.control-scrollpad[data-direction=horizontal]>div::-webkit-scrollbar{width:auto;height:0}div.control-scrollpad>.scrollpad-scrollbar{z-index:199;position:absolute;top:0;right:0;bottom:0;width:11px;background-color:transparent;opacity:0;overflow:hidden;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-webkit-transition:opacity .3s;transition:opacity .3s}div.control-scrollpad>.scrollpad-scrollbar .drag-handle{position:absolute;right:2px;min-height:10px;width:7px;background-color:rgba(0,0,0,0.35);-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}div.control-scrollpad>.scrollpad-scrollbar:hover{opacity:.7;transition:opacity 0 linear}div.control-scrollpad>.scrollpad-scrollbar[data-visible]{opacity:.7}div.control-scrollpad>.scrollpad-scrollbar[data-hidden]{display:none}div.control-scrollpad[data-direction=horizontal]>.scrollpad-scrollbar{top:auto;left:0;width:auto;height:11px}div.control-scrollpad[data-direction=horizontal]>.scrollpad-scrollbar .drag-handle{right:auto;top:2px;height:7px;min-height:0;min-width:10px;width:auto}.svg-icon-container img.svg-icon{display:inline-block}.svg-icon-container.svg-active-effects img.svg-icon{filter:grayscale(100%)}.svg-icon-container.svg-active-effects.active img.svg-icon{filter:none}body:not(.drag) .svg-icon-container.svg-active-effects:hover img.svg-icon{filter:none}.october-snackbar{padding:14px 40px 0 16px;background:#323232;font-size:14px;border-radius:4px;box-shadow:0 1px 3px rgba(0,0,0,0.3);max-width:100%;margin-right:16px;position:fixed;z-index:10600;left:16px;bottom:16px;color:white;opacity:0}.october-snackbar.enter{transition:opacity .2s}.october-snackbar.show-snackbar{opacity:1}.october-snackbar .snackbar-label{float:left;margin-right:16px;margin-bottom:14px}.october-snackbar button{outline:none;-webkit-appearance:none;background:transparent;border-radius:3px;border:none;color:#FFD422;font-weight:600}.october-snackbar button:focus{background:rgba(215,225,234,0.2)}.october-snackbar .snackbar-dismiss{font-size:0;color:transparent;width:22px;height:22px;position:absolute;right:10px;top:14px}.october-snackbar .snackbar-dismiss:before{content:'';width:7px;height:7px;left:7px;top:8px;display:block;position:absolute;background-image:url('../foundation/elements/backendicons/backend-icons.png');background-size:300px 300px;background-position:-156px -10px}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){.october-snackbar .snackbar-dismiss:before{background-image:url('../foundation/elements/backendicons/backend-icons@2x.png')}}.october-snackbar .snackbar-action{float:left;margin:0 0 14px -8px;padding:1px 8px;text-transform:uppercase}.october-tooltip{display:inline-block;padding:4px 8px 5px;position:absolute;border-radius:4px;max-width:200px;transition:opacity .15s ease-out,transform .15s ease-out;transform:scale(1);opacity:1;color:#fff;background:#536061;font-size:13px;line-height:140%;z-index:10700}.october-tooltip .tooltip-hotkey{display:inline-block;margin-left:5px;letter-spacing:1px}.october-tooltip .tooltip-hotkey:empty{display:none}.october-tooltip .tooltip-hotkey i{font-style:normal;margin-left:5px;padding:0 2px;display:inline-block;border-radius:3px;background:rgba(255,255,255,0.11);color:rgba(255,255,255,0.6)}.october-tooltip .tooltip-hotkey i:last-child{margin-right:-3px}.october-tooltip.tooltip-hidden{display:none}.october-tooltip.tooltip-invisible{opacity:0;transform:scale(.95)}#flotTip,#chart-tooltip{white-space:nowrap;padding:4px 8px 5px;background:#536061;position:absolute;z-index:10700;color:#fff;border-radius:4px;font-size:13px;opacity:1}.backend-toolbar-button .badge{display:block;z-index:9;width:8px;height:8px;position:absolute;top:5px;right:-3px;border-radius:50%;background-color:#dd1100;padding:0}button.backend-toolbar-button,a.backend-toolbar-button{line-height:inherit;display:inline-block;color:inherit;padding:0 6px;min-width:40px;text-align:center;text-decoration:none !important;border-radius:4px;outline:none;box-shadow:none;-webkit-appearance:none;border:none;background:transparent}button.backend-toolbar-button.control-button,a.backend-toolbar-button.control-button{color:var(--oc-toolbar-color);font-size:14px;margin-right:8px;line-height:30px}button.backend-toolbar-button.icon-only,a.backend-toolbar-button.icon-only{min-width:30px}button.backend-toolbar-button[disabled],a.backend-toolbar-button[disabled]{cursor:default}button.backend-toolbar-button[disabled]>i,a.backend-toolbar-button[disabled]>i,button.backend-toolbar-button[disabled]>span,a.backend-toolbar-button[disabled]>span,button.backend-toolbar-button[disabled]:after,a.backend-toolbar-button[disabled]:after{opacity:.5}button.backend-toolbar-button i,a.backend-toolbar-button i{display:inline-block;position:relative;font-size:16px;top:1px}button.backend-toolbar-button i+span.button-label,a.backend-toolbar-button i+span.button-label{margin-left:6px}button.backend-toolbar-button:not([disabled]):hover,a.backend-toolbar-button:not([disabled]):hover{background:var(--oc-toolbar-hover-bg)}button.backend-toolbar-button:not([disabled]):focus,a.backend-toolbar-button:not([disabled]):focus{background:var(--oc-toolbar-hover-bg)}button.backend-toolbar-button.has-menu:after,a.backend-toolbar-button.has-menu:after{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\e90a";font-size:16px;vertical-align:middle;margin-left:0;margin-right:-3px;position:relative;top:-1px}@media (hover:hover){button.backend-toolbar-button:focus:not([disabled]),a.backend-toolbar-button:focus:not([disabled]){background:var(--oc-toolbar-hover-bg)}}div.control-simplelist.is-selectable-box.menu-mode-selector{margin-left:0;margin-top:10px;border:none}div.control-simplelist.is-selectable-box.menu-mode-selector li{margin:0 16px 16px 0;width:175px}div.control-simplelist.is-selectable-box.menu-mode-selector li.active .menu-mode-box{border-color:var(--oc-accent)}div.control-simplelist.is-selectable-box.menu-mode-selector li .heading{margin:0;text-align:left;padding:18px 17px}div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box{display:block;border:2px solid var(--bs-border-color);background:var(--oc-form-control-bg);border-radius:10px}div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box .mode-image{display:block;height:42px;border-bottom:1px solid var(--bs-border-color);position:relative}div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box .mode-image:before{position:absolute;left:17px}div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box.menu-mode-box-inline .mode-image:before{content:'';width:131px;height:10px;display:inline-block;background-image:url('../foundation/elements/backendicons/backend-icons.png');background-size:300px 300px;background-position:0 -100px;top:15px}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box.menu-mode-box-inline .mode-image:before{background-image:url('../foundation/elements/backendicons/backend-icons@2x.png')}}div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box.menu-mode-box-text .mode-image:before{content:'';width:137px;height:6px;display:inline-block;background-image:url('../foundation/elements/backendicons/backend-icons.png');background-size:300px 300px;background-position:0 -120px;top:17px}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box.menu-mode-box-text .mode-image:before{background-image:url('../foundation/elements/backendicons/backend-icons@2x.png')}}div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box.menu-mode-box-tiles .mode-image:before{content:'';width:126px;height:20px;display:inline-block;background-image:url('../foundation/elements/backendicons/backend-icons.png');background-size:300px 300px;background-position:0 -139px;top:10px}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box.menu-mode-box-tiles .mode-image:before{background-image:url('../foundation/elements/backendicons/backend-icons@2x.png')}}div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box.menu-mode-box-icons .mode-image:before{content:'';width:79px;height:10px;display:inline-block;background-image:url('../foundation/elements/backendicons/backend-icons.png');background-size:300px 300px;background-position:-61px -169px;top:15px}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box.menu-mode-box-icons .mode-image:before{background-image:url('../foundation/elements/backendicons/backend-icons@2x.png')}}div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box.menu-mode-box-collapsed .mode-image:before{content:'';width:35px;height:10px;display:inline-block;background-image:url('../foundation/elements/backendicons/backend-icons.png');background-size:300px 300px;background-position:0 -170px;top:15px}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box.menu-mode-box-collapsed .mode-image:before{background-image:url('../foundation/elements/backendicons/backend-icons@2x.png')}}div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box.menu-mode-box-collapsed .mode-image:after{position:absolute;right:17px;top:13px;content:'';width:10px;height:16px;display:inline-block;background-image:url('../foundation/elements/backendicons/backend-icons.png');background-size:300px 300px;background-position:-44px -167px}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box.menu-mode-box-collapsed .mode-image:after{background-image:url('../foundation/elements/backendicons/backend-icons@2x.png')}}div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box.menu-mode-box-left{height:98px}div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box.menu-mode-box-left .mode-image{width:42px;height:93px;float:left;border-bottom:none;border-right:1px solid var(--bs-border-color)}div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box.menu-mode-box-left .mode-image:before{top:17px;content:'';width:10px;height:56px;display:inline-block;background-image:url('../foundation/elements/backendicons/backend-icons.png');background-size:300px 300px;background-position:-148px -100px}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box.menu-mode-box-left .mode-image:before{background-image:url('../foundation/elements/backendicons/backend-icons@2x.png')}}div.control-simplelist.is-selectable-box.menu-mode-selector .menu-mode-box.menu-mode-box-left .heading{margin-left:42px;text-align:left;padding:18px 17px}div.control-simplelist.is-selectable-box.color-mode-selector{margin-left:0;margin-top:10px;border:none;margin-bottom:-16px}div.control-simplelist.is-selectable-box.color-mode-selector ul{margin-bottom:0}div.control-simplelist.is-selectable-box.color-mode-selector li{margin:0 16px 16px 0;width:215px}div.control-simplelist.is-selectable-box.color-mode-selector li.active .color-mode-box{border-color:var(--oc-accent)}div.control-simplelist.is-selectable-box.color-mode-selector li .heading{margin:0;text-align:left;padding:18px 17px}div.control-simplelist.is-selectable-box.color-mode-selector .color-mode-box{display:block;border:2px solid var(--bs-border-color);background:var(--oc-form-control-bg);border-radius:10px}.modal-content .onboarding-modal{margin:-1px;border-radius:15px}.modal-content .onboarding-modal .modal-header{height:132px;border-bottom:none;background-image:url('../images/license-header.png');background-size:cover;background-repeat:no-repeat}.modal-content .onboarding-modal .modal-header .onboarding-logo{width:248px;height:41px}.modal-content .onboarding-modal .modal-header .btn-close{position:absolute;width:19px;height:19px;top:15px;right:15px;border:none;background-color:transparent;background-image:url('../images/license-header-close.png');background-size:cover;background-size:20px 20px;opacity:1}.modal-content .onboarding-modal .modal-footer .btn{border-radius:44px}body.onboarding-popup-visible .onboarding-popup-collapsed{display:none}.onboarding-popup-collapsed{display:block;position:fixed;cursor:pointer;width:60px;height:60px;right:55px;bottom:40px;border-radius:60px;background-color:#E67E21;z-index:1049}.onboarding-popup-collapsed:after{content:'';width:30px;height:40px;position:absolute;top:10px;left:15px;background-image:url('../images/october-leaf-white.svg');background-size:cover}.onboarding-popup-collapsed>div{position:absolute;width:70px;height:70px;left:-5px;top:-5px;transform:rotate(0);display:none}.onboarding-popup-collapsed>div:before{content:'';width:38px;height:38px;position:absolute;bottom:-4px;right:-1px;opacity:1;background-image:url('../images/license-spinner.png');background-size:39px 39px}.onboarding-popup-collapsed.onboarding-popup-just-collapsed>div{transform:rotate(720deg);transition:transform 2s linear}.onboarding-popup-collapsed.onboarding-popup-just-collapsed>div:before{opacity:0;transition:opacity .5s;transition-delay:1s}.onboarding-popup-collapsed.onboarding-popup-collapse-animation-start>div{display:block}@media (max-height:740px){.onboarding-popup-collapsed{right:35px;bottom:10px}}:root,[data-bs-theme="light"]{--oc-page-size-outer-bg:var(--bs-tertiary-bg);--oc-page-size-inner-bg:var(--bs-body-bg);--oc-page-size-border:var(--bs-border-color)}[data-bs-theme="dark"]{--oc-page-size-outer-bg:var(--bs-tertiary-bg);--oc-page-size-inner-bg:var(--bs-body-bg);--oc-page-size-border:var(--bs-border-color)}@media (pointer:fine){body.drag *{cursor:grab !important}}body.dragging,body.dragging *{cursor:move !important}body.loading,body.loading *{cursor:wait !important}body.no-select{user-select:none;cursor:default !important}html,body{height:100%;font-size:14px;line-height:1.42857143}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body.has-page-size{background:var(--oc-page-size-outer-bg)}body.has-page-size #layout-body{background:var(--oc-page-size-inner-bg)}body.has-page-size.has-sidenav-tree #layout-body{border-right:1px solid var(--oc-page-size-border)}#layout-canvas{min-height:100%;height:100%}#layout-body{width:0}.layout-container,.padded-container{padding:20px 20px 0 20px}.layout-container .container-flush,.padded-container .container-flush{padding-top:0}.layout-container .padded-container-inset,.padded-container .padded-container-inset{margin-left:-20px;margin-right:-20px}.whiteboard{background:white}[data-bs-theme="dark"] .whiteboard{background:#181a1e}.layout-fill-container{position:absolute;left:0;top:0;width:100%;height:100%}[data-calculate-width]>form,[data-calculate-width]>div{display:inline-block}body.compact-container .layout-container{padding:0 !important}body.slim-container .layout-container{padding-left:0 !important;padding-right:0 !important}.flex-layout-column{display:flex;flex-direction:column}.flex-layout-column.absolute{position:absolute !important}.flex-layout-column.fill-container{position:absolute;left:0;top:0;width:100%;height:100%}.flex-layout-row{display:flex;flex-direction:row}.flex-layout-column.justify-center,.flex-layout-row.justify-center{justify-content:center}.flex-layout-column.align-center,.flex-layout-row.align-center{align-items:center;align-content:center}.flex-layout-column.full-height,.flex-layout-row.full-height{min-height:100%}.flex-layout-column.full-width,.flex-layout-row.full-width{width:100%}.flex-layout-column.full-height-strict,.flex-layout-row.full-height-strict{height:100%}.flex-layout-item{margin:0}.flex-layout-item.fix{flex:0 0 auto}.flex-layout-item.stretch{flex:1 1 auto}.flex-layout-item.stretch-constrain{flex:1}.flex-layout-item.center{align-self:center}.flex-layout-item.relative{position:relative}.flex-layout-item.layout-container{max-width:none}ul.mainmenu-items{padding:0;margin:0;font-size:0;white-space:nowrap;user-select:none}ul.mainmenu-items>li.mainmenu-item{display:block}.mainmenu-item{display:inline-block;position:relative;vertical-align:top;user-select:none}.mainmenu-item>a,.mainmenu-item>.mainmenu-item-container{display:flex;flex-direction:row;align-items:baseline;text-decoration:none!important;outline:none;color:var(--oc-mainnav-color)}.mainmenu-item>a:after,.mainmenu-item>.mainmenu-item-container:after{opacity:.5}.mainmenu-item .nav-label{opacity:.5;font-size:14px;white-space:nowrap;text-overflow:ellipsis;flex:1 1 auto}.mainmenu-item .nav-icon{opacity:.5;position:absolute;top:0;display:inline-block;height:100%;color:var(--oc-mainnav-icon-color)}.mainmenu-item .nav-icon .svg-icon{-webkit-backface-visibility:hidden;backface-visibility:hidden;position:relative}.mainmenu-item .nav-icon i{vertical-align:middle;display:block;height:100%}.mainmenu-item .nav-icon i:before{position:relative}.mainmenu-item span.counter{flex:0 0 auto;position:relative;top:1px;padding:2px 2px;background-color:#ff3e1d;color:#ffffff;font-size:14px;line-height:100%;opacity:1;border-radius:4px;transform:scale(1);transition:transform .3s;margin-left:6px}.mainmenu-item span.counter.empty{transform:scale(0);opacity:0;padding:0;margin-left:0!important}.mainmenu-item.has-subitems>a:after,.mainmenu-item.has-subitems>.mainmenu-item-container:after{display:inline-block}.mainmenu-item.has-subitems span.counter{right:17px}.mainmenu-item.mainmenu-account .nav-icon{opacity:1;width:42px}.mainmenu-item.active .nav-label,.mainmenu-item.active-dropdown .nav-label,.mainmenu-item.active .nav-icon,.mainmenu-item.active-dropdown .nav-icon,.mainmenu-item.active>a:after,.mainmenu-item.active-dropdown>a:after{opacity:1}.mainmenu-item.has-solidicon .nav-icon{opacity:1}body:not(.drag) .mainmenu-item:hover .nav-label,body:not(.drag) .mainmenu-item:hover .nav-icon,body:not(.drag) .mainmenu-item:hover>a:after{opacity:1}body:not(.drag) ul.mainmenu-items.hover-effects li>a:hover{background:var(--bs-primary);color:white}@media (pointer:coarse){.mainmenu-item .nav-label{font-size:16px}}.layout-mainmenu .navbar{padding:0 20px 0 0;height:70px;background-color:var(--oc-mainnav-bg)}.layout-mainmenu .navbar [data-control="toolbar"]{position:absolute}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu]{margin-left:20px;margin-top:7px}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item{display:inline-block;margin-right:15px}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item:last-child{margin-right:0}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item>a{height:50px;padding:15px 10px 0 38px}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item.has-subitems .nav-label{padding-right:17px}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item.has-subitems>a:after{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\e90a";position:absolute;right:6px;top:19.4px;font-size:19.5px}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item .nav-label{font-size:16px}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item .nav-icon{line-height:52px;left:0;width:30px;text-align:center}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item .nav-icon .svg-icon{height:30px;width:30px}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item .nav-icon i{line-height:inherit;font-size:30px}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item .nav-icon .nav-colorpicker{width:0;height:0;background-color:transparent;border-right-color:var(--background-color);border-bottom-color:var(--background-color);border-top-color:var(--foreground-color);border-left-color:var(--foreground-color);border-width:10px;border-style:solid;border-radius:20px;display:inline-block;position:relative;top:10px}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item .nav-icon .nav-colorpicker:after{content:"";top:-10px;left:-10px;width:20px;height:20px;position:absolute;box-shadow:inset 0 0 0 1px rgba(var(--bs-body-bg-rgb), .2);border-radius:20px}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item.mainmenu-preview>a{padding-left:34px}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item.mainmenu-preview:not(.has-nolabel)>a{padding-right:15px}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item.mainmenu-preview:not(.has-nolabel)>a:after{right:11px}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item.mainmenu-taskbar>a{padding-right:0}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras{margin-left:10px}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras>li.mainmenu-item.mainmenu-taskbar,.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras>li.mainmenu-item.mainmenu-preview{margin-right:0}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras>li.mainmenu-item.mainmenu-taskbar .nav-icon,.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras>li.mainmenu-item.mainmenu-preview .nav-icon{left:0}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras>li.mainmenu-item.mainmenu-taskbar .nav-icon i,.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras>li.mainmenu-item.mainmenu-preview .nav-icon i{font-size:20px}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras>li.mainmenu-item.mainmenu-taskbar .nav-icon i:before,.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras>li.mainmenu-item.mainmenu-preview .nav-icon i:before{top:1px}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras>li.mainmenu-item.mainmenu-account{margin-right:0}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras>li.mainmenu-item.mainmenu-account>a{padding:0;margin-top:2px;width:42px}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras>li.mainmenu-item.mainmenu-account>a:after{display:none}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras>li.mainmenu-item.mainmenu-account .nav-icon{left:0;width:auto;position:static}.layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras li.mainmenu-toggle{display:none}@media (min-width:768px){.layout-mainmenu .navbar.navbar-mode-icons ul.mainmenu-items[data-main-menu]>li.mainmenu-item.has-subitems>a:after,.layout-mainmenu .navbar.navbar-mode-icons ul.mainmenu-items[data-main-menu] .nav-label{display:none}.layout-mainmenu .navbar.navbar-mode-icons ul.mainmenu-items[data-main-menu] .nav-icon{text-align:center}.layout-mainmenu .navbar.navbar-mode-tile{height:78px}.layout-mainmenu .navbar.navbar-mode-tile ul.mainmenu-items[data-main-menu]{margin-top:0}.layout-mainmenu .navbar.navbar-mode-tile ul.mainmenu-items[data-main-menu]>li.mainmenu-item{text-align:center}.layout-mainmenu .navbar.navbar-mode-tile ul.mainmenu-items[data-main-menu]>li.mainmenu-item>a{padding:11px 15px 10px;height:auto;display:block}.layout-mainmenu .navbar.navbar-mode-tile ul.mainmenu-items[data-main-menu]>li.mainmenu-item .nav-label{display:block;overflow:hidden;max-width:150px}.layout-mainmenu .navbar.navbar-mode-tile ul.mainmenu-items[data-main-menu]>li.mainmenu-item.has-subitems .nav-label{padding-right:17px}.layout-mainmenu .navbar.navbar-mode-tile ul.mainmenu-items[data-main-menu]>li.mainmenu-item.has-subitems a:after{margin:0;top:auto;right:11px;bottom:8.9px}.layout-mainmenu .navbar.navbar-mode-tile ul.mainmenu-items[data-main-menu]>li.mainmenu-item .nav-icon{display:block;height:30px;width:auto;position:static;line-height:1;margin-bottom:4px}.layout-mainmenu .navbar.navbar-mode-tile ul.mainmenu-items[data-main-menu]>li.mainmenu-item .nav-icon .svg-icon,.layout-mainmenu .navbar.navbar-mode-tile ul.mainmenu-items[data-main-menu]>li.mainmenu-item .nav-icon i{display:inline-block}.layout-mainmenu .navbar.navbar-mode-tile ul.mainmenu-items[data-main-menu]>li.mainmenu-item .nav-icon i:before{margin-right:0}.layout-mainmenu .navbar.navbar-mode-tile ul.mainmenu-items[data-main-menu]>li.mainmenu-item .nav-icon .nav-colorpicker{top:7px}.layout-mainmenu .navbar.navbar-mode-tile ul.mainmenu-items[data-main-menu]>li.mainmenu-item span.counter{margin:0;position:absolute;top:8px;left:50%}.layout-mainmenu .navbar.navbar-mode-tile ul.mainmenu-items[data-main-menu]>li.mainmenu-item.mainmenu-account>a{padding:12px 0 0 0;margin-top:1px;width:auto}.layout-mainmenu .navbar.navbar-mode-tile ul.mainmenu-items[data-main-menu]>li.mainmenu-item.mainmenu-account .nav-icon{height:auto}.layout-mainmenu .navbar.navbar-mode-tile ul.mainmenu-items[data-main-menu]>li.mainmenu-item.mainmenu-account div.mainmenu-account-avatar{width:52px;height:52px}.layout-mainmenu .navbar.navbar-mode-tile ul.mainmenu-items[data-main-menu]>li.mainmenu-item.mainmenu-account div.mainmenu-account-avatar img{width:50px;height:50px}.layout-mainmenu .navbar.navbar-mode-tile ul.mainmenu-items[data-main-menu]>li.mainmenu-item.mainmenu-preview.has-nolabel>a{width:auto;margin-top:14px}.layout-mainmenu .navbar.navbar-mode-text ul.mainmenu-items.mainmenu-general>li.mainmenu-item>a{padding:15px 10px 0 10px}.layout-mainmenu .navbar.navbar-mode-text ul.mainmenu-items.mainmenu-general>li.mainmenu-item .nav-icon{display:none}}@media (max-width:767px){#layout-mainmenu .navbar ul.mainmenu-items.mainmenu-general>li.mainmenu-item:not(.active){display:none}#layout-mainmenu .navbar ul.mainmenu-items.mainmenu-general>li.mainmenu-item>a{cursor:default;pointer-events:none}#layout-mainmenu .navbar ul.mainmenu-items.mainmenu-general>li.mainmenu-item span.counter,#layout-mainmenu .navbar ul.mainmenu-items.mainmenu-general>li.mainmenu-item.has-subitems>a:after{display:none}#layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras li.mainmenu-toggle{display:inline-block;margin-right:-10px}#layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras li.mainmenu-toggle>a{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;width:50px;position:relative;opacity:.7}#layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras li.mainmenu-toggle>a:before{content:'';width:20px;height:18px;display:inline-block;background-image:url('../foundation/elements/backendicons/backend-icons.png');background-size:300px 300px;background-position:0 0;position:absolute;top:19px;left:18px}#layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras li.mainmenu-toggle>a:hover{opacity:1}}@media (max-width:767px) and (-webkit-min-device-pixel-ratio:2),(max-width:767px) and (min-resolution:192dpi){#layout-mainmenu .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras li.mainmenu-toggle>a:before{background-image:url('../foundation/elements/backendicons/backend-icons@2x.png')}}#layout-mainmenu .navbar.navbar-mode-collapse ul.mainmenu-items.mainmenu-general>li.mainmenu-item:not(.active){display:none}#layout-mainmenu .navbar.navbar-mode-collapse ul.mainmenu-items.mainmenu-general>li.mainmenu-item>a{cursor:default;pointer-events:none}#layout-mainmenu .navbar.navbar-mode-collapse ul.mainmenu-items.mainmenu-general>li.mainmenu-item span.counter,#layout-mainmenu .navbar.navbar-mode-collapse ul.mainmenu-items.mainmenu-general>li.mainmenu-item.has-subitems>a:after{display:none}#layout-mainmenu .navbar.navbar-mode-collapse ul.mainmenu-items[data-main-menu].mainmenu-extras li.mainmenu-toggle{display:inline-block;margin-right:-10px}#layout-mainmenu .navbar.navbar-mode-collapse ul.mainmenu-items[data-main-menu].mainmenu-extras li.mainmenu-toggle>a{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;width:50px;position:relative;opacity:.7}#layout-mainmenu .navbar.navbar-mode-collapse ul.mainmenu-items[data-main-menu].mainmenu-extras li.mainmenu-toggle>a:before{content:'';width:20px;height:18px;display:inline-block;background-image:url('../foundation/elements/backendicons/backend-icons.png');background-size:300px 300px;background-position:0 0;position:absolute;top:19px;left:18px}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#layout-mainmenu .navbar.navbar-mode-collapse ul.mainmenu-items[data-main-menu].mainmenu-extras li.mainmenu-toggle>a:before{background-image:url('../foundation/elements/backendicons/backend-icons@2x.png')}}#layout-mainmenu .navbar.navbar-mode-collapse ul.mainmenu-items[data-main-menu].mainmenu-extras li.mainmenu-toggle>a:hover{opacity:1}body.main-menu-left #layout-mainmenu .main-menu-container{display:none}div.mainmenu-account-avatar{overflow:hidden;border-radius:9px;border:1px solid rgba(236,240,241,0.1);display:inline-block;width:42px;height:42px;vertical-align:middle}div.mainmenu-account-avatar img{width:42px;height:42px;display:block}.mainmenu-account.active-dropdown div.mainmenu-account-avatar{box-shadow:0 0 4px 0 var(--bs-link-color)}.layout-mainmenu .control-toolbar .toolbar-item.toolbar-primary:before{left:0}.layout-mainmenu .control-toolbar .toolbar-item.toolbar-primary:after{right:0}.left-side-menu-container{display:none !important;width:63px}.left-side-menu-container .layout-mainmenu{position:fixed;height:100%;width:63px}.left-side-menu-container .layout-mainmenu .main-menu-container{position:absolute;width:100%;height:100%}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar{position:absolute;padding-right:0;width:100%;height:100%;flex-direction:column}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar.control-toolbar{padding-bottom:0}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar.control-toolbar [data-control=toolbar]{width:auto}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar.control-toolbar .toolbar-item{padding-right:0;padding-bottom:20px}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar.control-toolbar .toolbar-item:last-child{padding-bottom:0}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar.control-toolbar .toolbar-item:before{display:none}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar.control-toolbar .toolbar-item:after{content:'';position:absolute;bottom:-5px;left:0;width:100%;height:9px;top:auto;display:block!important;background:transparent url(../../images/scrollable-panel-shadow.png) repeat-x left top;transition:opacity .1s;opacity:0}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar.control-toolbar .toolbar-item.scroll-after:after{opacity:1}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar.control-toolbar .toolbar-item.fix-width:after{display:none !important}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar.control-toolbar [data-control="toolbar"]{height:100%}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu]{margin-left:10px}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu].mainmenu-general{margin-left:15px}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras>li.mainmenu-item.mainmenu-preview>a{padding-left:0}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras>li.mainmenu-item.mainmenu-preview>a:after{top:27px;right:15px}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras>li.mainmenu-item.mainmenu-preview>a .nav-label{padding-left:13px}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras>li.mainmenu-item.mainmenu-preview>a .nav-icon{left:5px;position:static;width:40px}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras>li.mainmenu-item.mainmenu-preview>a .nav-icon i{position:relative;top:-15px;width:40px}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu].mainmenu-extras>li.mainmenu-item.mainmenu-preview>a .nav-icon .nav-colorpicker{top:5px}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item{display:block;margin-right:0;margin-bottom:15px;text-align:center}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item.mainmenu-toggle{display:none}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item.mainmenu-account .nav-label{position:relative;top:5px;padding-left:11px}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item.mainmenu-account>a:after{top:17px;right:15px}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item.mainmenu-account .nav-label,.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item.mainmenu-account>a:after{display:none}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item .nav-icon i{display:inline-block}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item .nav-label{padding-left:10px;text-align:left;display:none}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item>a{padding-right:15px;width:100%}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item>a>span.counter{left:-20px}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item>a:after{top:18.4px;display:none}.left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item.has-subitems>a:after{transform:rotate(-90deg)}.left-side-menu-container.width-check .layout-mainmenu .main-menu-container .navbar.control-toolbar div[data-control="toolbar"]{width:auto !important}.left-side-menu-container.width-check .layout-mainmenu .main-menu-container .navbar.control-toolbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item .nav-label,.left-side-menu-container.width-check .layout-mainmenu .main-menu-container .navbar.control-toolbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item>a:after{visibility:hidden;display:block}body.reveal-left-side-menu .left-side-menu-container .layout-mainmenu{z-index:599;box-shadow:10px 0 10px rgba(0,0,0,0.2)}body.reveal-left-side-menu .left-side-menu-container .layout-mainmenu .main-menu-container .navbar [data-control="toolbar"]{width:100%}body.reveal-left-side-menu .left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item .nav-label{display:block}body.reveal-left-side-menu .left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item a:after{display:block}body.reveal-left-side-menu .left-side-menu-container .layout-mainmenu .main-menu-container .navbar ul.mainmenu-items[data-main-menu]>li.mainmenu-item>a>span.counter{left:-10px}body.left-menu-submenu-displayed{overflow:hidden}body.left-menu-submenu-displayed ul.mainmenu-items.mainmenu-submenu-dropdown{box-shadow:10px 0 15px rgba(0,0,0,0.3);border-bottom-left-radius:0;border-top-right-radius:6px;margin-top:10px}body.left-menu-submenu-displayed ul.mainmenu-items.mainmenu-submenu-dropdown li:last-child{margin-bottom:6px}body.left-menu-submenu-displayed ul.mainmenu-items.mainmenu-submenu-dropdown li:first-child{margin-top:6px}body.main-menu-left .left-side-menu-container{display:block !important}div.mainmenu-leftmenu-overlay{z-index:598 !important}ul.mainmenu-items.mainmenu-submenu-dropdown{display:none;position:absolute;background:var(--oc-mainnav-bg);box-shadow:0 10px 15px rgba(0,0,0,0.3);z-index:601;border-bottom-left-radius:6px;border-bottom-right-radius:6px;user-select:none;padding:0;margin:0}ul.mainmenu-items.mainmenu-submenu-dropdown.show{display:block}ul.mainmenu-items.mainmenu-submenu-dropdown>li.mainmenu-item a{padding:7px 17px 7px 47px}ul.mainmenu-items.mainmenu-submenu-dropdown>li.mainmenu-item .nav-label,ul.mainmenu-items.mainmenu-submenu-dropdown>li.mainmenu-item .nav-icon{opacity:1}ul.mainmenu-items.mainmenu-submenu-dropdown>li.mainmenu-item .nav-icon{left:17px;width:20px;line-height:34px}ul.mainmenu-items.mainmenu-submenu-dropdown>li.mainmenu-item .nav-icon .svg-icon{width:20px}ul.mainmenu-items.mainmenu-submenu-dropdown>li.mainmenu-item .nav-icon i{font-size:20px;line-height:inherit}ul.mainmenu-items.mainmenu-submenu-dropdown>li.mainmenu-item span.counter{font-size:12px;padding:2px 3px;margin:2px 0 0 10px}ul.mainmenu-items.mainmenu-submenu-dropdown>li.mainmenu-item:last-child{margin-bottom:6px}ul.mainmenu-items.mainmenu-submenu-dropdown>li.mainmenu-item.divider{height:1px;margin:5px 0;background:rgba(255,255,255,0.1)}ul.mainmenu-items.mainmenu-submenu-dropdown>li.mainmenu-item.section-title{color:white;padding:7px 17px;white-space:nowrap;font-weight:600}ul.mainmenu-items.mainmenu-submenu-dropdown>li.mainmenu-item.section-title .nav-label{margin-right:0;display:block;overflow:hidden;max-width:250px;vertical-align:middle}ul.mainmenu-items.mainmenu-submenu-dropdown>li.mainmenu-item.has-bullet.is-selected a:before{content:"";background-color:white;border-radius:10px;position:absolute;top:50%;left:21px;height:8px;width:8px;margin-top:-4px}ul.mainmenu-items.mainmenu-submenu-dropdown>li.mainmenu-item.has-flag .nav-label{padding-right:25px}ul.mainmenu-items.mainmenu-submenu-dropdown>li.mainmenu-item.has-flag .nav-icon.nav-icon-flag{top:2px;left:auto;right:17px}ul.mainmenu-items.mainmenu-submenu-dropdown>li.mainmenu-item.has-flag .nav-icon.nav-icon-flag i{opacity:1;font-size:14px;display:inline-block;height:14px;border-radius:2px;box-shadow:inset 0 0 0 1px rgba(var(--bs-body-bg-rgb), .2)}div.mainmenu-submenu-overlay,div.mainmenu-leftmenu-overlay{position:fixed;z-index:600;left:0;top:0;width:100%;height:100%;display:none;background:rgba(0,0,0,0.01);opacity:.01}div.mainmenu-submenu-overlay.show{display:block}@media (pointer:coarse){ul.mainmenu-items.mainmenu-submenu-dropdown>li.mainmenu-item .nav-icon{line-height:38px}}#layout-mainmenu-responsive-container{position:fixed;top:0;right:0;width:0;max-width:100%;height:100%;z-index:601;overflow:hidden;transition:width .25s ease-in-out}#layout-mainmenu-responsive-container nav.responsive-menu-pane{z-index:602;background-color:var(--oc-mainnav-bg);transition:margin .25s ease-in-out;position:absolute;top:0;right:0;width:100%;height:100%;border-left:1px solid rgba(255,255,255,0.1);display:flex;flex-direction:column}#layout-mainmenu-responsive-container nav.responsive-menu-pane.mainmenu-pane{box-shadow:-10px 0 10px rgba(0,0,0,0.2)}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header{padding:25px;flex:0 0 auto}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header .mainmenu-item .mainmenu-item-container{padding-left:50px;white-space:nowrap}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header .mainmenu-item .mainmenu-item-container .nav-icon{left:0}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header .mainmenu-item .mainmenu-item-container .company-name,#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header .mainmenu-item .mainmenu-item-container .user-name{text-overflow:ellipsis;overflow:hidden}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header .mainmenu-item .mainmenu-item-container .company-name{font-size:16px}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header .mainmenu-item .mainmenu-item-container .user-name{color:rgba(255,255,255,0.7);cursor:pointer}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header .mainmenu-item .mainmenu-item-container .user-name:hover{color:var(--oc-mainnav-color)}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header .mainmenu-item>a{cursor:default;pointer-events:none;padding:7px 10px 7px 43px}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header .mainmenu-item>a .nav-icon{line-height:34px;left:0;width:30px}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header .mainmenu-item>a .nav-icon .svg-icon{height:30px;width:30px}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header .mainmenu-item>a .nav-icon i{line-height:inherit;font-size:30px}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header .mainmenu-item>a .counter{display:none}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header .mainmenu-item>a .nav-label{display:block;overflow:hidden;width:100%}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header .mainmenu-item.has-subitems .mainmenu-item-container:after{bottom:4px;cursor:pointer}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header.has-back-link .mainmenu-item{margin-left:36px}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header.has-back-link .go-back-link{display:block;width:24px;height:34px;position:absolute;left:25px;top:24px;z-index:603;font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header.has-back-link .go-back-link:after{content:'';width:24px;height:11px;display:inline-block;background-image:url('../foundation/elements/backendicons/backend-icons.png');background-size:300px 300px;background-position:-52px 0;position:absolute;left:0;top:12px}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header.has-back-link .go-back-link:after{background-image:url('../foundation/elements/backendicons/backend-icons@2x.png')}}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header .close-link{display:none;position:absolute;width:24px;height:24px;left:19px;top:33px;font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;z-index:603}#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header .close-link:after{content:'';width:11px;height:11px;display:inline-block;background-image:url('../foundation/elements/backendicons/backend-icons.png');background-size:300px 300px;background-position:-96px 0;position:absolute;left:6px;top:7px}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#layout-mainmenu-responsive-container nav.responsive-menu-pane .menu-header .close-link:after{background-image:url('../foundation/elements/backendicons/backend-icons@2x.png')}}#layout-mainmenu-responsive-container nav.responsive-menu-pane .scrollable-panel-container{flex:1 1 auto}#layout-mainmenu-responsive-container nav.responsive-menu-pane .mainmenu-item.has-subitems>a,#layout-mainmenu-responsive-container nav.responsive-menu-pane .mainmenu-item.has-subitems>.mainmenu-item-container{padding-right:30px}#layout-mainmenu-responsive-container nav.responsive-menu-pane .mainmenu-item.has-subitems>a:after,#layout-mainmenu-responsive-container nav.responsive-menu-pane .mainmenu-item.has-subitems>.mainmenu-item-container:after{content:'';width:24px;height:11px;display:inline-block;background-image:url('../foundation/elements/backendicons/backend-icons.png');background-size:300px 300px;background-position:-22px 0;position:absolute;right:0}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#layout-mainmenu-responsive-container nav.responsive-menu-pane .mainmenu-item.has-subitems>a:after,#layout-mainmenu-responsive-container nav.responsive-menu-pane .mainmenu-item.has-subitems>.mainmenu-item-container:after{background-image:url('../foundation/elements/backendicons/backend-icons@2x.png')}}#layout-mainmenu-responsive-container nav.responsive-menu-pane .mainmenu-items.scrollable{overflow:hidden}#layout-mainmenu-responsive-container nav.responsive-menu-pane .mainmenu-items>.mainmenu-item{margin-bottom:6px}#layout-mainmenu-responsive-container nav.responsive-menu-pane .mainmenu-items>.mainmenu-item>a{padding:7px 25px 7px 60px}#layout-mainmenu-responsive-container nav.responsive-menu-pane .mainmenu-items>.mainmenu-item.has-subitems>a{padding-right:55px}#layout-mainmenu-responsive-container nav.responsive-menu-pane .mainmenu-items>.mainmenu-item.has-subitems>a:after{right:25px;top:11px}#layout-mainmenu-responsive-container nav.responsive-menu-pane .mainmenu-items>.mainmenu-item .nav-label{display:block;overflow:hidden}#layout-mainmenu-responsive-container nav.responsive-menu-pane .mainmenu-items>.mainmenu-item .nav-icon{left:25px;width:20px;line-height:34px}#layout-mainmenu-responsive-container nav.responsive-menu-pane .mainmenu-items>.mainmenu-item .nav-icon .svg-icon{width:24px}#layout-mainmenu-responsive-container nav.responsive-menu-pane .mainmenu-items>.mainmenu-item .nav-icon i{font-size:24px;line-height:inherit}#layout-mainmenu-responsive-container nav.responsive-menu-pane .mainmenu-items>.mainmenu-item .counter{top:0}#layout-mainmenu-responsive-container nav.responsive-menu-pane.submenu-pane{z-index:603;margin-right:-100%;box-shadow:none}#layout-mainmenu-responsive-container nav.responsive-menu-pane.submenu-pane .menu-header[data-submenu-index=account] .go-back-link{top:30px}#layout-mainmenu-responsive-container nav.responsive-menu-pane.submenu-pane .menu-header[data-submenu-index=account] .mainmenu-item{height:42px}#layout-mainmenu-responsive-container nav.responsive-menu-pane.submenu-pane .menu-header .mainmenu-item .mainmenu-item-container .company-name{display:none}#layout-mainmenu-responsive-container nav.responsive-menu-pane.submenu-pane .menu-header .mainmenu-item .mainmenu-item-container .user-name{color:var(--oc-mainnav-color);position:relative;top:11px;cursor:default}#layout-mainmenu-responsive-container nav.responsive-menu-pane.submenu-pane .mainmenu-item.section-user-title{display:none}#layout-mainmenu-responsive-container nav.responsive-menu-pane.submenu-pane .mainmenu-item.divider{height:1px;margin:5px 0;background:rgba(255,255,255,0.1)}#layout-mainmenu-responsive-container nav.responsive-menu-pane.submenu-pane .mainmenu-item.section-title{color:white;padding:7px 17px;white-space:nowrap;font-weight:600}#layout-mainmenu-responsive-container nav.responsive-menu-pane.submenu-pane .mainmenu-item.section-title .nav-label{opacity:.5;margin-right:0;display:block;overflow:hidden;max-width:250px;vertical-align:middle}body.responsive-submenu-displayed #layout-mainmenu-responsive-container .responsive-menu-pane.mainmenu-pane{margin-right:100%}body.responsive-submenu-displayed #layout-mainmenu-responsive-container .responsive-menu-pane.submenu-pane{margin-right:0}body.responsive-menu-displayed{overflow:hidden}body.responsive-menu-displayed #layout-mainmenu-responsive-container{width:300px;box-shadow:-10px 0 10px rgba(0,0,0,0.2)}@media (pointer:coarse){#layout-mainmenu-responsive-container nav.responsive-menu-pane .mainmenu-items>.mainmenu-item{margin-bottom:12px}#layout-mainmenu-responsive-container nav.responsive-menu-pane .mainmenu-items>.mainmenu-item .nav-icon{line-height:38px}#layout-mainmenu-responsive-container nav.responsive-menu-pane.mainmenu-pane .menu-header{padding-left:60px}#layout-mainmenu-responsive-container nav.responsive-menu-pane.mainmenu-pane .menu-header .close-link{display:block}}@media (max-width:414px){body.responsive-menu-displayed #layout-mainmenu-responsive-container{width:414px}}:root{--oc-nav-logo-offset-top:-2px;--oc-nav-logo-offset-start:0;--oc-nav-logo-opacity:.5}.layout-mainmenu .navbar.has-logo .toolbar-logo{position:relative}.layout-mainmenu .navbar.has-logo ul.mainmenu-items[data-main-menu]{margin-left:10px}.layout-mainmenu .navbar.has-logo ul.mainmenu-items[data-main-menu] li.is-dashboard{display:none}.layout-mainmenu .navbar.has-logo .toolbar-item.toolbar-primary:before{left:-5px}.layout-mainmenu .navbar.has-logo.navbar-mode-tile .mainmenu-logo{padding-top:20px}.mainmenu-logo{display:block;padding-top:15px;padding-left:20px;padding-right:10px}.mainmenu-logo .nav-logo{height:40px;position:relative;top:var(--oc-nav-logo-offset-top);left:var(--oc-nav-logo-offset-start);opacity:var(--oc-nav-logo-opacity)}.mainmenu-logo.active .nav-logo,.mainmenu-logo.active-dropdown .nav-logo,.mainmenu-logo:hover .nav-logo{opacity:1}@media (max-width:768px){.mainmenu-logo{display:none}}.layout-sidenav-container>.layout-sidenav-spacer{position:relative;height:100%;width:240px}nav.layout-sidenav{position:absolute;height:100%;width:100%;box-sizing:border-box;font-size:14px;background:var(--oc-sidebar-bg)}nav.layout-sidenav ul{position:relative;margin:0;padding:25px 25px 25px 0;height:100%;overflow:hidden}nav.layout-sidenav ul>li.mainmenu-item{margin:0 0 10px 0}nav.layout-sidenav ul>li.mainmenu-item>a{padding:7px 10px 7px 58px;border-top-right-radius:20px;border-bottom-right-radius:20px;margin:0;transition:padding .1s;display:flex;flex-direction:row}nav.layout-sidenav ul>li.mainmenu-item>a:hover{background:transparent}nav.layout-sidenav ul>li.mainmenu-item>a .nav-icon{left:25px;width:20px;line-height:34px;opacity:1;text-align:center}nav.layout-sidenav ul>li.mainmenu-item>a .nav-icon .svg-icon{width:20px;-webkit-filter:grayscale(100%) invert(100%);filter:grayscale(100%) invert(100%)}nav.layout-sidenav ul>li.mainmenu-item>a .nav-icon i{font-size:20px;line-height:inherit;color:var(--oc-sidebar-color)}nav.layout-sidenav ul>li.mainmenu-item>a .nav-label{color:var(--oc-sidebar-color);font-size:14px;opacity:1;overflow:hidden}nav.layout-sidenav ul>li.mainmenu-item>a span.counter{position:absolute;top:10px;right:10px;font-size:12px;transition:all .1s}nav.layout-sidenav ul>li.mainmenu-item.has-counter>a{padding-right:25px}nav.layout-sidenav ul>li.mainmenu-item:last-child{margin-bottom:0}nav.layout-sidenav ul>li.mainmenu-item.divider{height:1px;margin:5px 0 10px;background:var(--oc-primary-border);margin-right:-25px}nav.layout-sidenav ul>li.mainmenu-item.section-title{color:var(--oc-sidebar-color);padding:7px 0 7px 17px;white-space:nowrap;font-weight:600;margin-bottom:0;transition:margin .1s ease-in-out;transition-delay:.25s}nav.layout-sidenav ul>li.mainmenu-item.section-title .nav-label{opacity:1;margin-right:0;display:block;overflow:hidden;max-width:250px;vertical-align:middle}nav.layout-sidenav ul>li.mainmenu-item.active>a{background:var(--oc-sidebar-active-bg) !important;border-left:3px solid var(--oc-sidebar-active-border);padding-left:55px}nav.layout-sidenav ul>li.mainmenu-item.active>a .nav-label{color:var(--oc-sidebar-active-color);font-weight:600}nav.layout-sidenav ul>li.mainmenu-item.active>a .nav-icon{opacity:1}nav.layout-sidenav ul>li.mainmenu-item.active>a .nav-icon i{color:var(--oc-sidebar-active-color)}nav.layout-sidenav ul>li.mainmenu-item.sidebar-button{margin-bottom:25px}nav.layout-sidenav ul>li.mainmenu-item.sidebar-button>a{margin-left:15px;background:var(--bs-primary);padding:9px 38px 0 13px;border-radius:5px;transition:all .1s ease-in-out;transition-property:border-radius,padding;height:40px;box-shadow:0 0 10px rgba(0,0,0,0.27);min-width:40px}nav.layout-sidenav ul>li.mainmenu-item.sidebar-button>a:hover{background:#5A5CEF}nav.layout-sidenav ul>li.mainmenu-item.sidebar-button>a:active{box-shadow:none}nav.layout-sidenav ul>li.mainmenu-item.sidebar-button>a .nav-icon{transition:width .1s ease-in-out;transition-delay:.25s;width:40px;top:3px;right:0;left:auto;text-align:center}nav.layout-sidenav ul>li.mainmenu-item.sidebar-button>a .nav-icon .svg-icon,nav.layout-sidenav ul>li.mainmenu-item.sidebar-button>a .nav-icon i{color:white}nav.layout-sidenav ul>li.mainmenu-item.sidebar-button>a .nav-label{transition:opacity .1s ease-in-out;transition-delay:.25s;opacity:1;color:white;font-weight:600}nav.layout-sidenav ul>li.mainmenu-item.sidebar-button.active>a{background:var(--bs-primary) !important}nav.layout-sidenav ul>li.mainmenu-item.sidebar-button.active>a:hover{background:#5A5CEF !important}@media (max-width:1199px){.layout-sidenav-container>.layout-sidenav-spacer{width:70px}#layout-sidenav{transition:width .1s,box-shadow .1s ease-in-out;box-shadow:none}#layout-sidenav:not(:hover) ul>li.mainmenu-item>a{padding-right:0}#layout-sidenav:not(:hover) ul>li.mainmenu-item>a .nav-label{visibility:hidden}#layout-sidenav:not(:hover) ul>li.mainmenu-item>a span.counter{top:-8px;right:1px}#layout-sidenav:not(:hover) ul>li.mainmenu-item.section-title{transition:none;margin-right:-15px}#layout-sidenav:not(:hover) ul>li.mainmenu-item.sidebar-button>a{margin-left:15px;border-radius:25px;width:40px;padding:9px 13px 0 0}#layout-sidenav:not(:hover) ul>li.mainmenu-item.sidebar-button>a .nav-label{transition:none;opacity:0}#layout-sidenav:not(:hover) ul>li.mainmenu-item.sidebar-button>a .nav-icon{transition:none;width:20px}#layout-sidenav:hover{z-index:20;width:240px;box-shadow:5px 0 5px rgba(0,0,0,0.1);transition-delay:.25s}#layout-sidenav:hover ul>li.mainmenu-item>a{transition-delay:.25s}#layout-sidenav:hover ul>li.mainmenu-item>a span.counter{top:9px;right:10px;transition-delay:.25s}}body:not(.drag) #layout-sidenav ul>li.mainmenu-item:not(.sidebar-button)>a:hover{background:var(--oc-sidebar-hover-bg)}body.sidenav-responsive .layout-sidenav-container{display:none!important}@media (max-width:767px){.layout-sidenav-container{display:none!important}}.layout-sidenav.sidenav-responsive{position:static;height:60px}.layout-sidenav.sidenav-responsive ul{padding:13px 20px 0 20px;overflow:visible}.layout-sidenav.sidenav-responsive ul>li.mainmenu-item{display:inline-block;margin:0 10px 0 0}.layout-sidenav.sidenav-responsive ul>li.mainmenu-item>a span.counter{position:relative;top:0;right:0}.layout-sidenav.sidenav-responsive ul>li.mainmenu-item.divider{height:60px;width:1.4px;margin:-13px 10px 0 0;border-left:1px solid var(--bs-border-color)}.layout-sidenav.sidenav-responsive ul>li.mainmenu-item.divider+.section-title{margin-left:-10px}.layout-sidenav.sidenav-responsive ul>li.mainmenu-item.sidebar-button{margin-bottom:0;margin-right:25px}.layout-sidenav.sidenav-responsive ul>li.mainmenu-item.sidebar-button>a{margin:0;height:34px;padding:7px 33px 0 13px}.layout-sidenav.sidenav-responsive ul>li.mainmenu-item.sidebar-button>a .nav-icon{top:0}.layout-sidenav.sidenav-responsive ul>li.mainmenu-item:not(.sidebar-button)>a{border-radius:20px;padding:7px 10px 7px 41px}.layout-sidenav.sidenav-responsive ul>li.mainmenu-item:not(.sidebar-button)>a .nav-icon{left:15px}.layout-sidenav.sidenav-responsive ul>li.mainmenu-item:not(.sidebar-button).active>a{border-left:none}.responsive-sidebar-toolbar{background:var(--oc-sidebar-bg)}.responsive-sidebar-toolbar:before,.responsive-sidebar-toolbar:after{display:none !important}.secondary-nav>.control-toolbar{padding-bottom:0}.secondary-nav{display:none}body:not(.drag) .layout-sidenav.sidenav-responsive ul>li.mainmenu-item:not(.sidebar-button)>a:hover{background:var(--oc-sidebar-hover-bg)}body.sidenav-responsive .secondary-nav{display:block}@media (max-width:767px){.secondary-nav{display:block}}#layout-side-panel{border-right:1px solid var(--oc-primary-border)}#layout-side-panel .sidepanel-content-header{background:#d7e1eA;font-size:15px;padding:12px 20px 13px;position:relative}body.display-side-panel #layout-side-panel{display:block;position:absolute;z-index:600;width:350px;border-right:none;-webkit-box-shadow:3px 0 3px 0 rgba(0,0,0,0.1);box-shadow:3px 0 3px 0 rgba(0,0,0,0.1)}[data-bs-theme="dark"] #layout-side-panel .sidepanel-content-header{background:var(--bs-secondary-bg)}#layout-footer{width:100%;z-index:100;height:60px;position:fixed;bottom:0;color:#666666;background-color:rgba(255,255,255,0.8);border-top:1px solid #dfdfdf}#layout-footer .brand,#layout-footer .tagline{margin:10px;height:40px;line-height:40px}#layout-footer .brand{float:left;font-size:16px}#layout-footer .brand .logo{margin:0 10px}#layout-footer .tagline{float:right}#layout-footer .tagline p{color:#999}html body.outer{background:white}body.outer .outer-form-cell{width:350px;padding:40px;border-right:1px solid #ECF0F1;vertical-align:top}body.outer .outer-theme-cell{background-color:#FEF6EB;vertical-align:middle;text-align:center;overflow:hidden}body.outer .outer-theme-cell img{display:inline-block;margin:0 auto}body.outer h1{margin:0 0 20px 0;padding:0;text-align:center;font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}body.outer h1 img{max-width:180px;width:100%;display:inline-block}body.outer .outer-form-container{margin:0 auto}body.outer .outer-form-container p{color:#2C3E4F}body.outer .outer-form-container h2{font-size:22px;font-family:serif;margin:0;padding:10px 0 30px 0;color:#2C3E4F;text-align:center}body.outer .outer-form-container h2:empty{padding:0}body.outer .outer-form-container h2:after{content:'';display:block;height:2px;width:60px;margin:30px auto 20px;background:#536061}body.outer .outer-form-container .forgot-password{margin-top:8px}body.outer .outer-form-container .forgot-password a{color:#536061;text-decoration:underline}body.outer .outer-form-container .horizontal-form input.form-control{margin-bottom:20px}body.outer .outer-form-container .horizontal-form+.pull-right.forgot-password{margin-top:-47px;position:relative}body.outer.setup .outer-form-cell{width:500px}@media (max-width:576px){body.outer .outer-form-cell{width:100% !important}body.outer .outer-theme-cell{display:none}}:root,[data-bs-theme="light"]{--oc-fancy-tabs-bg:var(--bs-secondary)}[data-bs-theme="dark"]{--oc-fancy-tabs-bg:var(--oc-editor-tab-bg)}body.breadcrumb-fancy .control-breadcrumb,.control-breadcrumb.breadcrumb-fancy{margin-bottom:0;margin-left:15px;margin-top:10px;margin-bottom:10px}.fancy-layout .tab-collapse-icon{position:absolute;display:block;text-decoration:none;outline:none;opacity:.6;transition:all .3s;font-size:12px;color:var(--bs-body-color);right:11px}.fancy-layout .tab-collapse-icon:hover{text-decoration:none;opacity:1}.fancy-layout .tab-collapse-icon.primary{color:var(--bs-body-bg);bottom:-25px;z-index:100;-webkit-transform:scale(1, -1);transform:scale(1, -1)}.fancy-layout .tab-collapse-icon.primary i{position:relative;display:block}.fancy-layout .tab-collapse-icon.tabless{display:none}.fancy-layout .control-tabs.master-tabs:before,.fancy-layout.control-tabs.master-tabs:before,.fancy-layout .control-tabs.master-tabs:after,.fancy-layout.control-tabs.master-tabs:after{top:13px;font-size:14px;color:rgba(255,255,255,0.5)}.fancy-layout .control-tabs.master-tabs:before,.fancy-layout.control-tabs.master-tabs:before{left:8px}.fancy-layout .control-tabs.master-tabs:after,.fancy-layout.control-tabs.master-tabs:after{right:8px}.fancy-layout .control-tabs.master-tabs.scroll-before:before,.fancy-layout.control-tabs.master-tabs.scroll-before:before{color:var(--bs-body-color)}.fancy-layout .control-tabs.master-tabs.scroll-after:after,.fancy-layout.control-tabs.master-tabs.scroll-after:after{color:var(--bs-body-color)}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container{background:var(--oc-fancy-tabs-bg);padding-left:20px;padding-right:20px}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs{margin-left:-8px}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li{margin-left:-5px;padding-top:3px}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li span.tab-close,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li span.tab-close{top:14px;right:-5px;left:auto;z-index:110;font-family:sans-serif;color:rgba(255,255,255,0.3)}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li span.tab-close i,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li span.tab-close i{top:4px;right:1px;font-style:normal;font-weight:bold;font-size:16px}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li span.tab-close:hover,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li span.tab-close:hover{color:white !important}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a{border-bottom:none;background:transparent;font-size:14px;color:rgba(255,255,255,0.5);padding:6px 0 0 24px!important;overflow:visible;text-decoration:none}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a:hover,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a:hover{color:white}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a>span.title,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a>span.title{position:relative;display:inline-block;padding:12px 5px 0 5px;height:38px;font-size:14px;z-index:100;background-color:transparent}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a>span.title:before,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a>span.title:before,.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a>span.title:after,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a>span.title:after{content:' ';position:absolute;width:20px;display:block;height:37px;top:0;z-index:100;background-color:transparent}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a>span.title:before,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a>span.title:before{left:-14px;border-radius:8px 0 0 0;transform:skewX(-10deg)}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a>span.title:after,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a>span.title:after{right:-14px;border-radius:0 8px 0 0;transform:skewX(10deg)}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a>span.title span,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a>span.title span{border-top:none;padding:0;margin-top:0;overflow:visible}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a:before,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a:before{z-index:110;position:absolute;top:18px;left:22px}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a[class*=icon]>span.title,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li a[class*=icon]>span.title{padding-left:18px}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li.active,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li.active{top:1px}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li.active a,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li.active a{z-index:107;color:var(--bs-body-color)}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li.active span.tab-close,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li.active span.tab-close{color:rgba(var(--bs-body-color-rgb), .3)}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li.active span.tab-close:hover,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li.active span.tab-close:hover{color:var(--bs-body-color) !important}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li.active a>span.title,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li.active a>span.title{background-color:var(--bs-body-bg);z-index:105}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li.active a>span.title:before,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li.active a>span.title:before{z-index:107;background-color:var(--bs-body-bg)}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li.active a>span.title:after,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li.active a>span.title:after{background-color:var(--bs-body-bg);z-index:107}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li[data-modified] span.tab-close i,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li[data-modified] span.tab-close i{top:5px;font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;color:inherit}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li[data-modified] span.tab-close i:before,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li[data-modified] span.tab-close i:before{font-family:'octo-icon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f111";font-size:9px}.fancy-layout .control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li:first-child,.fancy-layout.control-tabs.master-tabs>div>div.tabs-container>ul.nav-tabs>li:first-child{margin-left:0}.fancy-layout .control-tabs.master-tabs[data-closable]>div>div.tabs-container>ul.nav-tabs>li a>span.title,.fancy-layout.control-tabs.master-tabs[data-closable]>div>div.tabs-container>ul.nav-tabs>li a>span.title{padding-right:10px}.fancy-layout .control-tabs.master-tabs.has-tabs:before,.fancy-layout.control-tabs.master-tabs.has-tabs:before,.fancy-layout .control-tabs.master-tabs.has-tabs:after,.fancy-layout.control-tabs.master-tabs.has-tabs:after{display:block}.fancy-layout .control-tabs.secondary-tabs:before,.fancy-layout.control-tabs.secondary-tabs:before{left:5px}.fancy-layout .control-tabs.secondary-tabs:after,.fancy-layout.control-tabs.secondary-tabs:after{right:5px}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs{position:relative;border-top:1px solid var(--oc-primary-border);background:var(--bs-body-bg);padding-top:10px}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs:before,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs:before{position:absolute;bottom:0;height:1px;width:100%;z-index:9;content:' ';border-bottom:2px solid var(--oc-primary-border)}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li{border-right:none;padding-right:0;margin-right:-10px;vertical-align:top}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li a,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li a{font-size:14px;font-weight:600;padding-bottom:3px;margin:0;position:relative;top:-1px;z-index:11;background:transparent;overflow:visible;border-bottom:1px solid transparent}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li a span,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li a span{position:relative;display:inline-block;padding:4px 25px 0px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li a span:before,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li a span:before,.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li a span:after,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li a span:after{content:'';display:none;border-top:2px solid var(--oc-primary-border);position:absolute;background:transparent;top:0;z-index:-1;width:20px;bottom:-2px;transform-origin:bottom}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li a span:before,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li a span:before{left:0;border-left:2px solid var(--oc-primary-border);-webkit-border-radius:8px 0 0 0;-moz-border-radius:8px 0 0 0;border-radius:8px 0 0 0;-webkit-transform:skewX(-10deg);-ms-transform:skewX(-10deg);transform:skewX(-10deg)}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li a span:after,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li a span:after{right:0;border-right:2px solid var(--oc-primary-border);-webkit-border-radius:0 8px 0 0;-moz-border-radius:0 8px 0 0;border-radius:0 8px 0 0;-webkit-transform:skewX(10deg);-ms-transform:skewX(10deg);transform:skewX(10deg)}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li a span span,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li a span span{border-top:2px solid transparent;margin-top:-4px;padding-left:0;padding-right:0;padding-top:7px}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li:hover a,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li:hover a{color:var(--bs-body-color)}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li:first-child,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li:first-child{padding-left:10px}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li:last-child,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li:last-child{margin-right:0}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li.active a,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li.active a{z-index:13;top:0}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li.active a>span.title,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li.active a>span.title{border-top-color:var(--oc-primary-border)}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li.active a>span.title:before,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li.active a>span.title:before,.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li.active a>span.title:after,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li.active a>span.title:after{display:block;border-color:var(--oc-primary-border)}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li.active a>span.title span,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li.active a>span.title span{border-top-color:var(--oc-primary-border)}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li.active a:before,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li.active a:before{position:absolute;bottom:-1px;height:2.2px;right:2px;left:2px;content:' ';background-color:var(--bs-body-bg)}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li.active.tab-content-bg a span.title:before,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li.active.tab-content-bg a span.title:before,.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li.active.tab-content-bg a span.title:after,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li.active.tab-content-bg a span.title:after{background-color:var(--bs-body-bg)}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li.active.tab-content-bg a span.title span,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li.active.tab-content-bg a span.title span{background-color:var(--bs-body-bg);margin-left:-6px;padding-left:6px;margin-right:-6px;padding-right:6px;margin-bottom:-6px;padding-bottom:6px}.fancy-layout .control-tabs.secondary-tabs>div>ul.nav-tabs>li.active.tab-content-bg a:before,.fancy-layout.control-tabs.secondary-tabs>div>ul.nav-tabs>li.active.tab-content-bg a:before{background-color:var(--bs-body-bg)}.fancy-layout .control-tabs.secondary-tabs .tab-collapse-icon,.fancy-layout.control-tabs.secondary-tabs .tab-collapse-icon{position:absolute;display:block;text-decoration:none;outline:none;opacity:.6;transition:all .3s;font-size:12px;color:var(--bs-body-color);right:11px}.fancy-layout .control-tabs.secondary-tabs .tab-collapse-icon:hover,.fancy-layout.control-tabs.secondary-tabs .tab-collapse-icon:hover{text-decoration:none;opacity:1}.fancy-layout .control-tabs.secondary-tabs .tab-collapse-icon.primary,.fancy-layout.control-tabs.secondary-tabs .tab-collapse-icon.primary{color:var(--bs-body-bg);bottom:-25px;z-index:100;-webkit-transform:scale(1, -1);transform:scale(1, -1)}.fancy-layout .control-tabs.secondary-tabs .tab-collapse-icon.primary i,.fancy-layout.control-tabs.secondary-tabs .tab-collapse-icon.primary i{position:relative;display:block}.fancy-layout .control-tabs.secondary-tabs .tab-collapse-icon.tabless,.fancy-layout.control-tabs.secondary-tabs .tab-collapse-icon.tabless{display:none}.fancy-layout .control-tabs.secondary-tabs .tab-collapse-icon.primary,.fancy-layout.control-tabs.secondary-tabs .tab-collapse-icon.primary{color:var(--bs-body-color);top:15px;right:11px;bottom:auto}.fancy-layout .control-tabs.secondary-tabs.primary-collapsed .tab-collapse-icon.primary,.fancy-layout.control-tabs.secondary-tabs.primary-collapsed .tab-collapse-icon.primary{-webkit-transform:scale(1, 1);transform:scale(1, 1)}.fancy-layout .control-tabs.primary-tabs,.fancy-layout.control-tabs.primary-tabs{border-top:1px solid var(--oc-primary-border)}.fancy-layout .control-tabs.primary-tabs.master-area>div>ul.nav-tabs,.fancy-layout.control-tabs.primary-tabs.master-area>div>ul.nav-tabs{-webkit-transition:background-color .5s;transition:background-color .5s;background:var(--bs-body-bg)}.fancy-layout .control-tabs.primary-tabs>div>ul.nav-tabs,.fancy-layout.control-tabs.primary-tabs>div>ul.nav-tabs{background:#7F8C8D;margin-left:0 !important;margin-right:0 !important}.fancy-layout .control-tabs.primary-tabs>div>ul.nav-tabs:before,.fancy-layout.control-tabs.primary-tabs>div>ul.nav-tabs:before{border-bottom-width:1px}.fancy-layout .control-tabs.primary-tabs>div>ul.nav-tabs>li,.fancy-layout.control-tabs.primary-tabs>div>ul.nav-tabs>li{background:transparent;border:none;margin-right:-8px}.fancy-layout .control-tabs.primary-tabs>div>ul.nav-tabs>li:first-child,.fancy-layout.control-tabs.primary-tabs>div>ul.nav-tabs>li:first-child{margin-left:-15px}.fancy-layout .control-tabs.primary-tabs>div>ul.nav-tabs>li a,.fancy-layout.control-tabs.primary-tabs>div>ul.nav-tabs>li a{background:transparent;border:none;padding:3px 10px 3px;font-size:14px;font-weight:400;color:#536061;top:2px}.fancy-layout .control-tabs.primary-tabs>div>ul.nav-tabs>li a span.title,.fancy-layout.control-tabs.primary-tabs>div>ul.nav-tabs>li a span.title{border-bottom:2px solid transparent;border-top:none;padding:0 7px 6px}.fancy-layout .control-tabs.primary-tabs>div>ul.nav-tabs>li a span.title:before,.fancy-layout.control-tabs.primary-tabs>div>ul.nav-tabs>li a span.title:before,.fancy-layout .control-tabs.primary-tabs>div>ul.nav-tabs>li a span.title:after,.fancy-layout.control-tabs.primary-tabs>div>ul.nav-tabs>li a span.title:after{display:none !important}.fancy-layout .control-tabs.primary-tabs>div>ul.nav-tabs>li a span.title span,.fancy-layout.control-tabs.primary-tabs>div>ul.nav-tabs>li a span.title span{border-width:0;vertical-align:top}.fancy-layout .control-tabs.primary-tabs>div>ul.nav-tabs>li a:before,.fancy-layout.control-tabs.primary-tabs>div>ul.nav-tabs>li a:before{top:2px;bottom:-6px}.fancy-layout .control-tabs.primary-tabs>div>ul.nav-tabs>li.active a,.fancy-layout.control-tabs.primary-tabs>div>ul.nav-tabs>li.active a{color:var(--bs-body-color);font-weight:600}.fancy-layout .control-tabs.primary-tabs>div>ul.nav-tabs>li.active a:before,.fancy-layout.control-tabs.primary-tabs>div>ul.nav-tabs>li.active a:before{display:none}.fancy-layout .control-tabs.primary-tabs>div>ul.nav-tabs>li.active a span.title,.fancy-layout.control-tabs.primary-tabs>div>ul.nav-tabs>li.active a span.title{border-bottom-color:var(--bs-primary)}.fancy-layout .control-tabs.primary-tabs>.tab-content>.tab-pane,.fancy-layout.control-tabs.primary-tabs>.tab-content>.tab-pane{padding:20px 20px 0 20px}.fancy-layout .control-tabs.primary-tabs>.tab-content>.tab-pane.pane-compact,.fancy-layout.control-tabs.primary-tabs>.tab-content>.tab-pane.pane-compact{padding:0}.fancy-layout .control-tabs.primary-tabs.collapsed,.fancy-layout.control-tabs.primary-tabs.collapsed{display:none}.fancy-layout .control-tabs.has-tabs>div.tab-content,.fancy-layout.control-tabs.has-tabs>div.tab-content{background:var(--bs-body-bg)}.fancy-layout .control-tabs>div.tab-content>div.tab-pane,.fancy-layout.control-tabs>div.tab-content>div.tab-pane{padding:0}.fancy-layout .control-tabs>div.tab-content>div.tab-pane.padded-pane,.fancy-layout.control-tabs>div.tab-content>div.tab-pane.padded-pane{padding:20px 20px 0 20px}.fancy-layout .form-tabless-fields:not(.not-fancy){position:relative;background:var(--bs-body-bg);padding:10px 15px 0 15px}.fancy-layout .form-tabless-fields:not(.not-fancy):before,.fancy-layout .form-tabless-fields:not(.not-fancy):after{content:" ";display:table}.fancy-layout .form-tabless-fields:not(.not-fancy):after{clear:both}.fancy-layout .form-tabless-fields:not(.not-fancy):before,.fancy-layout .form-tabless-fields:not(.not-fancy):after{content:" ";display:table}.fancy-layout .form-tabless-fields:not(.not-fancy):after{clear:both}.fancy-layout .form-tabless-fields:not(.not-fancy) label{text-transform:uppercase;font-size:12px;color:var(--oc-primary-color);margin-bottom:0}.fancy-layout .form-tabless-fields:not(.not-fancy) input[type=text]{background:transparent;border:none;color:var(--bs-body-color);font-size:24px;font-weight:400;height:auto;padding:0;box-shadow:none;border:1px solid transparent}.fancy-layout .form-tabless-fields:not(.not-fancy) input[type=text]:focus,.fancy-layout .form-tabless-fields:not(.not-fancy) input[type=text]:hover{border:1px solid var(--oc-primary-border)}.fancy-layout .form-tabless-fields:not(.not-fancy) .form-group{padding-bottom:0}.fancy-layout .form-tabless-fields:not(.not-fancy) .form-group.is-required>label:after{display:none}.fancy-layout .form-tabless-fields:not(.not-fancy) .form-group .form-translatable{position:absolute;margin:0;right:3px;top:28px}.fancy-layout .form-tabless-fields:not(.not-fancy) .tab-collapse-icon{position:absolute;display:block;text-decoration:none;outline:none;opacity:.6;transition:all .3s;font-size:12px;color:var(--bs-body-color);right:11px}.fancy-layout .form-tabless-fields:not(.not-fancy) .tab-collapse-icon:hover{text-decoration:none;opacity:1}.fancy-layout .form-tabless-fields:not(.not-fancy) .tab-collapse-icon.primary{color:var(--bs-body-bg);bottom:-25px;z-index:100;-webkit-transform:scale(1, -1);transform:scale(1, -1)}.fancy-layout .form-tabless-fields:not(.not-fancy) .tab-collapse-icon.primary i{position:relative;display:block}.fancy-layout .form-tabless-fields:not(.not-fancy) .tab-collapse-icon.tabless{display:none}.fancy-layout .form-tabless-fields:not(.not-fancy) .tab-collapse-icon.tabless{top:14px}.fancy-layout .form-tabless-fields:not(.not-fancy).collapsed{padding:5px 23px 0 10px}.fancy-layout .form-tabless-fields:not(.not-fancy).collapsed .tab-collapse-icon.tabless{-webkit-transform:scale(1, -1);transform:scale(1, -1)}.fancy-layout .form-tabless-fields:not(.not-fancy).collapsed .form-group:not(.collapse-visible){display:none}.fancy-layout .form-tabless-fields:not(.not-fancy).collapsed .form-buttons{margin-left:10px;padding-bottom:0}.fancy-layout .form-tabless-fields:not(.not-fancy) .loading-indicator-container .loading-indicator{background-color:var(--bs-body-bg);color:var(--oc-primary-color)}.fancy-layout .form-tabless-fields:not(.not-fancy) .loading-indicator-container .loading-indicator>span,.fancy-layout .form-tabless-fields:not(.not-fancy) .loading-indicator-container .loading-indicator>div{margin-left:15px}.fancy-layout .form-buttons{margin-top:10px;padding-top:5px;padding-bottom:3px;margin-left:-15px;margin-right:-15px;padding-left:10px;padding-right:10px;border-top:1px solid var(--oc-primary-border)}.fancy-layout .form-buttons.loading-indicator-container.in-progress{overflow:hidden}.fancy-layout .form-buttons .btn{padding:6px;margin-right:8px;background:transparent;color:var(--oc-toolbar-color);font-size:14px;font-weight:normal;box-shadow:none}.fancy-layout .form-buttons .btn:before{margin-right:2px}.fancy-layout .form-buttons .btn:hover{background:var(--oc-toolbar-hover-bg)}.fancy-layout .form-buttons .btn:last-child{margin-right:0}.fancy-layout .form-buttons .btn[class^="oc-icon-"]:before,.fancy-layout .form-buttons .btn[class*=" oc-icon-"]:before{opacity:1}.fancy-layout form.oc-data-changed .btn.save{opacity:1}.fancy-layout .field-codeeditor{border:none !important;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.fancy-layout .field-codeeditor .editor-code{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.fancy-layout .field-markdowneditor{border:none !important}.fancy-layout .field-richeditor{border:none}.fancy-layout .field-richeditor,.fancy-layout .field-richeditor .fr-toolbar,.fancy-layout .field-richeditor .fr-wrapper{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;border-top-right-radius:0;border-top-left-radius:0}.fancy-layout .secondary-content-tabs .field-richeditor .fr-toolbar{background:var(--bs-body-bg)}body.side-panel-not-fixed .fancy-layout .field-richeditor{border-left:none}.form-document-layout .control-tabs.primary-tabs:not(.is-nested)>div>ul.nav-tabs{padding:13px 0;background:var(--oc-document-tabs-bg);border-bottom:1px solid var(--oc-primary-border)}.form-document-layout .control-tabs.primary-tabs:not(.is-nested)>div>ul.nav-tabs:before{display:none}.form-document-layout .control-tabs.primary-tabs:not(.is-nested)>div>ul.nav-tabs>li{border-right:1px solid var(--oc-primary-border);border-bottom:none;margin:0!important;padding:0}.form-document-layout .control-tabs.primary-tabs:not(.is-nested)>div>ul.nav-tabs>li:first-child{margin-left:-12px !important}.form-document-layout .control-tabs.primary-tabs:not(.is-nested)>div>ul.nav-tabs>li:last-child{border-right:none}.form-document-layout .control-tabs.primary-tabs:not(.is-nested)>div>ul.nav-tabs>li a{padding:2px 12px 0;height:24px;border:none;top:0}.form-document-layout .control-tabs.primary-tabs:not(.is-nested)>div>ul.nav-tabs>li a:before{display:none}.form-document-layout .control-tabs.primary-tabs:not(.is-nested)>div>ul.nav-tabs>li a>span.title{padding:0;margin:0}.form-document-layout .control-tabs.primary-tabs:not(.is-nested)>div>ul.nav-tabs>li a>span.title:before,.form-document-layout .control-tabs.primary-tabs:not(.is-nested)>div>ul.nav-tabs>li a>span.title:after{display:none}.form-document-layout .control-tabs.primary-tabs:not(.is-nested)>div>ul.nav-tabs>li a>span.title>span{margin:0;padding:0;border-top:none;color:inherit;background-color:transparent}.form-document-layout .control-tabs.primary-tabs:not(.is-nested)>div>ul.nav-tabs>li a>span.title>span:before,.form-document-layout .control-tabs.primary-tabs:not(.is-nested)>div>ul.nav-tabs>li a>span.title>span:after{display:none}.form-document-layout .control-tabs.primary-tabs:not(.is-nested)>div>ul.nav-tabs>li.active a{color:var(--bs-primary)}.form-document-layout .control-tabs.primary-tabs:not(.is-nested)[data-single-tab]>div>ul.nav-tabs{display:none!important}.form-document-layout .document-header .form-tabless-fields .form-group.primary-title-field .form-label,.form-document-layout .document-header .form-tabless-fields .form-group.primary-title-field .form-translatable{display:none}.form-document-layout .document-header .form-tabless-fields .form-group.primary-title-field .form-control{display:block;font-size:24px;width:100%;border:1px solid var(--bs-body-bg);outline:none;background:transparent;color:var(--bs-body-color);padding:1px}.form-document-layout .document-header .form-tabless-fields .form-group.primary-title-field .form-control:not([disabled]):hover,.form-document-layout .document-header .form-tabless-fields .form-group.primary-title-field .form-control:not([disabled]):focus{border:1px solid var(--bs-border-color);box-shadow:none}.form-document-layout .document-header .form-tabless-fields .form-group.primary-title-field span.form-control{opacity:.5}.form-document-layout div.primary-tabs-container>div>.layout-row{display:table-cell}.form-document-layout div.primary-tabs-container>div>.layout-row .tab-pane.is-adaptive{padding-top:0}.form-document-layout .component-backend-document .document-progress-indicator{z-index:1}.form-document-layout .form-group.span-adaptive{margin-left:-20px;margin-right:-20px}.form-document-layout .form-group.span-adaptive>.form-label,.form-document-layout .form-group.span-adaptive>.form-translatable{display:none}.form-document-layout .record-management-controls{padding-left:20px;padding-bottom:20px}.form-with-sidebar .form-with-sidebar-canvas{position:absolute;top:0;bottom:0;left:0;right:0}.form-with-sidebar>.form-sidebar{width:300px;background:var(--bs-tertiary-bg);border-left:1px solid var(--oc-primary-border)}.form-with-sidebar.sidebar-width-350>.form-sidebar{width:350px}.form-with-sidebar.sidebar-width-400>.form-sidebar{width:400px}.form-with-sidebar.sidebar-width-450>.form-sidebar{width:450px}.form-with-sidebar.sidebar-width-500>.form-sidebar{width:500px}.form-with-sidebar.sidebar-width-550>.form-sidebar{width:550px}.form-with-sidebar.sidebar-width-600>.form-sidebar{width:600px}.form-with-sidebar.sidebar-width-650>.form-sidebar{width:650px}.form-with-sidebar.sidebar-width-700>.form-sidebar{width:700px}.form-with-sidebar.sidebar-width-750>.form-sidebar{width:750px}@media (max-width:992px){.form-with-sidebar{flex-direction:column-reverse}.form-with-sidebar .form-with-sidebar-canvas{position:relative}.form-with-sidebar>.form-contents{height:auto}.form-with-sidebar>.form-contents .control-breadcrumb{display:none}.form-with-sidebar>.form-sidebar{width:100% !important;height:auto;border-left:none;border-bottom:1px solid var(--oc-primary-border)}.form-with-sidebar>.form-sidebar .control-scrollbar{overflow:visible;height:auto}.form-with-sidebar>.form-sidebar .control-scrollbar .scrollbar-scrollbar{display:none !important}}.flyout-container>.flyout-content{overflow:hidden;width:0;left:0!important;transition:width .1s}.flyout-overlay{width:100%;height:100%;top:0;z-index:5000;position:absolute;background-color:rgba(0,0,0,0);transition:background-color .3s}.flyout-toggle{position:absolute;top:20px;left:0;width:23px;height:25px;background:#35425b;cursor:pointer;border-bottom-right-radius:4px;border-top-right-radius:4px;color:#bdc3c7;font-size:10px}.flyout-toggle i{margin:7px 0 0 6px;display:inline-block}.flyout-toggle:hover i{color:#ffffff}body.flyout-visible{overflow:hidden}body.flyout-visible .flyout-overlay{background-color:rgba(0,0,0,0.3)}#layout-sidenav-responsive.has-toggle{position:relative}#layout-sidenav-responsive.has-toggle ul.nav{padding-left:35px}#layout-sidenav-responsive.has-toggle .flyout-toggle{top:18px}
================================================
FILE: modules/backend/assets/foundation/controls/autocomplete/README.md
================================================
# Autocomplete
### Autocomplete
Autocomplete control.
## JavaScript API
```js
$('input').autocomplete({
source: { something: 'Something', else: 'Else' }
})
```
================================================
FILE: modules/backend/assets/foundation/controls/autocomplete/autocomplete.js
================================================
/*
* The autcomplete plugin, a forked version of Bootstrap's original typeahead plugin.
*
* Data attributes:
* - data-control="autocomplete" - enables the autocomplete plugin
*
* JavaScript API:
* $('input').autocomplete()
*
* Forked by daftspunk:
*
* - Source can be an object [{ value: 'something', label: 'Something' }, { value: 'else', label: 'Something Else' }]
* - Source can also be { something: 'Something', else: 'Else' }
*/
!function($){
"use strict"; // jshint ;_;
/* AUTOCOMPLETE PUBLIC CLASS DEFINITION
* ================================= */
var Autocomplete = function (element, options) {
this.$element = $(element)
this.options = $.extend({}, $.fn.autocomplete.defaults, options)
this.matcher = this.options.matcher || this.matcher
this.sorter = this.options.sorter || this.sorter
this.highlighter = this.options.highlighter || this.highlighter
this.updater = this.options.updater || this.updater
this.source = this.options.source
this.$menu = $(this.options.menu)
this.shown = false
this.listen()
}
Autocomplete.prototype = {
constructor: Autocomplete,
select: function () {
var val = this.$menu.find('.active').attr('data-value')
this.$element
.val(this.updater(val))
.change()
return this.hide()
},
updater: function (item) {
return item
},
show: function () {
var offset = this.options.bodyContainer ? this.$element.offset() : this.$element.position(),
pos = $.extend({}, offset, {
height: this.$element[0].offsetHeight
}),
cssOptions = {
top: pos.top + pos.height
, left: pos.left
}
if (this.options.matchWidth) {
cssOptions.width = this.$element[0].offsetWidth
}
this.$menu.css(cssOptions)
if (this.options.bodyContainer) {
$(document.body).append(this.$menu)
}
else {
this.$menu.insertAfter(this.$element)
}
this.$menu.show()
this.shown = true
return this
},
hide: function () {
this.$menu.hide()
this.shown = false
return this
},
lookup: function (event) {
var items
this.query = this.$element.val()
if (!this.query || this.query.length < this.options.minLength) {
return this.shown ? this.hide() : this
}
items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source
return items ? this.process(items) : this
},
itemValue: function (item) {
if (typeof item === 'object')
return item.value;
return item;
},
itemLabel: function (item) {
if (typeof item === 'object')
return item.label;
return item;
},
itemsToArray: function (items) {
var newArray = []
$.each(items, function(value, label){
newArray.push({ label: label, value: value })
})
return newArray
},
process: function (items) {
var that = this
if (typeof items == 'object')
items = this.itemsToArray(items)
items = $.grep(items, function (item) {
return that.matcher(item)
})
items = this.sorter(items)
if (!items.length) {
return this.shown ? this.hide() : this
}
return this.render(items.slice(0, this.options.items)).show()
},
matcher: function (item) {
return ~this.itemValue(item).toLowerCase().indexOf(this.query.toLowerCase())
},
sorter: function (items) {
var beginswith = [],
caseSensitive = [],
caseInsensitive = [],
item,
itemValue
while (item = items.shift()) {
itemValue = this.itemValue(item)
if (!itemValue.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
else if (~itemValue.indexOf(this.query)) caseSensitive.push(item)
else caseInsensitive.push(item)
}
return beginswith.concat(caseSensitive, caseInsensitive)
},
highlighter: function (item) {
var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
return '' + match + ''
})
},
render: function (items) {
var that = this
items = $(items).map(function (i, item) {
i = $(that.options.item).attr('data-value', that.itemValue(item))
i.find('a').html(that.highlighter(that.itemLabel(item)))
return i[0]
})
items.first().addClass('active')
this.$menu.html(items)
return this
},
next: function (event) {
var active = this.$menu.find('.active').removeClass('active'),
next = active.next()
if (!next.length) {
next = $(this.$menu.find('li')[0])
}
next.addClass('active')
},
prev: function (event) {
var active = this.$menu.find('.active').removeClass('active'),
prev = active.prev()
if (!prev.length) {
prev = this.$menu.find('li').last()
}
prev.addClass('active')
},
listen: function () {
this.$element
.on('focus.autocomplete', $.proxy(this.focus, this))
.on('blur.autocomplete', $.proxy(this.blur, this))
.on('keypress.autocomplete', $.proxy(this.keypress, this))
.on('keyup.autocomplete', $.proxy(this.keyup, this))
if (this.eventSupported('keydown')) {
this.$element.on('keydown.autocomplete', $.proxy(this.keydown, this))
}
this.$menu
.on('click.autocomplete', $.proxy(this.click, this))
.on('mouseenter.autocomplete', 'li', $.proxy(this.mouseenter, this))
.on('mouseleave.autocomplete', 'li', $.proxy(this.mouseleave, this))
},
eventSupported: function(eventName) {
var isSupported = eventName in this.$element
if (!isSupported) {
this.$element.setAttribute(eventName, 'return;')
isSupported = typeof this.$element[eventName] === 'function'
}
return isSupported
},
move: function (e) {
if (!this.shown) return
switch(e.key) {
case 'Tab':
case 'Enter':
case 'Escape':
e.preventDefault()
break
case 'ArrowUp':
e.preventDefault()
this.prev()
break
case 'ArrowDown':
e.preventDefault()
this.next()
break
}
e.stopPropagation()
},
keydown: function (e) {
this.suppressKeyPressRepeat = ~$.inArray(e.key, ['ArrowDown','ArrowUp','Tab','Enter','Escape'])
this.move(e)
},
keypress: function (e) {
if (this.suppressKeyPressRepeat) return
this.move(e)
},
keyup: function (e) {
switch(e.keyCode) {
case 40: // down arrow
case 38: // up arrow
case 16: // shift
case 17: // ctrl
case 18: // alt
break
case 9: // tab
case 13: // enter
if (!this.shown) return
this.select()
break
case 27: // escape
if (!this.shown) return
this.hide()
break
default:
this.lookup()
}
e.stopPropagation()
e.preventDefault()
},
focus: function (e) {
this.focused = true
},
blur: function (e) {
this.focused = false
if (!this.mousedover && this.shown) this.hide()
},
click: function (e) {
e.stopPropagation()
e.preventDefault()
this.select()
this.$element.focus()
},
mouseenter: function (e) {
this.mousedover = true
this.$menu.find('.active').removeClass('active')
$(e.currentTarget).addClass('active')
},
mouseleave: function (e) {
this.mousedover = false
if (!this.focused && this.shown) this.hide()
},
destroy: function() {
this.hide()
this.$element.removeData('autocomplete')
this.$menu.remove()
this.$element.off('.autocomplete')
this.$menu.off('.autocomplete')
this.$element = null
this.$menu = null
}
}
/* AUTOCOMPLETE PLUGIN DEFINITION
* =========================== */
var old = $.fn.autocomplete
$.fn.autocomplete = function (option) {
return this.each(function () {
var $this = $(this)
, data = $this.data('autocomplete')
, options = typeof option == 'object' && option
if (!data) $this.data('autocomplete', (data = new Autocomplete(this, options)))
if (typeof option == 'string') data[option]()
})
}
$.fn.autocomplete.defaults = {
source: [],
items: 8,
menu: '
',
item: '
',
minLength: 1,
bodyContainer: false
}
$.fn.autocomplete.Constructor = Autocomplete
/* AUTOCOMPLETE NO CONFLICT
* =================== */
$.fn.autocomplete.noConflict = function () {
$.fn.autocomplete = old
return this
}
/* AUTOCOMPLETE DATA-API
* ================== */
function paramToObj(name, value) {
if (value === undefined) value = ''
if (typeof value == 'object') return value
try {
return oc.parseJSON("{" + value + "}")
}
catch (e) {
throw new Error('Error parsing the '+name+' attribute value. '+e)
}
}
$(document).on('focus.autocomplete.data-api', '[data-control="autocomplete"]', function (e) {
var $this = $(this)
if ($this.data('autocomplete')) return
var opts = $this.data()
if (opts.source) {
opts.source = paramToObj('data-source', opts.source)
}
$this.autocomplete(opts)
})
}(window.jQuery);
/* =============================================================
* bootstrap-autocomplete.js v2.3.1
* http://twitter.github.com/bootstrap/javascript.html#autocomplete
* =============================================================
* Copyright 2012 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ============================================================ */
================================================
FILE: modules/backend/assets/foundation/controls/autocomplete/autocomplete.less
================================================
.autocomplete.dropdown-menu {
background: @dropdown-bg;
li a {
padding: 3px 12px;
}
}
================================================
FILE: modules/backend/assets/foundation/controls/balloon-selector/README.md
================================================
# Balloon selector
One
Two
Three
If you don't define `data-control="balloon-selector"` then the control will act as a static list of labels.
 {.img-responsive .frame}
## Line chart
The next example shows a line chart markup. Data sets are defined with the SPAN elements inside the chart element.
 {.img-responsive .frame}
## Bar chart
The next example shows a bar chart markup. The **wrap-legend** class is optional, it manages the legend layout. The **data-height** and **data-full-width** attributes are optional as well.
Label 1 100
Label 2 100
Label 3 100
 {.img-responsive .frame}
# Example
Label 1 100
Label 2 100
Label 3 100
Label 1 100
Label 2 100
Label 3 100
## Status list
A list of statuses and values
# Example
```
### Display static
A flash message can be rendered as a static element by attaching the `static` class. The `data-control` attribute is not needed.
```html
Import completed successfully (success)
Informative info box is informational (info)
Phasers have been set to stun (warning)
We couldn't help you with that (error)
```
### Data attributes
- data-control="flash-message" - enables the flash message plugin
- data-interval="2" - the interval to display the message in seconds, optional. Default: 2
### JavaScript API
```js
oc.flashMsg({
'text': 'Record saved.',
'type': 'success',
'interval': 3
})
```
================================================
FILE: modules/backend/assets/foundation/controls/flashmessage/flashmessage.less
================================================
//
// Flash Messages
// --------------------------------------------------
@color-flash-success-bg: @brand-success;
@color-flash-error-bg: #cc3300;
@color-flash-warning-bg: #f0ad4e;
@color-flash-info-bg: #5fb6f5;
@color-flash-text: #ffffff;
.flash-message.static {
color: @color-flash-text;
font-size: 14px;
padding: 10px 30px 10px 15px;
word-wrap: break-word;
text-align: left;
border-radius: @border-radius-base;
&.success { background: @color-flash-success-bg; }
&.error { background: @color-flash-error-bg; }
&.warning { background: @color-flash-warning-bg; }
&.info { background: @color-flash-info-bg; }
button {
float: none;
position: absolute;
right: 10px;
top: 12px;
color: white;
.hide-text();
opacity: 1;
outline: none;
&:before {
.icon-OctoFont();
content: @icon-cross;
color: white;
font-size: 16px;
position: relative;
}
&:hover {
opacity: 1;
}
}
}
================================================
FILE: modules/backend/assets/foundation/controls/inspector/README.md
================================================
# Inspector control
Inspector is a visual configuration tool that is used in several places of October back-end. The most known usage of Inspector is the CMS components configuration feature, but Inspector is not limited with the CMS. In fact, it's a universal tool that can be used with any element on a back-end page.
The Inspector loads the configuration schema from an inspectable element, builds the user interface, and writes values entered by users back to the inspectable element. The first version of Inspector was supporting only a few scalar value types - strings and Booleans, without an option to edit any complex data.
The current version of Inspector allows to edit any imaginable data structures, including cases where users create enumerable data elements right in the Inspector interface.
This section describes the client-side Inspector API without going into details about the back-end usage of the data Inspector generates. Inspector accepts the configuration schema in JSON format and generates values in JSON format as well. Providing the configuration and interpreting the generated values is up to developers. For example, the CMS module uses information returned from component's defineProperties() method to generate the configuration JSON string and converts JSON values generated by Inspector to the components configuration in CMS templates. In this document we are focusing only on the JSON format.
## Configuring inspectable elements
Clicking an inspectable element displays Inspector for that element. Any HTML element could be made inspectable by adding data attributes to it. The required attributes are:
* `data-inspectable` - indicates that Inspector should be created when the element is clicked.
* `data-inspector-title` - sets the Inspector popup title.
* `data-inspector-config` - contains the Inspector configuration JSON string. If this attribute is not specified, the configuration is loaded from the server, see the [Dynamic configuration and dynamic items](#dynamic-configuration-and-dynamic-items) section below.
Inspectable elements should also contain a hidden input element used by Inspector for reading and writing values. The input element should be marked with the `data-inspector-values` data attribute.
Example inspectable element markup:
```html
```
### Optional data attributes
There are several optional data attributes and features that could be defined in an inspectable element or in elements around it:
* `data-inspector-offset` - sets offset, in pixels, for the Inspector popup.
* `data-inspector-offset-x` - sets horizontal offset, in pixels, for the Inspector popup.
* `data-inspector-offset-y` - sets vertical offset, in pixels, for the Inspector popup.
* `data-inspector-placement` - sets defines placement for the Inspector popup, optional. If omitted, Inspector evaluates a placement automatically. Supported values: top, bottom, left, top.
* `data-inspector-fallback-placement` - sets less preferable placement for the Inspector popup, optional. This value is used if Inspector can't use the placement specified in data-inspector-placement. Supported values: top, bottom, left, top.
* `data-inspector-external-parameters` - if this attribute exists in any parent element of the inspectable element, the external parameters editors will be enabled in Inspector (unless property-specific rules cancel the external editor).
### Dynamic configuration and dynamic items
In case if the `data-inspector-config` attribute is missing in the inspectable element Inspector tries to load its configuration from the server. An important note - there should be a FORM element wrapping inspectable elements in order to use any dynamic features of Inspector.
The AJAX request used for loading the configuration from the server is named `onGetInspectorConfiguration`. The handler should be defined in the back-end controller and should return an array containing the Inspector configuration (in the PHP equivalent of the JSON configuration structure described later in this section), inspector title and description. Example of a server-side AJAX dynamic configuration request handler:
```php
public function onGetInspectorConfiguration()
{
// Load and use some values from the posted form
//
$someValue = Request::input('someValue');
// ... do some processing ...
return [
'configuration' => [
'properties' => [list of properties],
'title' => 'Inspector title',
'description' => 'Inspector description'
]
];
}
```
Some Inspector editors - (drop-down, set, autocomplete) support static and dynamic options. Dynamic options are requested from the server, rather than being defined in the configuration JSON string. For using this feature, the inspectable element must have the `data-inspector-class` attribute defined. The attribute value should contain a name of a PHP class corresponding to the inspectable element.
The server-side controller should use the `Backend\Traits\InspectableContainer` trait in order to provide the dynamic options loading. The inspectable PHP class (specified with `data-inspector-class`) must either have a method `get[Property]Options()`, where the [Property] part corresponds the name of the dynamic property, or `getPropertyOptions($propertyName)` method that is more universal and accepts the property name as a parameter. The methods should return the `options` array containing associative arrays with keys `option` and `value`. Example:
```php
public function getContextOptions()
{
$optionsArray = [];
$optionsArray[] = ['value' => 'create', 'title' => 'Create'];
$optionsArray[] = ['value' => 'update', 'title' => 'Update'];
$optionsArray[] = ['value' => 'delete', 'title' => 'Delete'];
return [
'options' => $optionsArray
];
}
```
### Container and popups
By default Inspector is displayed in a popup, but there's an option to display it right on the page, in a container element. To enable this option, all inspectable elements should be wrapped into another element with `data-inspector-container` attribute. The attribute value should be a CSS selector pointing to an element inside the wrapper. Example:
```html
```
The inner element will act as host element for Inspector when an inspectable element is clicked. The element should have the `inspector-container` class and can be optionally marked with `data-inspector-scrollable` attribute to make the Inspector scrollable. For the scrolling feature, the container element should have height defined explicitly.
When the container is used, Inspector is still displayed in a popup by default, but users can click an icon in the Inspector header to move it to the container.
## Data schema configuration
Inspector configuration, defined with `data-inspector-config` attribute or loaded from the server, should be an array containing a list of property definition. All examples in this section use JSON format. Below is an example of a configuration for two properties:
```json
[
{
"property": "firstName",
"title": "First name",
"type": "string"
},
{
"property": "lastName",
"title": "Last name",
"type": "string"
}
]
```
This configuration creates two text fields with titles "First name" and "Last name". When the data is saved back to the inspectable element (to the `data-inspector-values` hidden input element), it would have the following format:
```json
{"firstName":"John", "lastName":"Smith"}
```
Each property should have attributes `property`, `title` and `type`. The `type` attribute defines a type of an editor that should be created for the property. The supported editors are described further.
Other attributes supported by all (or most of the) property types are:
* `description` - description string, which is available in a tooltip displayed when a user overs the 'i' icon in the property editor.
* `group` - allows to group multiple properties. The attribute should contain a group name. Groups could be collapsed by users, making the Inspector interface less cluttered.
* `showExternalParam` - enables the inspector parameter editor for the property. External parameters are currently used only by the CMS. Note that some property types do not support external property editors. See also `data-inspector-external-parameters` attribute described above.
* `placeholder` - text to display in the editor if property value is empty.
* `validation` - validation configuration. See the complete validation description below.
* `default` - default property value. The property value format depends on the property type - for the `string` type it's an array, for `stringList` type it's an array of strings. See more details below.
All other configuration properties are specific for different property types.
### String editor
String editor allows entering a single line of a text and represented with a simple input text field. The editor doesn't have any specific parameters. The optional `default` parameter for the editor should contain a string.
```json
{
"property": "firstName",
"title": "First name",
"type": "string",
"default": "John"
}
```
The editor generates string values:
```json
{"firstName":"Sam"}
```
### Text editor
Text editor allows entering multi-line long text values in a popup window. The editor doesn't have any specific parameters. The optional `default` parameter for the editor should contain a string.
```json
{
"property": "description",
"title": "Description",
"type": "text",
"default": "This is a default description"
}
```
The editor generates string values:
```json
{"description":"This is a description"}
```
### String list editor
Allows users to enter lists of strings. The editor opens in a popup window and displays a text area. Each line of text represents an element in the result array. The optional `default` parameter should contain an array of strings. Example:
```json
{
"property": "items",
"title": "Items"
"type": "stringList",
"default": ["String 1", "String 2"]
}
```
A value generated by the editor is an array of strings, for example:
```json
{"items":["String 1","String 2","String 3"]}
```
### Autocomplete editor
This editor works like the `string` editor, but includes the autocomplete feature. Autocompletion options can be specified statically, with the `items` parameter or loaded dynamically. Example with static options:
```json
{
"property": "condition",
"title": "Condition"
"type": "autocomplete",
"items": {"start": "Start", "end": "End"}
}
```
The items are specified as a key-value object. The `items` parameter is optional, if it's not provided, the items will be loaded from the server - see [Dynamic configuration and dynamic items](#dynamic-configuration-and-dynamic-items) section above.
Values generated by the editor are strings. Example:
```json
{"condition":"start"}
```
Fields of this type do not support external property editors.
### Checkbox editor
Properties of this type are represented with a checkbox in the Inspector UI. This property doesn't have any special parameters. The `default` parameter, if specified, should contain a Boolean value or string values "true", "false", "1", "0". Example:
```json
{
"property": "enabled",
"title": "Enabled",
"type": "checkbox",
"default": true
}
```
Values generated by the editor are 0 (unchecked) or 1 (checked). Example:
```json
{"enabled":1}
```
### Dropdown editor
Displays a drop-down list. Options for the drop-down list can be specified statically with the `options` attribute or loaded from the server dynamically. Example:
```json
{
"property": "action",
"title": "Action",
"type": "dropdown",
"options": {
"show": "Show",
"hide": "Hide",
"enable": "Enable",
"disable": "Disable",
"empty": "Empty"
}
}
```
The `options` attribute should be a key-value object. If the attribute is not specified, Inspector will try to load options from the server - see [Dynamic configuration and dynamic items](#dynamic-configuration-and-dynamic-items) section above.
The editor generates a string value corresponding to the selected option, for example:
```json
{"action":"hide"}
```
### Dictionary editor
Dictionary editor allows to create key-value pairs with a simple user interface consisting of a table with two columns. The `default` parameter, if specified, should contain a key-value object. Example:
```json
{
"property": "options",
"title": "Options",
"type": "dictionary",
"default": {"option1": "Option 1"}
}
```
The editor generates an object value, for example:
```json
{"options":{"option1":"Option 1","option2":"Option 2"}}
```
The dictionary editor supports validation for the entire set (`required` and `length` validators) and for keys and values separately. See the [validation description](#defining-the-validation-rules) further in this document. The `validationKey` and `validationValue` define validation for keys and values, for example:
```json
{
"property": "options",
"title": "Options",
"type": "dictionary",
"validation": {
"required": {
"message": "Please create options"
},
"length": {
"min": {
"value": 2,
"message": "Create at least two options."
}
}
},
"validationKey": {
"regex": {
"pattern": "^[a-z]+$",
"message": "Keys can contain only lowercase Latin letters"
}
},
"validationValue": {
"regex": {
"pattern": "^[a-zA-Z0-9]+$",
"message": "Values can contain only Latin letters and digits"
}
}
}
```
### Object editor
Allows to define an object with specific properties editable by users. Object properties are specified with the `properties` attribute. The value of the attribute is an array, which has exactly the same structure as the Inspector properties array.
```json
{
"property": "address",
"title": "Address",
"type": "object",
"properties": [
{
"property": "streetAddress",
"title": "Street address",
"type": "string"
},
{
"property": "city",
"title": "City",
"type": "string"
},
{
"property": "country",
"title": "Country",
"type": "dropdown",
"options": {"us": "US", "ca": "Canada"}
}
]
}
```
The example above creates an object with three properties. Two of them are displayed as text fields, and the third as a drop-down.
Object editor values are objects. Example:
```json
{
"address": {
"streetAddress":"321-210 Second ave",
"city":"Springfield",
"country":"us"
}
}
```
The object properties can be of any type supported by Inspector, including other objects.
There's a way to exclude an object from Inspector values completely, if one of the object fields is empty. The field is identified with `ignoreIfPropertyEmpty` parameter. For example:
```json
{
"property": "address",
"title": "Address",
"type": "object",
"ignoreIfPropertyEmpty": "title",
"properties": [
{
"property": "streetAddress",
"title": "Street address",
"type": "string"
},
{
"property": "city",
"title": "City",
"type": "string"
}
]
}
```
In the example above, if the street address is not specified, the object ("address") will be completely removed from the Inspector output. If there are any validation rules defined on other object properties and the required property is empty, those rules will be ignored.
A `default` value for the editor, if specified, should be an object with the same properties as defined in the `properties` configuration parameter.
Object editors do not support the external property editor feature.
### Object list editor
The object list editor allows users to create multiple objects with a pre-defined structure. For example, it could be used for creating a list of person, where each person has a name and address.
The properties of objects that can be created with the editor are defined with `itemProperties` parameter. The parameter should contain an array of properties, similar to Inspector configuration array. Another required parameter is `titleProperty`, which identifies a property that should be used as a title in Inspector UI. Example configuration:
```json
{
"property": "people",
"title": "People",
"type": "objectList",
"titleProperty": "fullName",
"itemProperties": [
{
"property": "fullName",
"title": "Full name",
"type": "string"
},
{
"property": "address",
"title": "Address",
"type": "string"
}
]
}
```
The array of properties defined with `itemProperties` supports all property types.
The Object List editor type doesn't support default values.
By default the value created by the editor of this type is a non-associative array:
```json
{
"people":[
{"fullName":"John Smith","address":"Palo Alto"},
{"fullName":"Bart Simpson","address":"Springfield"}
]
}
```
If the result value should be an associative array (object), use the `keyProperty` configuration option. The option value should refer to a property that should be used as a key. The key property can use only the string or drop-down editors, its value should be unique and cannot be empty. Example:
```json
{
"property": "people",
"title": "People",
"type": "objectList",
"titleProperty": "fullName",
"keyProperty": "login",
"itemProperties": [
{
"property": "fullName",
"title": "Full name",
"type": "string"
},
{
"property": "login",
"title": "Login",
"type": "string"
},
{
"property": "address",
"title": "Address",
"type": "string"
}
]
}
```
The `login` property in the example above will be used as a key in the result value:
```json
{
"people":{
"john":{"fullName":"John Smith","address":"Palo Alto"},
"bart":{"fullName":"Bart Simpson","address":"Springfield"}
}
}
```
### Set editor
The set editor allows users to select multiple predefined options with checkboxes. Set items can be specified statically with the configuration, using the `items` parameter, or loaded dynamically. Example with static items definition:
```json
{
"property": "context",
"title": "Context",
"type": "set",
"items": {
"create": "Create",
"update": "Update",
"preview": "Preview"
},
"default": ["create", "update"]
}
```
The `items` attribute should be a key-value object. If the attribute is not specified, Inspector will try to load options from the server - see [Dynamic configuration and dynamic items](#dynamic-configuration-and-dynamic-items) section above.
The `default` parameter, if specified, should be an array listing item keys selected by default.
Set editors do not support the external property editor feature.
## Defining the validation rules
Inspector support several validation rules that can be applied to properties. Validation rules can be applied to top-level properties as well as to internal property definitions of object and object list editors. There are two ways to define validation rules - the legacy syntax and the new syntax.
The legacy syntax is supported for the backwards compatibility with existing CMS components definitions. This syntax will always be supported, but it's limited, and cannot be mixed with the new syntax. Example of the legacy syntax:
```json
{
"property": "name",
"title": "Name",
"type": "string",
"required": true,
"validationPattern": "^[a-zA-Z]+$"
"validationMessage": "The Name field is required and can contain only Latin letters.",
}
```
The legacy syntax supports only two validation rules - required and regular expression. The new syntax is much more flexible and extendable:
```json
{
"property": "name",
"title": "Name",
"type": "string",
"validation": {
"required": {
"message": "The Name field is required"
},
"regex": {
"message": "The Name field can contain only Latin letters.",
"pattern": "^[a-zA-Z]+$"
}
}
}
```
The key value in the `validation` object refers to a validator (see below). Validators are configured with objects, which properties depend on a validator. One property - `message` is common for all validators.
### required validator
Checks if a value is not empty. The validator can be used with any editor, including complex editors (sets, dictionaries, object lists, etc.). Example:
```json
{
"property": "name",
"title": "Name",
"type": "string",
"validation": {
"required": {
"message": "The Name field is required"
}
}
}
```
### regex validator
Validates string values with a regular expression. The validator can be use only with string-typed editors. Example:
```json
{
"property": "name",
"title": "Name",
"type": "string",
"validation": {
"regex": {
"message": "The Name field can contain only Latin letters",
"pattern": "^[a-z]+$",
"modifiers": "i"
}
}
}
```
The regular expression is specified with the required `pattern` parameter. The `modifiers` parameter is optional and can be used for setting regular expression modifiers.
### integer validator
Checks if the value is integer and can optionally validate if the value is within a specific interval. The validator can be used only with string-typed editors. Example:
```json
{
"property": "numOfColumns",
"title": "Number of Columns",
"type": "string",
"validation": {
"integer": {
"message": "The Number of Columns field should contain an integer value",
"allowNegative": true,
"min": {
"value": -10,
"message": "The number of columns should not be less than -10."
},
"max": {
"value": 10,
"message": "The number of columns should not be greater than 10."
}
}
}
}
```
Supported parameters:
* `allowNegative` - optional, determines if negative values are allowed. By default negative values are not allowed.
* `min` - optional object, defines the minimum allowed value and error message. Object fields:
* `value` - defines the minimum value.
* `message` - optional, defines the error message.
* `max` - optional object, defines the maximum allowed value and error message. Object fields:
* `value` - defines the maximum value.
* `message` - optional, defines the error message.
### float validator
Checks if the value is a floating point number. The parameters for this validator match the parameters of the **integer** validator described above. Example:
```json
{
"property": "amount",
"title": "Amount",
"type": "string",
"validation": {
"float": {
"message": "The Amount field should contain a positive floating point value."
}
}
}
```
Valid floating point number formats:
* 10
* 10.302
* -10 (if `allowNegative` is `true`)
* -10.84 (if `allowNegative` is `true`)
### length validator
Checks if a string, array or object is not shorter or longer than specified values. This validator can work with the string, text, set, string list, dictionary and object list editors. In multiple-value editors (set, string list, dictionary and object list) it validates the number of items created in the editor.
> **Note**: the `length` validator doesn't validate empty values. For example, if it's applied to a set editor, and the set is empty, the validation will pass regardless of the `min` and `max` parameter values. Use the `required` validator together with the `length` validator to make sure that the value is not empty before the length validation is applied.
```json
{
"property": "name",
"title": "Name",
"type": "string",
"validation": {
"length": {
"min": {
"value": 2,
"message": "The name should not be shorter than two letters."
},
"max": {
"value": 10,
"message": "name should not be longer than 10 letters."
}
}
}
}
```
Supported parameters:
* `min` - optional object, defines the minimum allowed length and error message. Object fields:
* `value` - defines the minimum value.
* `message` - optional, defines the error message.
* `max` - optional object, defines the maximum allowed length and error message. Object fields:
* `value` - defines the maximum value.
* `message` - optional, defines the error message.
## Inspector events
Inspector triggers several events on the inspectable elements.
### change
The `change` event is triggered after Inspector applies updated values to the inspectable element. The event is triggered only if the user has changed values in the Inspector UI.
### showing.oc.inspector
The `showing.oc.inspector` event is triggered before Inspector is displayed. The event handler can optionally stop the process with calling `ev.isDefaultPrevented()`. Example - prevent Inspector showing:
```js
$(document).on('showing.oc.inspector', 'div[data-inspectable]', function(ev, data){
ev.preventDefault()
})
```
The handler could perform any required processing, even asynchronous, and then call the callback function passed to the handler, to continue showing the Inspector. In this case the handler should call `ev.stopPropagation()` method to stop the default Inspector initialization. Example - continue showing after some processing:
```js
$(document).on('showing.oc.inspector', 'div[data-inspectable]', function(ev, data){
ev.stopPropagation()
// The callback function can be called asynchronously
data.callback()
})
```
### hiding.oc.inspector
The `hiding.oc.inspector` is called before Inspector hiding process starts. The handler can stop the hiding with calling `ev.preventDefault()`. Example:
```js
$(document).on('hiding.oc.inspector', 'div[data-inspectable]', function(ev, data){
if (!confirm('Allow hiding?')) {
ev.preventDefault()
}
})
```
The values entered in Inspector are available through the `values` element of the second handler argument:
```js
$(document).on('hiding.oc.inspector', 'div[data-inspectable]', function(ev, data){
console.log(data.values)
})
```
### hidden.oc.inspector
The `hidden.oc.inspector` is triggered after Inspector is hidden.
================================================
FILE: modules/backend/assets/foundation/controls/inspector/inspector.datainteraction.js
================================================
/*
* Inspector data interaction class.
*
* Provides methods for loading and writing Inspector configuration
* and values form and to inspectable elements.
*/
+function ($) { "use strict";
// CLASS DEFINITION
// ============================
var Base = $.oc.foundation.base,
BaseProto = Base.prototype
var DataInteraction = function(element) {
this.element = element;
Base.call(this);
}
DataInteraction.prototype = Object.create(BaseProto)
DataInteraction.prototype.constructor = Base
DataInteraction.prototype.dispose = function() {
this.element = null
BaseProto.dispose.call(this)
}
DataInteraction.prototype.getElementValuesInput = function() {
return this.element.querySelector('input[data-inspector-values]')
}
DataInteraction.prototype.normalizePropertyCode = function(code, configuration) {
var lowerCaseCode = code.toLowerCase()
for (var index in configuration) {
var propertyInfo = configuration[index]
if (propertyInfo.property.toLowerCase() == lowerCaseCode) {
return propertyInfo.property
}
}
return code
}
DataInteraction.prototype.loadValues = function(configuration) {
var valuesField = this.getElementValuesInput()
if (valuesField) {
var valuesStr = $.trim(valuesField.value)
try {
return valuesStr.length === 0 ? {} : JSON.parse(valuesStr)
}
catch (err) {
throw new Error('Error parsing Inspector field values. ' + err)
}
}
var values = {},
attributes = this.element.attributes
for (var i=0, len = attributes.length; i < len; i++) {
var attribute = attributes[i],
matches = []
if (matches = attribute.name.match(/^data-property-(.*)$/)) {
// Important - values contained in data-property-xxx attributes are
// considered strings and never parsed with JSON. The use of the
// data-property-xxx attributes is very limited - they're only
// used in Pages for creating snippets from partials, where properties
// are created with a table UI widget, which doesn't allow creating
// properties of any complex types.
//
// There is no a technically reliable way to determine when a string
// is a JSON data or a regular string. Users can enter a value
// like [10], which is a proper JSON value, but meant to be a string.
//
// One possible way to resolve it, if to check the property type loaded
// from the configuration and see if the corresponding editor expects
// complex data.
var normalizedPropertyName = normalizePropertyCode(matches[1], configuration)
values[normalizedPropertyName] = attribute.value
}
}
return values
}
DataInteraction.prototype.loadConfiguration = function(onComplete) {
var configurationField = this.element.querySelector('input[data-inspector-config]'),
result = {
configuration: {},
title: null,
description: null
},
$element = $(this.element);
result.title = $element.data('inspector-title');
result.description = $element.data('inspector-description');
if (configurationField) {
result.configuration = this.parseConfiguration(configurationField.value);
onComplete(result, this);
return;
}
var $form = $element.closest('form'),
data = $element.data(),
self = this;
$.oc.stripeLoadIndicator.show();
$form.request($.oc.inspector.helpers.getEventHandler($element, 'onGetInspectorConfiguration'), {
data: data
})
.done(function inspectorConfigurationRequestDoneClosure(data) {
self.configurationRequestDone(data, onComplete, result);
})
.always(function() {
$.oc.stripeLoadIndicator.hide();
});
}
//
// Internal methods
//
DataInteraction.prototype.parseConfiguration = function(configuration) {
if (!Array.isArray(configuration) && !$.isPlainObject(configuration)) {
if ($.trim(configuration) === 0) {
return {};
}
try {
return JSON.parse(configuration);
}
catch(err) {
throw new Error('Error parsing Inspector configuration. ' + err);
}
}
else {
return configuration;
}
}
DataInteraction.prototype.configurationRequestDone = function(data, onComplete, result) {
result.configuration = this.parseConfiguration(data.configuration.properties);
if (data.configuration.title !== undefined) {
result.title = data.configuration.title;
}
if (data.configuration.description !== undefined) {
result.description = data.configuration.description;
}
onComplete(result, this);
}
$.oc.inspector.dataInteraction = DataInteraction;
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/controls/inspector/inspector.editor.autocomplete.js
================================================
/*
* Inspector autocomplete editor class.
*
* Depends on october.autocomplete.js
*/
+function ($) { "use strict";
var Base = $.oc.inspector.propertyEditors.string,
BaseProto = Base.prototype;
var AutocompleteEditor = function(inspector, propertyDefinition, containerCell, group) {
this.autoUpdateTimeout = null;
Base.call(this, inspector, propertyDefinition, containerCell, group);
}
AutocompleteEditor.prototype = Object.create(BaseProto);
AutocompleteEditor.prototype.constructor = Base;
AutocompleteEditor.prototype.dispose = function() {
this.clearAutoUpdateTimeout();
this.removeAutocomplete();
BaseProto.dispose.call(this);
}
AutocompleteEditor.prototype.build = function() {
var container = document.createElement('div'),
editor = document.createElement('input'),
placeholder = this.propertyDefinition.placeholder !== undefined ? this.propertyDefinition.placeholder : '',
value = this.inspector.getPropertyValue(this.propertyDefinition.property);
editor.setAttribute('type', 'text');
editor.setAttribute('class', 'string-editor');
editor.setAttribute('placeholder', placeholder);
container.setAttribute('class', 'autocomplete-container');
if (value === undefined) {
value = this.propertyDefinition.default;
}
if (value === undefined) {
value = '';
}
editor.value = value;
$.oc.foundation.element.addClass(this.containerCell, 'text autocomplete');
container.appendChild(editor);
this.containerCell.appendChild(container);
if (this.propertyDefinition.items !== undefined) {
this.buildAutoComplete(this.propertyDefinition.items);
}
else {
this.loadDynamicItems();
}
}
AutocompleteEditor.prototype.buildAutoComplete = function(items) {
var input = this.getInput()
if (items === undefined) {
items = [];
}
var $input = $(input),
autocomplete = $input.data('autocomplete')
if (!autocomplete) {
$input.autocomplete({
source: this.prepareItems(items),
matchWidth: true
});
}
else {
autocomplete.source = this.prepareItems(items);
}
}
AutocompleteEditor.prototype.removeAutocomplete = function() {
var input = this.getInput();
$(input).autocomplete('destroy');
}
AutocompleteEditor.prototype.prepareItems = function(items) {
var result = {};
if (Array.isArray(items)) {
for (var i = 0, len = items.length; i < len; i++) {
result[items[i]] = items[i];
}
}
else {
result = items;
}
return result;
}
AutocompleteEditor.prototype.supportsExternalParameterEditor = function() {
return false;
}
AutocompleteEditor.prototype.getContainer = function() {
return this.getInput().parentNode;
}
AutocompleteEditor.prototype.registerHandlers = function() {
BaseProto.registerHandlers.call(this);
$(this.getInput()).on('change', this.proxy(this.onInputKeyUp));
}
AutocompleteEditor.prototype.unregisterHandlers = function() {
BaseProto.unregisterHandlers.call(this);
$(this.getInput()).off('change', this.proxy(this.onInputKeyUp));
}
AutocompleteEditor.prototype.saveDependencyValues = function() {
this.prevDependencyValues = this.getDependencyValues();
}
AutocompleteEditor.prototype.getDependencyValues = function() {
var result = '';
for (var i = 0, len = this.propertyDefinition.depends.length; i < len; i++) {
var property = this.propertyDefinition.depends[i],
value = this.inspector.getPropertyValue(property);
if (value === undefined) {
value = '';
}
result += property + ':' + value + '-';
}
return result;
}
AutocompleteEditor.prototype.onInspectorPropertyChanged = function(property) {
if (!this.propertyDefinition.depends || this.propertyDefinition.depends.indexOf(property) === -1) {
return;
}
this.clearAutoUpdateTimeout();
if (this.prevDependencyValues === undefined || this.prevDependencyValues != dependencyValues) {
this.autoUpdateTimeout = setTimeout(this.proxy(this.loadDynamicItems), 200);
}
}
AutocompleteEditor.prototype.clearAutoUpdateTimeout = function() {
if (this.autoUpdateTimeout !== null) {
clearTimeout(this.autoUpdateTimeout);
this.autoUpdateTimeout = null;
}
}
//
// Dynamic items
//
AutocompleteEditor.prototype.showLoadingIndicator = function() {
$(this.getContainer()).loadIndicator();
}
AutocompleteEditor.prototype.hideLoadingIndicator = function() {
if (this.isDisposed()) {
return;
}
var $container = $(this.getContainer());
$container.loadIndicator('hide');
$container.loadIndicator('destroy');
$container.removeClass('loading-indicator-container');
}
AutocompleteEditor.prototype.loadDynamicItems = function() {
if (this.isDisposed()) {
return;
}
this.clearAutoUpdateTimeout();
var container = this.getContainer(),
data = this.getRootSurface().getValues(),
$form = $(container).closest('form');
$.oc.foundation.element.addClass(container, 'loading-indicator-container size-small');
this.showLoadingIndicator();
if (this.triggerGetItems(data) === false) {
return;
}
data['inspectorProperty'] = this.getPropertyPath();
data['inspectorClassName'] = this.inspector.options.inspectorClass;
$form.request(this.inspector.getEventHandler('onInspectableGetOptions'), {
data: data,
progressBar: false
})
.done(this.proxy(this.itemsRequestDone))
.always(this.proxy(this.hideLoadingIndicator));
}
AutocompleteEditor.prototype.triggerGetItems = function(values) {
var $inspectable = this.getInspectableElement();
if (!$inspectable) {
return true;
}
var itemsEvent = $.Event('autocompleteitems.oc.inspector');
$inspectable.trigger(itemsEvent, [{
values: values,
callback: this.proxy(this.itemsRequestDone),
property: this.inspector.getPropertyPath(this.propertyDefinition.property),
propertyDefinition: this.propertyDefinition
}]);
if (itemsEvent.isDefaultPrevented()) {
return false;
}
return true;
}
AutocompleteEditor.prototype.itemsRequestDone = function(data) {
if (this.isDisposed()) {
// Handle the case when the asynchronous request finishes after
// the editor is disposed
return
}
this.hideLoadingIndicator();
var loadedItems = {};
if (data.options) {
for (var i = data.options.length-1; i >= 0; i--) {
loadedItems[data.options[i].value] = data.options[i].title;
}
}
this.buildAutoComplete(loadedItems);
}
$.oc.inspector.propertyEditors.autocomplete = AutocompleteEditor;
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/controls/inspector/inspector.editor.base.js
================================================
/*
* Inspector editor base class.
*/
+function ($) { "use strict";
// NAMESPACES
// ============================
if ($.oc === undefined) {
$.oc = {}
}
if ($.oc.inspector === undefined) {
$.oc.inspector = {}
}
if ($.oc.inspector.propertyEditors === undefined) {
$.oc.inspector.propertyEditors = {}
}
// CLASS DEFINITION
// ============================
var Base = $.oc.foundation.base,
BaseProto = Base.prototype
var BaseEditor = function(inspector, propertyDefinition, containerCell, group) {
this.inspector = inspector;
this.propertyDefinition = propertyDefinition;
this.containerCell = containerCell;
this.containerRow = containerCell.parentNode;
this.parentGroup = group;
// Group created by a grouped editor, for example by the set editor
this.group = null;
this.childInspector = null;
this.validationSet = null;
this.disposed = false;
Base.call(this);
this.init();
}
BaseEditor.prototype = Object.create(BaseProto)
BaseEditor.prototype.constructor = Base
BaseEditor.prototype.dispose = function() {
// After this point editors can't rely on any DOM references
this.disposed = true;
this.disposeValidation();
if (this.childInspector) {
this.childInspector.dispose();
}
this.inspector = null;
this.propertyDefinition = null;
this.containerCell = null;
this.containerRow = null;
this.childInspector = null;
this.parentGroup = null;
this.group = null;
this.validationSet = null;
BaseProto.dispose.call(this)
}
BaseEditor.prototype.init = function() {
this.build()
this.registerHandlers()
this.initValidation()
}
BaseEditor.prototype.build = function() {
return null
}
BaseEditor.prototype.isDisposed = function() {
return this.disposed
}
BaseEditor.prototype.registerHandlers = function() {
}
BaseEditor.prototype.onInspectorPropertyChanged = function(property, value) {
}
BaseEditor.prototype.notifyChildSurfacesPropertyChanged = function(property, value) {
if (!this.hasChildSurface()) {
return;
}
this.childInspector.notifyEditorsPropertyChanged(property, value)
}
BaseEditor.prototype.focus = function() {
}
BaseEditor.prototype.hasChildSurface = function() {
return this.childInspector !== null
}
BaseEditor.prototype.getRootSurface = function() {
return this.inspector.getRootSurface()
}
BaseEditor.prototype.getPropertyPath = function() {
return this.inspector.getPropertyPath(this.propertyDefinition.property)
}
/**
* Updates displayed value in the editor UI. The value is already set
* in the Inspector and should be loaded from Inspector.
*/
BaseEditor.prototype.updateDisplayedValue = function(value) {
}
BaseEditor.prototype.getPropertyName = function() {
return this.propertyDefinition.property
}
BaseEditor.prototype.getUndefinedValue = function() {
return this.propertyDefinition.default === undefined ? undefined : this.propertyDefinition.default
}
BaseEditor.prototype.throwError = function(errorMessage) {
throw new Error(errorMessage + ' Property: ' + this.propertyDefinition.property)
}
BaseEditor.prototype.getInspectableElement = function() {
return this.getRootSurface().getInspectableElement()
}
BaseEditor.prototype.isEmptyValue = function(value) {
return value === undefined
|| value === null
|| (typeof value == 'object' && $.isEmptyObject(value) )
|| (typeof value == 'string' && $.trim(value).length === 0)
|| (Object.prototype.toString.call(value) === '[object Array]' && value.length === 0)
}
//
// Validation
//
BaseEditor.prototype.initValidation = function() {
this.validationSet = new $.oc.inspector.validationSet(this.propertyDefinition, this.propertyDefinition.property)
}
BaseEditor.prototype.disposeValidation = function() {
this.validationSet.dispose()
}
BaseEditor.prototype.getValueToValidate = function() {
return this.inspector.getPropertyValue(this.propertyDefinition.property)
}
BaseEditor.prototype.validate = function(silentMode) {
var value = this.getValueToValidate()
if (value === undefined) {
value = this.getUndefinedValue()
}
var validationResult = this.validationSet.validate(value)
if (validationResult !== null) {
if (!silentMode) {
$.oc.flashMsg({text: validationResult, 'class': 'error', 'interval': 5})
}
return false
}
return true
}
BaseEditor.prototype.markInvalid = function() {
$.oc.foundation.element.addClass(this.containerRow, 'invalid');
this.inspector.getGroupManager().markGroupRowInvalid(this.parentGroup, this.inspector.getRootTable());
this.inspector.getRootSurface().expandGroupParents(this.parentGroup);
this.focus();
}
//
// External editor
//
BaseEditor.prototype.supportsExternalParameterEditor = function() {
return true
}
BaseEditor.prototype.onExternalPropertyEditorHidden = function() {
}
//
// Grouping
//
BaseEditor.prototype.isGroupedEditor = function() {
return false
}
BaseEditor.prototype.initControlGroup = function() {
this.group = this.inspector.getGroupManager().createGroup(this.propertyDefinition.property, this.parentGroup)
}
BaseEditor.prototype.createGroupedRow = function(property) {
var row = this.inspector.buildRow(property, this.group),
groupedClass = this.inspector.getGroupManager().isGroupExpanded(this.group) ? 'expanded' : 'collapsed'
this.inspector.applyGroupLevelToRow(row, this.group)
$.oc.foundation.element.addClass(row, 'property')
$.oc.foundation.element.addClass(row, groupedClass)
return row
}
$.oc.inspector.propertyEditors.base = BaseEditor
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/controls/inspector/inspector.editor.checkbox.js
================================================
/*
* Inspector checkbox editor class.
*
* This editor is used in $.oc.inspector.propertyEditors.set class.
* If updates that affect references to this.inspector and propertyDefinition are done,
* the propertyEditors.set class implementation should be reviewed.
*/
+function ($) { "use strict";
var Base = $.oc.inspector.propertyEditors.base,
BaseProto = Base.prototype
var CheckboxEditor = function(inspector, propertyDefinition, containerCell, group) {
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
CheckboxEditor.prototype = Object.create(BaseProto)
CheckboxEditor.prototype.constructor = Base
CheckboxEditor.prototype.dispose = function() {
this.unregisterHandlers()
BaseProto.dispose.call(this)
}
CheckboxEditor.prototype.build = function() {
var editor = document.createElement('input'),
container = document.createElement('div'),
value = this.inspector.getPropertyValue(this.propertyDefinition.property),
label = document.createElement('label'),
isChecked = false,
id = this.inspector.generateSequencedId();
container.setAttribute('tabindex', 0);
container.setAttribute('class', 'form-check');
editor.setAttribute('type', 'checkbox');
editor.setAttribute('value', '1');
editor.setAttribute('placeholder', 'placeholder');
editor.setAttribute('id', id);
editor.setAttribute('class', 'form-check-input');
container.appendChild(editor);
if (value === undefined) {
if (this.propertyDefinition.default !== undefined) {
isChecked = this.normalizeCheckedValue(this.propertyDefinition.default);
}
}
else {
isChecked = this.normalizeCheckedValue(value);
}
editor.checked = isChecked;
this.containerCell.appendChild(container);
}
CheckboxEditor.prototype.normalizeCheckedValue = function(value) {
if (value == '0' || value == 'false') {
return false;
}
return value;
}
CheckboxEditor.prototype.getInput = function() {
return this.containerCell.querySelector('input');
}
CheckboxEditor.prototype.focus = function() {
this.getInput().parentNode.focus({ preventScroll: true });
}
CheckboxEditor.prototype.updateDisplayedValue = function(value) {
this.getInput().checked = this.normalizeCheckedValue(value);
}
CheckboxEditor.prototype.isEmptyValue = function(value) {
if (value === 0 || value === '0' || value === 'false') {
return true;
}
return BaseProto.isEmptyValue.call(this, value);
}
CheckboxEditor.prototype.registerHandlers = function() {
var input = this.getInput()
input.addEventListener('change', this.proxy(this.onInputChange))
}
CheckboxEditor.prototype.unregisterHandlers = function() {
var input = this.getInput()
input.removeEventListener('change', this.proxy(this.onInputChange))
}
CheckboxEditor.prototype.onInputChange = function() {
var isChecked = this.getInput().checked
this.inspector.setPropertyValue(this.propertyDefinition.property, isChecked ? 1 : 0)
}
$.oc.inspector.propertyEditors.checkbox = CheckboxEditor
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/controls/inspector/inspector.editor.dictionary.js
================================================
/*
* Inspector dictionary editor class.
*/
+function ($) { "use strict";
var Base = $.oc.inspector.propertyEditors.popupBase,
BaseProto = Base.prototype
var DictionaryEditor = function(inspector, propertyDefinition, containerCell, group) {
this.keyValidationSet = null
this.valueValidationSet = null
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
DictionaryEditor.prototype = Object.create(BaseProto)
DictionaryEditor.prototype.constructor = Base
DictionaryEditor.prototype.dispose = function() {
this.disposeValidators()
this.keyValidationSet = null
this.valueValidationSet = null
BaseProto.dispose.call(this)
}
DictionaryEditor.prototype.init = function() {
this.initValidators()
BaseProto.init.call(this)
}
DictionaryEditor.prototype.supportsExternalParameterEditor = function() {
return false
}
//
// Popup editor methods
//
DictionaryEditor.prototype.setLinkText = function(link, value) {
var value = value !== undefined ? value
: this.inspector.getPropertyValue(this.propertyDefinition.property)
if (value === undefined) {
value = this.propertyDefinition.default
}
if (value === undefined || $.isEmptyObject(value)) {
var placeholder = this.propertyDefinition.placeholder
if (placeholder !== undefined) {
$.oc.foundation.element.addClass(link, 'cell-placeholder')
link.textContent = placeholder
}
else {
link.textContent = 'Items: 0'
}
}
else {
if (typeof value !== 'object') {
this.throwError('Object list value should be an object.')
}
var itemCount = this.getValueKeys(value).length
$.oc.foundation.element.removeClass(link, 'cell-placeholder')
link.textContent = 'Items: ' + itemCount
}
}
DictionaryEditor.prototype.getPopupContent = function() {
return ''
}
DictionaryEditor.prototype.configurePopup = function(popup) {
this.buildItemsTable(popup.get(0))
this.focusFirstInput()
}
DictionaryEditor.prototype.handleSubmit = function($form) {
return this.applyValues()
}
//
// Building and row management
//
DictionaryEditor.prototype.buildItemsTable = function(popup) {
var table = popup.querySelector('table.inspector-dictionary-table'),
tbody = document.createElement('tbody'),
items = this.inspector.getPropertyValue(this.propertyDefinition.property),
titleProperty = this.propertyDefinition.titleProperty
if (items === undefined) {
items = this.propertyDefinition.default
}
if (items === undefined || this.getValueKeys(items).length === 0) {
var row = this.buildEmptyRow()
tbody.appendChild(row)
}
else {
for (var key in items) {
var row = this.buildTableRow(key, items[key])
tbody.appendChild(row)
}
}
table.appendChild(tbody)
this.updateScrollpads()
}
DictionaryEditor.prototype.buildTableRow = function(key, value) {
var row = document.createElement('tr'),
keyCell = document.createElement('td'),
valueCell = document.createElement('td')
this.createInput(keyCell, key)
this.createInput(valueCell, value)
row.appendChild(keyCell)
row.appendChild(valueCell)
return row
}
DictionaryEditor.prototype.buildEmptyRow = function() {
return this.buildTableRow(null, null)
}
DictionaryEditor.prototype.createInput = function(container, value) {
var input = document.createElement('input'),
controlContainer = document.createElement('div')
input.setAttribute('type', 'text')
input.setAttribute('class', 'form-control')
input.value = value
controlContainer.appendChild(input)
container.appendChild(controlContainer)
}
DictionaryEditor.prototype.setActiveCell = function(input) {
var activeCells = this.popup.querySelectorAll('td.active')
for (var i = activeCells.length-1; i >= 0; i--) {
$.oc.foundation.element.removeClass(activeCells[i], 'active')
}
var activeCell = input.parentNode.parentNode // input / div / td
$.oc.foundation.element.addClass(activeCell, 'active')
}
DictionaryEditor.prototype.createItem = function() {
var activeRow = this.getActiveRow(),
newRow = this.buildEmptyRow(),
tbody = this.getTableBody(),
nextSibling = activeRow ? activeRow.nextElementSibling : null
tbody.insertBefore(newRow, nextSibling)
this.focusAndMakeActive(newRow.querySelector('input'))
this.updateScrollpads()
}
DictionaryEditor.prototype.deleteItem = function() {
var activeRow = this.getActiveRow(),
tbody = this.getTableBody()
if (!activeRow) {
return
}
var nextRow = activeRow.nextElementSibling,
prevRow = activeRow.previousElementSibling
tbody.removeChild(activeRow)
var newSelectedRow = nextRow ? nextRow : prevRow
if (!newSelectedRow) {
newSelectedRow = this.buildEmptyRow()
tbody.appendChild(newSelectedRow)
}
this.focusAndMakeActive(newSelectedRow .querySelector('input'))
this.updateScrollpads()
}
DictionaryEditor.prototype.applyValues = function() {
var tbody = this.getTableBody(),
dataRows = tbody.querySelectorAll('tr'),
link = this.getLink(),
result = {}
for (var i = 0, len = dataRows.length; i < len; i++) {
var dataRow = dataRows[i],
keyInput = this.getRowInputByIndex(dataRow, 0),
valueInput = this.getRowInputByIndex(dataRow, 1),
key = $.trim(keyInput.value),
value = $.trim(valueInput.value)
if (key.length == 0 && value.length == 0) {
continue
}
if (key.length == 0) {
$.oc.flashMsg({text: 'The key cannot be empty.', 'class': 'error', 'interval': 3})
this.focusAndMakeActive(keyInput)
return false
}
if (value.length == 0) {
$.oc.flashMsg({text: 'The value cannot be empty.', 'class': 'error', 'interval': 3})
this.focusAndMakeActive(valueInput)
return false
}
if (result[key] !== undefined) {
$.oc.flashMsg({text: 'Keys should be unique.', 'class': 'error', 'interval': 3})
this.focusAndMakeActive(keyInput)
return false
}
var validationResult = this.keyValidationSet.validate(key)
if (validationResult !== null) {
$.oc.flashMsg({text: validationResult, 'class': 'error', 'interval': 5})
return false
}
validationResult = this.valueValidationSet.validate(value)
if (validationResult !== null) {
$.oc.flashMsg({text: validationResult, 'class': 'error', 'interval': 5})
return false
}
result[key] = value
}
this.inspector.setPropertyValue(this.propertyDefinition.property, result)
this.setLinkText(link, result)
}
//
// Helpers
//
DictionaryEditor.prototype.getValueKeys = function(value) {
var result = []
for (var key in value) {
result.push(key)
}
return result
}
DictionaryEditor.prototype.getActiveRow = function() {
var activeCell = this.popup.querySelector('td.active')
if (!activeCell) {
return null
}
return activeCell.parentNode
}
DictionaryEditor.prototype.getTableBody = function() {
return this.popup.querySelector('table.inspector-dictionary-table tbody')
}
DictionaryEditor.prototype.updateScrollpads = function() {
$('.control-scrollpad', this.popup).scrollpad('update')
}
DictionaryEditor.prototype.focusFirstInput = function() {
var input = this.popup.querySelector('td input')
if (input) {
input.focus()
this.setActiveCell(input)
}
}
DictionaryEditor.prototype.getEditorCell = function(cell) {
return cell.parentNode.parentNode // cell / div / td
}
DictionaryEditor.prototype.getEditorRow = function(cell) {
return cell.parentNode.parentNode.parentNode // cell / div / td / tr
}
DictionaryEditor.prototype.focusAndMakeActive = function(input) {
input.focus()
this.setActiveCell(input)
}
DictionaryEditor.prototype.getRowInputByIndex = function(row, index) {
return row.cells[index].querySelector('input')
}
//
// Navigation
//
DictionaryEditor.prototype.navigateDown = function(ev) {
var cell = this.getEditorCell(ev.currentTarget),
row = this.getEditorRow(ev.currentTarget),
nextRow = row.nextElementSibling
if (!nextRow) {
return
}
var newActiveEditor = nextRow.cells[cell.cellIndex].querySelector('input')
this.focusAndMakeActive(newActiveEditor)
}
DictionaryEditor.prototype.navigateUp = function(ev) {
var cell = this.getEditorCell(ev.currentTarget),
row = this.getEditorRow(ev.currentTarget),
prevRow = row.previousElementSibling
if (!prevRow) {
return
}
var newActiveEditor = prevRow.cells[cell.cellIndex].querySelector('input')
this.focusAndMakeActive(newActiveEditor)
}
//
// Validation
//
DictionaryEditor.prototype.initValidators = function() {
this.keyValidationSet = new $.oc.inspector.validationSet({
validation: this.propertyDefinition.validationKey
}, this.propertyDefinition.property+'.validationKey')
this.valueValidationSet = new $.oc.inspector.validationSet({
validation: this.propertyDefinition.validationValue
}, this.propertyDefinition.property+'.validationValue')
}
DictionaryEditor.prototype.disposeValidators = function() {
this.keyValidationSet.dispose()
this.valueValidationSet.dispose()
}
//
// Event handlers
//
DictionaryEditor.prototype.onPopupShown = function(ev, link, popup) {
BaseProto.onPopupShown.call(this,ev, link, popup )
popup.on('focus.inspector', 'td input', this.proxy(this.onFocus))
popup.on('keydown.inspector', 'td input', this.proxy(this.onKeyDown))
popup.on('click.inspector', '[data-cmd]', this.proxy(this.onCommand))
}
DictionaryEditor.prototype.onPopupHidden = function(ev, link, popup) {
popup.off('.inspector', 'td input')
popup.off('.inspector', '[data-cmd]', this.proxy(this.onCommand))
BaseProto.onPopupHidden.call(this, ev, link, popup)
}
DictionaryEditor.prototype.onFocus = function(ev) {
this.setActiveCell(ev.currentTarget)
}
DictionaryEditor.prototype.onCommand = function(ev) {
var command = ev.currentTarget.getAttribute('data-cmd')
switch (command) {
case 'create-item' :
this.createItem()
break;
case 'delete-item' :
this.deleteItem()
break;
}
}
DictionaryEditor.prototype.onKeyDown = function(ev) {
if (ev.key === 'ArrowDown') {
return this.navigateDown(ev)
}
else if (ev.key === 'ArrowUp') {
return this.navigateUp(ev)
}
}
$.oc.inspector.propertyEditors.dictionary = DictionaryEditor
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/controls/inspector/inspector.editor.dropdown.js
================================================
/*
* Inspector checkbox dropdown class.
*/
+function ($) { "use strict";
var Base = $.oc.inspector.propertyEditors.base,
BaseProto = Base.prototype
var DropdownEditor = function(inspector, propertyDefinition, containerCell, group) {
this.indicatorContainer = null
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
DropdownEditor.prototype = Object.create(BaseProto)
DropdownEditor.prototype.constructor = Base
DropdownEditor.prototype.init = function() {
this.dynamicOptions = this.propertyDefinition.options ? false : true
this.initialization = false
BaseProto.init.call(this)
}
DropdownEditor.prototype.dispose = function() {
this.unregisterHandlers()
this.destroyCustomSelect()
this.indicatorContainer = null
BaseProto.dispose.call(this)
}
//
// Building
//
DropdownEditor.prototype.build = function() {
var select = document.createElement('select')
$.oc.foundation.element.addClass(this.containerCell, 'dropdown')
$.oc.foundation.element.addClass(select, 'custom-select')
if (!this.dynamicOptions) {
this.loadStaticOptions(select)
}
this.containerCell.appendChild(select)
this.initCustomSelect()
if (this.dynamicOptions) {
this.loadDynamicOptions(true)
}
}
DropdownEditor.prototype.formatSelectOption = function(state) {
if (!state.id)
return state.text; // optgroup
var option = state.element,
iconClass = option.getAttribute('data-icon'),
imageSrc = option.getAttribute('data-image')
if (iconClass) {
return ' ' + state.text
}
if (imageSrc) {
return ' ' + state.text
}
return state.text
}
DropdownEditor.prototype.createOption = function(select, title, value) {
var option = document.createElement('option')
if (title !== null) {
if (!Array.isArray(title)) {
option.textContent = title
}
else {
if (title[1].indexOf('.') !== -1) {
option.setAttribute('data-image', title[1])
}
else {
option.setAttribute('data-icon', title[1])
}
option.textContent = title[0]
}
}
if (value !== null) {
option.value = value
}
select.appendChild(option)
}
DropdownEditor.prototype.createOptions = function(select, options) {
for (var value in options) {
this.createOption(select, options[value], value)
}
}
DropdownEditor.prototype.initCustomSelect = function() {
var select = this.getSelect()
var options = {
dropdownCssClass: 'ocInspectorDropdown'
}
if (this.propertyDefinition.emptyOption !== undefined) {
options.placeholder = this.propertyDefinition.emptyOption
}
if (this.propertyDefinition.placeholder !== undefined) {
options.placeholder = this.propertyDefinition.placeholder
}
options.templateResult = this.formatSelectOption
options.templateSelection = this.formatSelectOption
options.escapeMarkup = function(m) {
return m
}
$(select).select2(options)
if (!('ontouchstart' in window || navigator.maxTouchPoints > 0)) {
this.indicatorContainer = $('.select2-container', this.containerCell)
this.indicatorContainer.addClass('loading-indicator-container size-small')
}
}
DropdownEditor.prototype.createPlaceholder = function(select) {
var placeholder = this.propertyDefinition.placeholder || this.propertyDefinition.emptyOption
var isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0
if (placeholder !== undefined && !isTouchDevice) {
this.createOption(select, null, null)
}
if (placeholder !== undefined && isTouchDevice) {
this.createOption(select, placeholder, null)
}
}
//
// Helpers
//
DropdownEditor.prototype.getSelect = function() {
return this.containerCell.querySelector('select')
}
DropdownEditor.prototype.clearOptions = function(select) {
while (select.firstChild) {
select.removeChild(select.firstChild)
}
}
DropdownEditor.prototype.hasOptionValue = function(select, value) {
var options = select.children
for (var i = 0, len = options.length; i < len; i++) {
if (options[i].value == value) {
return true
}
}
return false
}
DropdownEditor.prototype.normalizeValue = function(value) {
if (!this.propertyDefinition.booleanValues) {
return value
}
var str = String(value)
if (str.length === 0) {
return ''
}
if (str === 'true') {
return true
}
return false
}
//
// Event handlers
//
DropdownEditor.prototype.registerHandlers = function() {
var select = this.getSelect()
$(select).on('change', this.proxy(this.onSelectionChange))
}
DropdownEditor.prototype.onSelectionChange = function() {
var select = this.getSelect()
this.inspector.setPropertyValue(this.propertyDefinition.property, this.normalizeValue(select.value), this.initialization)
}
DropdownEditor.prototype.onInspectorPropertyChanged = function(property) {
if (!this.propertyDefinition.depends || this.propertyDefinition.depends.indexOf(property) === -1) {
return
}
var dependencyValues = this.getDependencyValues()
if (this.prevDependencyValues === undefined || this.prevDependencyValues != dependencyValues) {
this.loadDynamicOptions()
}
}
DropdownEditor.prototype.onExternalPropertyEditorHidden = function() {
if (this.dynamicOptions) {
this.loadDynamicOptions(false)
}
}
//
// Editor API methods
//
DropdownEditor.prototype.updateDisplayedValue = function(value) {
var select = this.getSelect()
select.value = value
}
DropdownEditor.prototype.getUndefinedValue = function() {
// Return default value if the default value is defined
if (this.propertyDefinition.default !== undefined) {
return this.propertyDefinition.default
}
// Return undefined if there's a placeholder value
if (this.propertyDefinition.placeholder !== undefined) {
return undefined
}
// Otherwise - return the first value in the list
var select = this.getSelect()
if (select) {
return this.normalizeValue(select.value)
}
return undefined
}
DropdownEditor.prototype.isEmptyValue = function(value) {
if (this.propertyDefinition.booleanValues) {
if (value === '') {
return true
}
return false
}
return BaseProto.isEmptyValue.call(this, value)
}
//
// Disposing
//
DropdownEditor.prototype.destroyCustomSelect = function() {
var $select = $(this.getSelect())
if ($select.data('select2') != null) {
$select.select2('destroy')
}
}
DropdownEditor.prototype.unregisterHandlers = function() {
var select = this.getSelect()
$(select).off('change', this.proxy(this.onSelectionChange))
}
//
// Static options
//
DropdownEditor.prototype.loadStaticOptions = function(select) {
var value = this.inspector.getPropertyValue(this.propertyDefinition.property)
this.createPlaceholder(select)
this.createOptions(select, this.propertyDefinition.options)
if (value === undefined) {
value = this.propertyDefinition.default
}
select.value = value
}
//
// Dynamic options
//
DropdownEditor.prototype.loadDynamicOptions = function(initialization) {
var currentValue = this.inspector.getPropertyValue(this.propertyDefinition.property),
data = this.getRootSurface().getValues(),
self = this,
$form = $(this.getSelect()).closest('form'),
dependents = this.inspector.findDependentProperties(this.propertyDefinition.property)
if (currentValue === undefined) {
currentValue = this.propertyDefinition.default
}
var callback = function dropdownOptionsRequestDoneClosure(data) {
self.hideLoadingIndicator();
self.optionsRequestDone(data, currentValue, true);
if (dependents.length > 0 && self.inspector) {
for (var i in dependents) {
var editor = self.inspector.findPropertyEditor(dependents[i])
if (editor && typeof editor.onInspectorPropertyChanged === 'function') {
editor.onInspectorPropertyChanged(self.propertyDefinition.property)
}
}
}
}
if (this.propertyDefinition.depends) {
this.saveDependencyValues();
}
data['inspectorProperty'] = this.getPropertyPath();
data['inspectorClassName'] = this.inspector.options.inspectorClass;
this.showLoadingIndicator();
if (this.triggerGetOptions(data, callback) === false) {
return;
}
$form.request(this.inspector.getEventHandler('onInspectableGetOptions'), {
data: data,
progressBar: false
})
.done(callback).always(this.proxy(this.hideLoadingIndicator));
}
DropdownEditor.prototype.triggerGetOptions = function(values, callback) {
var $inspectable = this.getInspectableElement()
if (!$inspectable) {
return true
}
var optionsEvent = $.Event('dropdownoptions.oc.inspector')
$inspectable.trigger(optionsEvent, [{
values: values,
callback: callback,
property: this.inspector.getPropertyPath(this.propertyDefinition.property),
propertyDefinition: this.propertyDefinition
}])
if (optionsEvent.isDefaultPrevented()) {
return false
}
return true
}
DropdownEditor.prototype.saveDependencyValues = function() {
this.prevDependencyValues = this.getDependencyValues()
}
DropdownEditor.prototype.getDependencyValues = function() {
var result = ''
for (var i = 0, len = this.propertyDefinition.depends.length; i < len; i++) {
var property = this.propertyDefinition.depends[i],
value = this.inspector.getPropertyValue(property)
if (value === undefined) {
value = '';
}
result += property + ':' + value + '-'
}
return result
}
DropdownEditor.prototype.showLoadingIndicator = function() {
if (!('ontouchstart' in window || navigator.maxTouchPoints > 0)) {
this.indicatorContainer.loadIndicator()
}
}
DropdownEditor.prototype.hideLoadingIndicator = function() {
if (this.isDisposed()) {
return
}
if (!('ontouchstart' in window || navigator.maxTouchPoints > 0)) {
this.indicatorContainer.loadIndicator('hide')
this.indicatorContainer.loadIndicator('destroy')
}
}
DropdownEditor.prototype.optionsRequestDone = function(data, currentValue, initialization) {
if (this.isDisposed()) {
// Handle the case when the asynchronous request finishes after
// the editor is disposed
return
}
var select = this.getSelect()
// Without destroying and recreating the custom select
// there could be detached DOM nodes.
this.destroyCustomSelect()
this.clearOptions(select)
this.initCustomSelect()
this.createPlaceholder(select)
if (data.options) {
for (var i = 0, len = data.options.length; i < len; i++) {
this.createOption(select, data.options[i].title, data.options[i].value)
}
}
if (this.hasOptionValue(select, currentValue)) {
select.value = currentValue
}
else {
select.selectedIndex = this.propertyDefinition.placeholder === undefined ? 0 : -1
}
this.initialization = initialization
$(select).trigger('change')
this.initialization = false
}
$.oc.inspector.propertyEditors.dropdown = DropdownEditor
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/controls/inspector/inspector.editor.object.js
================================================
/*
* Inspector object editor class.
*
* This class uses other editors.
*/
+function ($) { "use strict";
var Base = $.oc.inspector.propertyEditors.base,
BaseProto = Base.prototype
var ObjectEditor = function(inspector, propertyDefinition, containerCell, group) {
if (propertyDefinition.properties === undefined) {
this.throwError('The properties property should be specified in the object editor configuration.')
}
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
ObjectEditor.prototype = Object.create(BaseProto)
ObjectEditor.prototype.constructor = Base
ObjectEditor.prototype.init = function() {
this.initControlGroup()
BaseProto.init.call(this)
}
//
// Building
//
ObjectEditor.prototype.build = function() {
var currentRow = this.containerCell.parentNode,
inspectorContainer = document.createElement('div'),
options = {
enableExternalParameterEditor: false,
onChange: this.proxy(this.onInspectorDataChange),
inspectorClass: this.inspector.options.inspectorClass
},
values = this.inspector.getPropertyValue(this.propertyDefinition.property)
if (values === undefined) {
values = {}
}
this.childInspector = new $.oc.inspector.surface(inspectorContainer,
this.propertyDefinition.properties,
values,
this.inspector.getInspectorUniqueId() + '-' + this.propertyDefinition.property,
options,
this.inspector,
this.group,
this.propertyDefinition.property)
this.inspector.mergeChildSurface(this.childInspector, currentRow)
}
//
// Helpers
//
ObjectEditor.prototype.cleanUpValue = function(value) {
if (value === undefined || typeof value !== 'object') {
return undefined
}
if (this.propertyDefinition.ignoreIfPropertyEmpty === undefined) {
return value
}
return this.getValueOrRemove(value)
}
ObjectEditor.prototype.getValueOrRemove = function(value) {
if (this.propertyDefinition.ignoreIfPropertyEmpty === undefined) {
return value
}
var targetProperty = this.propertyDefinition.ignoreIfPropertyEmpty,
targetValue = value[targetProperty]
if (this.isEmptyValue(targetValue)) {
return $.oc.inspector.removedProperty
}
return value
}
//
// Editor API methods
//
ObjectEditor.prototype.supportsExternalParameterEditor = function() {
return false
}
ObjectEditor.prototype.isGroupedEditor = function() {
return true
}
ObjectEditor.prototype.getUndefinedValue = function() {
var result = {}
for (var i = 0, len = this.propertyDefinition.properties.length; i < len; i++) {
var propertyName = this.propertyDefinition.properties[i].property,
editor = this.childInspector.findPropertyEditor(propertyName)
if (editor) {
result[propertyName] = editor.getUndefinedValue()
}
}
return this.getValueOrRemove(result)
}
ObjectEditor.prototype.validate = function(silentMode) {
var values = this.childInspector.getValues()
if (this.cleanUpValue(values) === $.oc.inspector.removedProperty) {
// Ignore any validation rules if the object's required
// property is empty (ignoreIfPropertyEmpty)
return true
}
return this.childInspector.validate(silentMode)
}
//
// Event handlers
//
ObjectEditor.prototype.onInspectorDataChange = function(property, value) {
var values = this.childInspector.getValues()
this.inspector.setPropertyValue(this.propertyDefinition.property, this.cleanUpValue(values))
}
$.oc.inspector.propertyEditors.object = ObjectEditor
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/controls/inspector/inspector.editor.objectlist.js
================================================
/*
* Inspector object list editor class.
*/
+function ($) { "use strict";
var Base = $.oc.inspector.propertyEditors.base,
BaseProto = Base.prototype
var ObjectListEditor = function(inspector, propertyDefinition, containerCell, group) {
this.currentRowInspector = null
this.popup = null
if (propertyDefinition.titleProperty === undefined) {
throw new Error('The titleProperty property should be specified in the objectList editor configuration. Property: ' + propertyDefinition.property)
}
if (propertyDefinition.itemProperties === undefined) {
throw new Error('The itemProperties property should be specified in the objectList editor configuration. Property: ' + propertyDefinition.property)
}
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
ObjectListEditor.prototype = Object.create(BaseProto)
ObjectListEditor.prototype.constructor = Base
ObjectListEditor.prototype.init = function() {
if (this.isKeyValueMode()) {
var keyProperty = this.getKeyProperty()
if (!keyProperty) {
throw new Error('Object list key property ' + this.propertyDefinition.keyProperty
+ ' is not defined in itemProperties. Property: ' + this.propertyDefinition.property)
}
}
BaseProto.init.call(this)
}
ObjectListEditor.prototype.dispose = function() {
this.unregisterHandlers()
this.removeControls()
this.currentRowInspector = null
this.popup = null
BaseProto.dispose.call(this)
}
ObjectListEditor.prototype.supportsExternalParameterEditor = function() {
return false
}
//
// Building
//
ObjectListEditor.prototype.build = function() {
var link = document.createElement('a')
$.oc.foundation.element.addClass(link, 'trigger')
link.setAttribute('href', '#')
this.setLinkText(link)
$.oc.foundation.element.addClass(this.containerCell, 'trigger-cell')
this.containerCell.appendChild(link)
}
ObjectListEditor.prototype.setLinkText = function(link, value) {
var value = value !== undefined && value !== null ? value
: this.inspector.getPropertyValue(this.propertyDefinition.property)
if (value === null) {
value = undefined
}
if (value === undefined) {
var placeholder = this.propertyDefinition.placeholder
if (placeholder !== undefined) {
$.oc.foundation.element.addClass(link, 'cell-placeholder')
link.textContent = placeholder
}
else {
link.textContent = 'Items: 0'
}
}
else {
var itemCount = 0
if (!this.isKeyValueMode()) {
if (value.length === undefined) {
throw new Error('Object list value should be an array. Property: ' + this.propertyDefinition.property)
}
itemCount = value.length
}
else {
if (typeof value !== 'object') {
throw new Error('Object list value should be an object. Property: ' + this.propertyDefinition.property)
}
itemCount = this.getValueKeys(value).length
}
$.oc.foundation.element.removeClass(link, 'cell-placeholder')
link.textContent = 'Items: ' + itemCount
}
}
ObjectListEditor.prototype.getPopupContent = function() {
return '';
}
ObjectListEditor.prototype.buildPopupContents = function(popup) {
this.buildItemsTable(popup)
}
ObjectListEditor.prototype.buildItemsTable = function(popup) {
var table = popup.querySelector('table'),
tbody = document.createElement('tbody'),
items = this.inspector.getPropertyValue(this.propertyDefinition.property),
titleProperty = this.propertyDefinition.titleProperty
if (items === undefined || this.getValueKeys(items).length === 0) {
var row = this.buildEmptyRow()
tbody.appendChild(row)
}
else {
var firstRow = undefined
for (var key in items) {
var item = items[key],
itemInspectorValue = this.addKeyProperty(key, item),
itemText = item[titleProperty],
row = this.buildTableRow(itemText, 'rowlink')
row.setAttribute('data-inspector-values', JSON.stringify(itemInspectorValue))
tbody.appendChild(row)
if (firstRow === undefined) {
firstRow = row
}
}
}
table.appendChild(tbody)
if (firstRow !== undefined) {
this.selectRow(firstRow, true)
}
this.updateScrollpads()
}
ObjectListEditor.prototype.buildEmptyRow = function() {
return this.buildTableRow('No items found', 'no-data', 'nolink')
}
ObjectListEditor.prototype.removeEmptyRow = function() {
var tbody = this.getTableBody(),
row = tbody.querySelector('tr.no-data')
if (row) {
tbody.removeChild(row)
}
}
ObjectListEditor.prototype.buildTableRow = function(text, rowClass, cellClass) {
var row = document.createElement('tr'),
cell = document.createElement('td')
cell.textContent = text
if (rowClass !== undefined) {
$.oc.foundation.element.addClass(row, rowClass)
}
if (cellClass !== undefined) {
$.oc.foundation.element.addClass(cell, cellClass)
}
row.appendChild(cell)
return row
}
ObjectListEditor.prototype.updateScrollpads = function() {
$('.control-scrollpad', this.popup).scrollpad('update')
}
//
// Built-in Inspector management
//
ObjectListEditor.prototype.selectRow = function(row, forceSelect) {
var tbody = row.parentNode,
inspectorContainer = this.getInspectorContainer(),
selectedRow = this.getSelectedRow()
if (selectedRow === row && !forceSelect) {
return
}
if (selectedRow) {
if (!this.validateKeyValue()) {
return
}
if (this.currentRowInspector) {
if (!this.currentRowInspector.validate()) {
return
}
}
this.applyDataToRow(selectedRow)
$.oc.foundation.element.removeClass(selectedRow, 'active')
}
this.disposeInspector()
$.oc.foundation.element.addClass(row, 'active')
this.createInspectorForRow(row, inspectorContainer)
}
ObjectListEditor.prototype.createInspectorForRow = function(row, inspectorContainer) {
var dataStr = row.getAttribute('data-inspector-values')
if (dataStr === undefined || typeof dataStr !== 'string') {
throw new Error('Values not found for the selected row.')
}
var properties = this.propertyDefinition.itemProperties,
values = JSON.parse(dataStr),
options = {
enableExternalParameterEditor: false,
onChange: this.proxy(this.onInspectorDataChange),
inspectorClass: this.inspector.options.inspectorClass
}
this.currentRowInspector = new $.oc.inspector.surface(inspectorContainer, properties, values,
$.oc.inspector.helpers.generateElementUniqueId(inspectorContainer), options)
}
ObjectListEditor.prototype.disposeInspector = function() {
$.oc.foundation.controlUtils.disposeControls(this.popup.querySelector('[data-inspector-container]'))
this.currentRowInspector = null
}
ObjectListEditor.prototype.applyDataToRow = function(row) {
if (this.currentRowInspector === null) {
return
}
var data = this.currentRowInspector.getValues()
row.setAttribute('data-inspector-values', JSON.stringify(data))
}
ObjectListEditor.prototype.updateRowText = function(property, value) {
var selectedRow = this.getSelectedRow()
if (!selectedRow) {
throw new Exception('A row is not found for the updated data')
}
if (property !== this.propertyDefinition.titleProperty) {
return
}
value = $.trim(value)
if (value.length === 0) {
value = '[No title]'
$.oc.foundation.element.addClass(selectedRow, 'disabled')
}
else {
$.oc.foundation.element.removeClass(selectedRow, 'disabled')
}
selectedRow.firstChild.textContent = value
}
ObjectListEditor.prototype.getSelectedRow = function() {
if (!this.popup) {
throw new Error('Trying to get selected row without a popup reference.')
}
var rows = this.getTableBody().children
for (var i = 0, len = rows.length; i < len; i++) {
if ($.oc.foundation.element.hasClass(rows[i], 'active')) {
return rows[i]
}
}
return null
}
ObjectListEditor.prototype.createItem = function() {
var selectedRow = this.getSelectedRow()
if (selectedRow) {
if (!this.validateKeyValue()) {
return
}
if (this.currentRowInspector) {
if (!this.currentRowInspector.validate()) {
return
}
}
this.applyDataToRow(selectedRow)
$.oc.foundation.element.removeClass(selectedRow, 'active')
}
this.disposeInspector()
var title = 'New item',
row = this.buildTableRow(title, 'rowlink active'),
tbody = this.getTableBody(),
data = {}
data[this.propertyDefinition.titleProperty] = title
row.setAttribute('data-inspector-values', JSON.stringify(data))
tbody.appendChild(row)
this.selectRow(row, true)
this.removeEmptyRow()
this.updateScrollpads()
}
ObjectListEditor.prototype.deleteItem = function() {
var selectedRow = this.getSelectedRow()
if (!selectedRow) {
return
}
var nextRow = selectedRow.nextElementSibling,
prevRow = selectedRow.previousElementSibling,
tbody = this.getTableBody()
this.disposeInspector()
tbody.removeChild(selectedRow)
var newSelectedRow = nextRow ? nextRow : prevRow
if (newSelectedRow) {
this.selectRow(newSelectedRow)
}
else {
tbody.appendChild(this.buildEmptyRow())
}
this.updateScrollpads()
}
ObjectListEditor.prototype.applyDataToParentInspector = function() {
var selectedRow = this.getSelectedRow(),
tbody = this.getTableBody(),
dataRows = tbody.querySelectorAll('tr[data-inspector-values]'),
link = this.getLink(),
result = this.getEmptyValue()
if (selectedRow) {
if (!this.validateKeyValue()) {
return
}
if (this.currentRowInspector) {
if (!this.currentRowInspector.validate()) {
return
}
}
this.applyDataToRow(selectedRow)
}
for (var i = 0, len = dataRows.length; i < len; i++) {
var dataRow = dataRows[i],
rowData = JSON.parse(dataRow.getAttribute('data-inspector-values'))
if (!this.isKeyValueMode()) {
result.push(rowData)
}
else {
var rowKey = rowData[this.propertyDefinition.keyProperty]
result[rowKey] = this.removeKeyProperty(rowData)
}
}
this.inspector.setPropertyValue(this.propertyDefinition.property, result)
this.setLinkText(link, result)
$(link).popup('hide')
return false
}
ObjectListEditor.prototype.validateKeyValue = function() {
if (!this.isKeyValueMode()) {
return true
}
if (this.currentRowInspector === null) {
return true
}
var data = this.currentRowInspector.getValues(),
keyProperty = this.propertyDefinition.keyProperty
if (data[keyProperty] === undefined) {
throw new Error('Key property ' + keyProperty + ' is not found in the Inspector data. Property: ' + this.propertyDefinition.property)
}
var keyPropertyValue = data[keyProperty],
keyPropertyTitle = this.getKeyProperty().title
if (typeof keyPropertyValue !== 'string') {
throw new Error('Key property (' + keyProperty + ') value should be a string. Property: ' + this.propertyDefinition.property)
}
if ($.trim(keyPropertyValue).length === 0) {
$.oc.flashMsg({text: 'The value of key property ' + keyPropertyTitle + ' cannot be empty.', 'class': 'error', 'interval': 3})
return false
}
var selectedRow = this.getSelectedRow(),
tbody = this.getTableBody(),
dataRows = tbody.querySelectorAll('tr[data-inspector-values]')
for (var i = 0, len = dataRows.length; i < len; i++) {
var dataRow = dataRows[i],
rowData = JSON.parse(dataRow.getAttribute('data-inspector-values'))
if (selectedRow == dataRow) {
continue
}
if (rowData[keyProperty] == keyPropertyValue) {
$.oc.flashMsg({text: 'The value of key property ' + keyPropertyTitle + ' should be unique.', 'class': 'error', 'interval': 3})
return false
}
}
return true
}
//
// Helpers
//
ObjectListEditor.prototype.getLink = function() {
return this.containerCell.querySelector('a.trigger')
}
ObjectListEditor.prototype.getPopupFormElement = function() {
var form = this.popup.querySelector('form')
if (!form) {
this.throwError('Cannot find form element in the popup window.')
}
return form
}
ObjectListEditor.prototype.getInspectorContainer = function() {
return this.popup.querySelector('div[data-inspector-container]')
}
ObjectListEditor.prototype.getTableBody = function() {
return this.popup.querySelector('table.inspector-table-list tbody')
}
ObjectListEditor.prototype.isKeyValueMode = function() {
return this.propertyDefinition.keyProperty !== undefined
}
ObjectListEditor.prototype.getKeyProperty = function() {
for (var i = 0, len = this.propertyDefinition.itemProperties.length; i < len; i++) {
var property = this.propertyDefinition.itemProperties[i]
if (property.property == this.propertyDefinition.keyProperty) {
return property
}
}
}
ObjectListEditor.prototype.getValueKeys = function(value) {
var result = []
for (var key in value) {
result.push(key)
}
return result
}
ObjectListEditor.prototype.addKeyProperty = function(key, value) {
if (!this.isKeyValueMode()) {
return value
}
value[this.propertyDefinition.keyProperty] = key
return value
}
ObjectListEditor.prototype.removeKeyProperty = function(value) {
if (!this.isKeyValueMode()) {
return value
}
var result = value
if (result[this.propertyDefinition.keyProperty] !== undefined) {
delete result[this.propertyDefinition.keyProperty]
}
return result
}
ObjectListEditor.prototype.getEmptyValue = function() {
if (this.isKeyValueMode()) {
return {}
}
else {
return []
}
}
//
// Event handlers
//
ObjectListEditor.prototype.registerHandlers = function() {
var link = this.getLink(),
$link = $(link)
link.addEventListener('click', this.proxy(this.onTriggerClick))
$link.on('shown.oc.popup', this.proxy(this.onPopupShown))
$link.on('hidden.oc.popup', this.proxy(this.onPopupHidden))
}
ObjectListEditor.prototype.unregisterHandlers = function() {
var link = this.getLink(),
$link = $(link)
link.removeEventListener('click', this.proxy(this.onTriggerClick))
$link.off('shown.oc.popup', this.proxy(this.onPopupShown))
$link.off('hidden.oc.popup', this.proxy(this.onPopupHidden))
}
ObjectListEditor.prototype.onTriggerClick = function(ev) {
$.oc.foundation.event.stop(ev)
var content = this.getPopupContent()
content = content.replace('{{property}}', this.propertyDefinition.title)
$(ev.target).popup({
content: content
})
return false
}
ObjectListEditor.prototype.onPopupShown = function(ev, link, popup) {
$(popup).on('submit.inspector', 'form', this.proxy(this.onSubmit))
$(popup).on('click', 'tr.rowlink', this.proxy(this.onRowClick))
$(popup).on('click.inspector', '[data-cmd]', this.proxy(this.onCommand))
this.popup = popup.get(0)
this.buildPopupContents(this.popup)
this.getRootSurface().popupDisplayed()
}
ObjectListEditor.prototype.onPopupHidden = function(ev, link, popup) {
$(popup).off('.inspector', this.proxy(this.onSubmit))
$(popup).off('click', 'tr.rowlink', this.proxy(this.onRowClick))
$(popup).off('click.inspector', '[data-cmd]', this.proxy(this.onCommand))
this.disposeInspector()
$.oc.foundation.controlUtils.disposeControls(this.popup)
this.popup = null
this.getRootSurface().popupHidden()
}
ObjectListEditor.prototype.onSubmit = function(ev) {
this.applyDataToParentInspector()
ev.preventDefault()
return false
}
ObjectListEditor.prototype.onRowClick = function(ev) {
this.selectRow(ev.currentTarget)
}
ObjectListEditor.prototype.onInspectorDataChange = function(property, value) {
this.updateRowText(property, value)
}
ObjectListEditor.prototype.onCommand = function(ev) {
var command = ev.currentTarget.getAttribute('data-cmd')
switch (command) {
case 'create-item' :
this.createItem()
break;
case 'delete-item' :
this.deleteItem()
break;
}
}
//
// Disposing
//
ObjectListEditor.prototype.removeControls = function() {
if (this.popup) {
this.disposeInspector(this.popup)
}
}
$.oc.inspector.propertyEditors.objectList = ObjectListEditor
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/controls/inspector/inspector.editor.popupbase.js
================================================
/*
* Base class for Inspector editors that create popups.
*/
+function ($) { "use strict";
var Base = $.oc.inspector.propertyEditors.base,
BaseProto = Base.prototype
var PopupBase = function(inspector, propertyDefinition, containerCell, group) {
this.popup = null
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
PopupBase.prototype = Object.create(BaseProto)
PopupBase.prototype.constructor = Base
PopupBase.prototype.dispose = function() {
this.unregisterHandlers()
this.popup = null
BaseProto.dispose.call(this)
}
PopupBase.prototype.build = function() {
var link = document.createElement('a')
$.oc.foundation.element.addClass(link, 'trigger')
link.setAttribute('href', '#')
this.setLinkText(link)
$.oc.foundation.element.addClass(this.containerCell, 'trigger-cell')
this.containerCell.appendChild(link)
}
PopupBase.prototype.setLinkText = function(link, value) {
}
PopupBase.prototype.getPopupContent = function() {
return ''
}
PopupBase.prototype.updateDisplayedValue = function(value) {
this.setLinkText(this.getLink(), value)
}
PopupBase.prototype.registerHandlers = function() {
var link = this.getLink(),
$link = $(link)
link.addEventListener('click', this.proxy(this.onTriggerClick))
$link.on('shown.oc.popup', this.proxy(this.onPopupShown))
$link.on('hidden.oc.popup', this.proxy(this.onPopupHidden))
}
PopupBase.prototype.unregisterHandlers = function() {
var link = this.getLink(),
$link = $(link)
link.removeEventListener('click', this.proxy(this.onTriggerClick))
$link.off('shown.oc.popup', this.proxy(this.onPopupShown))
$link.off('hidden.oc.popup', this.proxy(this.onPopupHidden))
}
PopupBase.prototype.getLink = function() {
return this.containerCell.querySelector('a.trigger')
}
PopupBase.prototype.configurePopup = function(popup) {
}
PopupBase.prototype.handleSubmit = function($form) {
}
PopupBase.prototype.hidePopup = function() {
$(this.getLink()).popup('hide')
}
PopupBase.prototype.onTriggerClick = function(ev) {
$.oc.foundation.event.stop(ev)
var content = this.getPopupContent()
content = content.replace('{{property}}', this.propertyDefinition.title)
$(ev.target).popup({
content: content
})
return false
}
PopupBase.prototype.onPopupShown = function(ev, link, popup) {
$(popup).on('submit.inspector', 'form', this.proxy(this.onSubmit))
this.popup = popup.get(0)
this.configurePopup(popup)
this.getRootSurface().popupDisplayed()
}
PopupBase.prototype.onPopupHidden = function(ev, link, popup) {
$(popup).off('.inspector', 'form', this.proxy(this.onSubmit))
this.popup = null
this.getRootSurface().popupHidden()
}
PopupBase.prototype.onSubmit = function(ev) {
ev.preventDefault()
if (this.handleSubmit($(ev.target)) === false) {
return false
}
this.setLinkText(this.getLink())
this.hidePopup()
return false
}
$.oc.inspector.propertyEditors.popupBase = PopupBase
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/controls/inspector/inspector.editor.set.js
================================================
/*
* Inspector set editor class.
*
* This class uses $.oc.inspector.propertyEditors.checkbox editor.
*/
+function ($) { "use strict";
var Base = $.oc.inspector.propertyEditors.base,
BaseProto = Base.prototype
var SetEditor = function(inspector, propertyDefinition, containerCell, group) {
this.editors = []
this.loadedItems = null
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
SetEditor.prototype = Object.create(BaseProto)
SetEditor.prototype.constructor = Base
SetEditor.prototype.init = function() {
this.initControlGroup()
BaseProto.init.call(this)
}
SetEditor.prototype.dispose = function() {
this.disposeEditors()
this.disposeControls()
this.editors = null
BaseProto.dispose.call(this)
}
//
// Building
//
SetEditor.prototype.build = function() {
var link = document.createElement('a')
$.oc.foundation.element.addClass(link, 'trigger')
link.setAttribute('href', '#')
this.setLinkText(link)
$.oc.foundation.element.addClass(this.containerCell, 'trigger-cell')
this.containerCell.appendChild(link)
if (this.propertyDefinition.items !== undefined) {
this.loadStaticItems()
}
else {
this.loadDynamicItems()
}
}
SetEditor.prototype.loadStaticItems = function() {
var itemArray = []
for (var itemValue in this.propertyDefinition.items) {
itemArray.push({
value: itemValue,
title: this.propertyDefinition.items[itemValue]
})
}
for (var i = itemArray.length-1; i >=0; i--) {
this.buildItemEditor(itemArray[i].value, itemArray[i].title)
}
}
SetEditor.prototype.setLinkText = function(link, value) {
var value = (value !== undefined && value !== null) ? value
: this.getNormalizedValue(),
text = '[ ]'
if (value === undefined) {
value = this.propertyDefinition.default
}
if (value !== undefined && value.length !== undefined && value.length > 0 && typeof value !== 'string') {
var textValues = []
for (var i = 0, len = value.length; i < len; i++) {
textValues.push(this.valueToText(value[i]))
}
text = '[' + textValues.join(', ') + ']'
$.oc.foundation.element.removeClass(link, 'cell-placeholder')
}
else {
text = this.propertyDefinition.placeholder
if ((typeof text === 'string' && text.length == 0) || text === undefined) {
text = '[ ]'
}
$.oc.foundation.element.addClass(link, 'cell-placeholder')
}
link.textContent = text
}
SetEditor.prototype.buildItemEditor = function(value, text) {
var property = {
title: text,
itemType: 'property',
groupIndex: this.group.getGroupIndex()
},
newRow = this.createGroupedRow(property),
currentRow = this.containerCell.parentNode,
tbody = this.containerCell.parentNode.parentNode, // row / tbody
cell = document.createElement('td')
this.buildCheckbox(cell, value, text)
newRow.appendChild(cell)
tbody.insertBefore(newRow, currentRow.nextSibling)
}
SetEditor.prototype.buildCheckbox = function(cell, value, title) {
var property = {
property: value,
title: title,
default: this.isCheckedByDefault(value)
},
editor = new $.oc.inspector.propertyEditors.checkbox(this, property, cell, this.group)
this.editors.push[editor]
}
SetEditor.prototype.isCheckedByDefault = function(value) {
if (!this.propertyDefinition.default) {
return false
}
return this.propertyDefinition.default.indexOf(value) > -1
}
//
// Dynamic items
//
SetEditor.prototype.showLoadingIndicator = function() {
$(this.getLink()).loadIndicator()
}
SetEditor.prototype.hideLoadingIndicator = function() {
if (this.isDisposed()) {
return
}
var $link = $(this.getLink())
$link.loadIndicator('hide')
$link.loadIndicator('destroy')
}
SetEditor.prototype.loadDynamicItems = function() {
var link = this.getLink(),
data = this.inspector.getValues(),
$form = $(link).closest('form');
$.oc.foundation.element.addClass(link, 'loading-indicator-container size-small');
this.showLoadingIndicator();
data['inspectorProperty'] = this.getPropertyPath();
data['inspectorClassName'] = this.inspector.options.inspectorClass;
$form.request(this.inspector.getEventHandler('onInspectableGetOptions'), {
data: data,
progressBar: false
})
.done(this.proxy(this.itemsRequestDone))
.always(this.proxy(this.hideLoadingIndicator));
}
SetEditor.prototype.itemsRequestDone = function(data, currentValue, initialization) {
if (this.isDisposed()) {
// Handle the case when the asynchronous request finishes after
// the editor is disposed
return
}
this.loadedItems = {}
if (data.options) {
for (var i = data.options.length-1; i >= 0; i--) {
this.buildItemEditor(data.options[i].value, data.options[i].title)
this.loadedItems[data.options[i].value] = data.options[i].title
}
}
this.setLinkText(this.getLink())
}
//
// Helpers
//
SetEditor.prototype.getLink = function() {
return this.containerCell.querySelector('a.trigger')
}
SetEditor.prototype.getItemsSource = function() {
if (this.propertyDefinition.items !== undefined) {
return this.propertyDefinition.items
}
return this.loadedItems
}
SetEditor.prototype.valueToText = function(value) {
var source = this.getItemsSource()
if (!source) {
return value
}
for (var itemValue in source) {
if (itemValue == value) {
return source[itemValue]
}
}
return value
}
/*
* Removes items that don't exist in the defined items from
* the value.
*/
SetEditor.prototype.cleanUpValue = function(value) {
if (!value) {
return value
}
var result = [],
source = this.getItemsSource()
for (var i = 0, len = value.length; i < len; i++) {
var currentValue = value[i]
if (source[currentValue] !== undefined) {
result.push(currentValue)
}
}
return result
}
SetEditor.prototype.getNormalizedValue = function() {
var value = this.inspector.getPropertyValue(this.propertyDefinition.property);
if (value === null) {
value = undefined;
}
if (value === undefined) {
return value;
}
if (value.length === undefined || typeof value === 'string') {
return undefined;
}
return value;
}
//
// Editor API methods
//
SetEditor.prototype.supportsExternalParameterEditor = function() {
return false
}
SetEditor.prototype.isGroupedEditor = function() {
return true
}
//
// Inspector API methods
//
// This editor creates checkbox editor and acts as a container Inspector
// for them. The methods in this section emulate and proxy some functionality
// of the Inspector.
//
SetEditor.prototype.getPropertyValue = function(checkboxValue) {
// When a checkbox requests the property value, we return
// TRUE if the checkbox value is listed in the current values of
// the set.
// For example, the available set items are [create, update], the
// current set value is [create] and checkboxValue is "create".
// The result of the method will be TRUE.
var value = this.getNormalizedValue();
if (value === undefined) {
return this.isCheckedByDefault(checkboxValue);
}
if (!value) {
return false;
}
return value.indexOf(checkboxValue) > -1;
}
SetEditor.prototype.setPropertyValue = function(checkboxValue, isChecked) {
// In this method the Set Editor mimics the Surface.
// It acts as a parent surface for the children checkboxes,
// watching changes in them and updating the link text.
var currentValue = this.getNormalizedValue()
if (currentValue === undefined) {
currentValue = this.propertyDefinition.default
}
if (!currentValue) {
currentValue = []
}
var resultValue = [],
items = this.getItemsSource()
for (var itemValue in items) {
if (itemValue !== checkboxValue) {
if (currentValue.indexOf(itemValue) !== -1) {
resultValue.push(itemValue)
}
}
else {
if (isChecked) {
resultValue.push(itemValue);
}
}
}
this.inspector.setPropertyValue(this.propertyDefinition.property, this.cleanUpValue(resultValue));
this.setLinkText(this.getLink());
}
SetEditor.prototype.generateSequencedId = function() {
return this.inspector.generateSequencedId()
}
//
// Disposing
//
SetEditor.prototype.disposeEditors = function() {
for (var i = 0, len = this.editors.length; i < len; i++) {
var editor = this.editors[i]
editor.dispose()
}
}
SetEditor.prototype.disposeControls = function() {
var link = this.getLink()
if (this.propertyDefinition.items === undefined) {
$(link).loadIndicator('destroy')
}
link.parentNode.removeChild(link)
}
$.oc.inspector.propertyEditors.set = SetEditor
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/controls/inspector/inspector.editor.string.js
================================================
/*
* Inspector string editor class.
*/
+function ($) { "use strict";
var Base = $.oc.inspector.propertyEditors.base,
BaseProto = Base.prototype
var StringEditor = function(inspector, propertyDefinition, containerCell, group) {
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
StringEditor.prototype = Object.create(BaseProto)
StringEditor.prototype.constructor = Base
StringEditor.prototype.dispose = function() {
this.unregisterHandlers()
BaseProto.dispose.call(this)
}
StringEditor.prototype.build = function() {
var editor = document.createElement('input'),
placeholder = this.propertyDefinition.placeholder !== undefined ? this.propertyDefinition.placeholder : '',
value = this.inspector.getPropertyValue(this.propertyDefinition.property)
editor.setAttribute('type', 'text')
editor.setAttribute('class', 'string-editor')
editor.setAttribute('placeholder', placeholder)
if (value === undefined) {
value = this.propertyDefinition.default
}
if (value === undefined) {
value = ''
}
editor.value = value
$.oc.foundation.element.addClass(this.containerCell, 'text')
this.containerCell.appendChild(editor)
}
StringEditor.prototype.updateDisplayedValue = function(value) {
this.getInput().value = value
}
StringEditor.prototype.getInput = function() {
return this.containerCell.querySelector('input');
}
StringEditor.prototype.focus = function() {
this.getInput().focus({ preventScroll: true });
this.onInputFocus();
}
StringEditor.prototype.registerHandlers = function() {
var input = this.getInput();
input.addEventListener('focus', this.proxy(this.onInputFocus));
input.addEventListener('keyup', this.proxy(this.onInputKeyUp));
}
StringEditor.prototype.unregisterHandlers = function() {
var input = this.getInput();
input.removeEventListener('focus', this.proxy(this.onInputFocus));
input.removeEventListener('keyup', this.proxy(this.onInputKeyUp));
}
StringEditor.prototype.onInputFocus = function(ev) {
this.inspector.makeCellActive(this.containerCell);
}
StringEditor.prototype.onInputKeyUp = function() {
var value = $.trim(this.getInput().value);
this.inspector.setPropertyValue(this.propertyDefinition.property, value);
}
StringEditor.prototype.onExternalPropertyEditorHidden = function() {
this.focus()
}
$.oc.inspector.propertyEditors.string = StringEditor
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/controls/inspector/inspector.editor.stringlist.js
================================================
/*
* Inspector string list editor class.
*/
+function ($) { "use strict";
var Base = $.oc.inspector.propertyEditors.text,
BaseProto = Base.prototype
var StringListEditor = function(inspector, propertyDefinition, containerCell, group) {
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
StringListEditor.prototype = Object.create(BaseProto)
StringListEditor.prototype.constructor = Base
StringListEditor.prototype.setLinkText = function(link, value) {
var value = value !== undefined ? value
: this.inspector.getPropertyValue(this.propertyDefinition.property)
if (value === undefined) {
value = this.propertyDefinition.default
}
this.checkValueType(value)
if (!value) {
value = this.propertyDefinition.placeholder
$.oc.foundation.element.addClass(link, 'cell-placeholder')
if (!value) {
value = '[]'
}
link.textContent = value
}
else {
$.oc.foundation.element.removeClass(link, 'cell-placeholder')
link.textContent = '[' + value.join(', ') + ']'
}
}
StringListEditor.prototype.checkValueType = function(value) {
if (value && Object.prototype.toString.call(value) !== '[object Array]') {
this.throwError('The string list value should be an array.')
}
}
StringListEditor.prototype.configurePopup = function(popup) {
var $textarea = $(popup).find('textarea'),
value = this.inspector.getPropertyValue(this.propertyDefinition.property)
if (this.propertyDefinition.placeholder) {
$textarea.attr('placeholder', this.propertyDefinition.placeholder)
}
if (value === undefined) {
value = this.propertyDefinition.default
}
this.checkValueType(value)
if (value && value.length) {
$textarea.val(value.join('\n'))
}
$textarea.focus()
this.configureComment(popup)
}
StringListEditor.prototype.handleSubmit = function($form) {
var $textarea = $form.find('textarea'),
link = this.getLink(),
value = $.trim($textarea.val()),
arrayValue = [],
resultValue = []
if (value.length) {
value = value.replace(/\r\n/g, '\n')
arrayValue = value.split('\n')
for (var i = 0, len = arrayValue.length; i < len; i++) {
var currentValue = $.trim(arrayValue[i])
if (currentValue.length > 0) {
resultValue.push(currentValue)
}
}
}
this.inspector.setPropertyValue(this.propertyDefinition.property, resultValue)
}
$.oc.inspector.propertyEditors.stringList = StringListEditor
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/controls/inspector/inspector.editor.stringlistautocomplete.js
================================================
/*
* Inspector string list with autocompletion editor class.
*
* TODO: validation is not implemented in this editor. See the Dictionary editor for reference.
*/
+function ($) { "use strict";
var Base = $.oc.inspector.propertyEditors.popupBase,
BaseProto = Base.prototype
var StringListAutocomplete = function(inspector, propertyDefinition, containerCell, group) {
this.items = null
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
StringListAutocomplete.prototype = Object.create(BaseProto)
StringListAutocomplete.prototype.constructor = Base
StringListAutocomplete.prototype.dispose = function() {
BaseProto.dispose.call(this)
}
StringListAutocomplete.prototype.init = function() {
BaseProto.init.call(this)
}
StringListAutocomplete.prototype.supportsExternalParameterEditor = function() {
return false
}
StringListAutocomplete.prototype.setLinkText = function(link, value) {
var value = value !== undefined ? value
: this.inspector.getPropertyValue(this.propertyDefinition.property)
if (value === undefined) {
value = this.propertyDefinition.default
}
this.checkValueType(value)
if (!value) {
value = this.propertyDefinition.placeholder
$.oc.foundation.element.addClass(link, 'cell-placeholder')
if (!value) {
value = '[]'
}
link.textContent = value
}
else {
$.oc.foundation.element.removeClass(link, 'cell-placeholder')
link.textContent = '[' + value.join(', ') + ']'
}
}
StringListAutocomplete.prototype.checkValueType = function(value) {
if (value && Object.prototype.toString.call(value) !== '[object Array]') {
this.throwError('The string list value should be an array.')
}
}
//
// Popup editor methods
//
StringListAutocomplete.prototype.getPopupContent = function() {
return ''
}
StringListAutocomplete.prototype.configurePopup = function(popup) {
this.initAutocomplete()
this.buildItemsTable(popup.get(0))
this.focusFirstInput()
}
StringListAutocomplete.prototype.handleSubmit = function($form) {
return this.applyValues()
}
//
// Building and row management
//
StringListAutocomplete.prototype.buildItemsTable = function(popup) {
var table = popup.querySelector('table.inspector-dictionary-table'),
tbody = document.createElement('tbody'),
items = this.inspector.getPropertyValue(this.propertyDefinition.property)
if (items === undefined) {
items = this.propertyDefinition.default
}
if (items === undefined || this.getValueKeys(items).length === 0) {
var row = this.buildEmptyRow()
tbody.appendChild(row)
}
else {
for (var key in items) {
var row = this.buildTableRow(items[key])
tbody.appendChild(row)
}
}
table.appendChild(tbody)
this.updateScrollpads()
}
StringListAutocomplete.prototype.buildTableRow = function(value) {
var row = document.createElement('tr'),
valueCell = document.createElement('td')
this.createInput(valueCell, value)
row.appendChild(valueCell)
return row
}
StringListAutocomplete.prototype.buildEmptyRow = function() {
return this.buildTableRow(null)
}
StringListAutocomplete.prototype.createInput = function(container, value) {
var input = document.createElement('input'),
controlContainer = document.createElement('div')
input.setAttribute('type', 'text')
input.setAttribute('class', 'form-control')
input.value = value
controlContainer.appendChild(input)
container.appendChild(controlContainer)
}
StringListAutocomplete.prototype.setActiveCell = function(input) {
var activeCells = this.popup.querySelectorAll('td.active')
for (var i = activeCells.length-1; i >= 0; i--) {
$.oc.foundation.element.removeClass(activeCells[i], 'active')
}
var activeCell = input.parentNode.parentNode // input / div / td
$.oc.foundation.element.addClass(activeCell, 'active')
this.buildAutoComplete(input)
}
StringListAutocomplete.prototype.createItem = function() {
var activeRow = this.getActiveRow(),
newRow = this.buildEmptyRow(),
tbody = this.getTableBody(),
nextSibling = activeRow ? activeRow.nextElementSibling : null
tbody.insertBefore(newRow, nextSibling)
this.focusAndMakeActive(newRow.querySelector('input'))
this.updateScrollpads()
}
StringListAutocomplete.prototype.deleteItem = function() {
var activeRow = this.getActiveRow(),
tbody = this.getTableBody()
if (!activeRow) {
return
}
var nextRow = activeRow.nextElementSibling,
prevRow = activeRow.previousElementSibling,
input = this.getRowInputByIndex(activeRow, 0)
if (input) {
this.removeAutocomplete(input)
}
tbody.removeChild(activeRow)
var newSelectedRow = nextRow ? nextRow : prevRow
if (!newSelectedRow) {
newSelectedRow = this.buildEmptyRow()
tbody.appendChild(newSelectedRow)
}
this.focusAndMakeActive(newSelectedRow.querySelector('input'))
this.updateScrollpads()
}
StringListAutocomplete.prototype.applyValues = function() {
var tbody = this.getTableBody(),
dataRows = tbody.querySelectorAll('tr'),
link = this.getLink(),
result = []
for (var i = 0, len = dataRows.length; i < len; i++) {
var dataRow = dataRows[i],
valueInput = this.getRowInputByIndex(dataRow, 0),
value = $.trim(valueInput.value)
if (value.length == 0) {
continue
}
result.push(value)
}
this.inspector.setPropertyValue(this.propertyDefinition.property, result)
this.setLinkText(link, result)
}
//
// Helpers
//
StringListAutocomplete.prototype.getValueKeys = function(value) {
var result = []
for (var key in value) {
result.push(key)
}
return result
}
StringListAutocomplete.prototype.getActiveRow = function() {
var activeCell = this.popup.querySelector('td.active')
if (!activeCell) {
return null
}
return activeCell.parentNode
}
StringListAutocomplete.prototype.getTableBody = function() {
return this.popup.querySelector('table.inspector-dictionary-table tbody')
}
StringListAutocomplete.prototype.updateScrollpads = function() {
$('.control-scrollpad', this.popup).scrollpad('update')
}
StringListAutocomplete.prototype.focusFirstInput = function() {
var input = this.popup.querySelector('td input')
if (input) {
input.focus()
this.setActiveCell(input)
}
}
StringListAutocomplete.prototype.getEditorCell = function(cell) {
return cell.parentNode.parentNode // cell / div / td
}
StringListAutocomplete.prototype.getEditorRow = function(cell) {
return cell.parentNode.parentNode.parentNode // cell / div / td / tr
}
StringListAutocomplete.prototype.focusAndMakeActive = function(input) {
input.focus()
this.setActiveCell(input)
}
StringListAutocomplete.prototype.getRowInputByIndex = function(row, index) {
return row.cells[index].querySelector('input')
}
//
// Navigation
//
StringListAutocomplete.prototype.navigateDown = function(ev) {
var cell = this.getEditorCell(ev.currentTarget),
row = this.getEditorRow(ev.currentTarget),
nextRow = row.nextElementSibling
if (!nextRow) {
return
}
var newActiveEditor = nextRow.cells[cell.cellIndex].querySelector('input')
this.focusAndMakeActive(newActiveEditor)
}
StringListAutocomplete.prototype.navigateUp = function(ev) {
var cell = this.getEditorCell(ev.currentTarget),
row = this.getEditorRow(ev.currentTarget),
prevRow = row.previousElementSibling
if (!prevRow) {
return
}
var newActiveEditor = prevRow.cells[cell.cellIndex].querySelector('input')
this.focusAndMakeActive(newActiveEditor)
}
//
// Autocomplete
//
StringListAutocomplete.prototype.initAutocomplete = function() {
if (this.propertyDefinition.items !== undefined) {
this.items = this.prepareItems(this.propertyDefinition.items)
this.initializeAutocompleteForCurrentInput()
}
else {
this.loadDynamicItems()
}
}
StringListAutocomplete.prototype.initializeAutocompleteForCurrentInput = function() {
var activeElement = document.activeElement
if (!activeElement) {
return
}
var inputs = this.popup.querySelectorAll('td input.form-control')
if (!inputs) {
return
}
for (var i=inputs.length-1; i>=0; i--) {
if (inputs[i] === activeElement) {
this.buildAutoComplete(inputs[i])
return
}
}
}
StringListAutocomplete.prototype.buildAutoComplete = function(input) {
if (this.items === null) {
return
}
$(input).autocomplete({
source: this.items,
matchWidth: true,
menu: '
',
bodyContainer: true
})
}
StringListAutocomplete.prototype.removeAutocomplete = function(input) {
var $input = $(input)
if (!$input.data('autocomplete')) {
return
}
$input.autocomplete('destroy')
}
StringListAutocomplete.prototype.prepareItems = function(items) {
var result = {}
if (Array.isArray(items)) {
for (var i = 0, len = items.length; i < len; i++) {
result[items[i]] = items[i]
}
}
else {
result = items
}
return result
}
StringListAutocomplete.prototype.loadDynamicItems = function() {
if (this.isDisposed()) {
return;
}
var data = this.getRootSurface().getValues(),
$form = $(this.popup).find('form');
if (this.triggerGetItems(data) === false) {
return;
}
data['inspectorProperty'] = this.getPropertyPath();
data['inspectorClassName'] = this.inspector.options.inspectorClass;
$form.request(this.inspector.getEventHandler('onInspectableGetOptions'), {
data: data,
progressBar: false
})
.done(this.proxy(this.itemsRequestDone));
}
StringListAutocomplete.prototype.triggerGetItems = function(values) {
var $inspectable = this.getInspectableElement()
if (!$inspectable) {
return true
}
var itemsEvent = $.Event('autocompleteitems.oc.inspector')
$inspectable.trigger(itemsEvent, [{
values: values,
callback: this.proxy(this.itemsRequestDone),
property: this.inspector.getPropertyPath(this.propertyDefinition.property),
propertyDefinition: this.propertyDefinition
}])
if (itemsEvent.isDefaultPrevented()) {
return false
}
return true
}
StringListAutocomplete.prototype.itemsRequestDone = function(data) {
if (this.isDisposed()) {
// Handle the case when the asynchronous request finishes after
// the editor is disposed
return
}
var loadedItems = {}
if (data.options) {
for (var i = data.options.length-1; i >= 0; i--) {
loadedItems[data.options[i].value] = data.options[i].title
}
}
this.items = this.prepareItems(loadedItems)
this.initializeAutocompleteForCurrentInput()
}
StringListAutocomplete.prototype.removeAutocompleteFromAllRows = function() {
var inputs = this.popup.querySelector('td input.form-control')
for (var i=inputs.length-1; i>=0; i--) {
this.removeAutocomplete(inputs[i])
}
}
//
// Event handlers
//
StringListAutocomplete.prototype.onPopupShown = function(ev, link, popup) {
BaseProto.onPopupShown.call(this,ev, link, popup)
popup.on('focus.inspector', 'td input', this.proxy(this.onFocus))
popup.on('blur.inspector', 'td input', this.proxy(this.onBlur))
popup.on('keydown.inspector', 'td input', this.proxy(this.onKeyDown))
popup.on('click.inspector', '[data-cmd]', this.proxy(this.onCommand))
}
StringListAutocomplete.prototype.onPopupHidden = function(ev, link, popup) {
popup.off('.inspector', 'td input')
popup.off('.inspector', '[data-cmd]', this.proxy(this.onCommand))
this.removeAutocompleteFromAllRows()
this.items = null
BaseProto.onPopupHidden.call(this, ev, link, popup)
}
StringListAutocomplete.prototype.onFocus = function(ev) {
this.setActiveCell(ev.currentTarget)
}
StringListAutocomplete.prototype.onBlur = function(ev) {
if ($(ev.relatedTarget).closest('ul.inspector-autocomplete').length > 0) {
// Do not close the autocomplete results if a drop-down
// menu item was clicked
return
}
this.removeAutocomplete(ev.currentTarget)
}
StringListAutocomplete.prototype.onCommand = function(ev) {
var command = ev.currentTarget.getAttribute('data-cmd')
switch (command) {
case 'create-item' :
this.createItem()
break;
case 'delete-item' :
this.deleteItem()
break;
}
}
StringListAutocomplete.prototype.onKeyDown = function(ev) {
if (ev.key === 'ArrowDown') {
return this.navigateDown(ev)
}
else if (ev.key === 'ArrowUp') {
return this.navigateUp(ev)
}
}
$.oc.inspector.propertyEditors.stringListAutocomplete = StringListAutocomplete
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/controls/inspector/inspector.editor.text.js
================================================
/*
* Inspector text editor class.
*/
+function ($) { "use strict";
var Base = $.oc.inspector.propertyEditors.popupBase,
BaseProto = Base.prototype
var TextEditor = function(inspector, propertyDefinition, containerCell, group) {
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
TextEditor.prototype = Object.create(BaseProto)
TextEditor.prototype.constructor = Base
TextEditor.prototype.setLinkText = function(link, value) {
var value = value !== undefined ? value
: this.inspector.getPropertyValue(this.propertyDefinition.property)
if (value === undefined) {
value = this.propertyDefinition.default
}
if (!value) {
value = this.propertyDefinition.placeholder
$.oc.foundation.element.addClass(link, 'cell-placeholder')
}
else {
$.oc.foundation.element.removeClass(link, 'cell-placeholder')
}
if (typeof value === 'string') {
value = value.replace(/(?:\r\n|\r|\n)/g, ' ');
value = $.trim(value)
value = value.substring(0, 300);
}
link.textContent = value
}
TextEditor.prototype.getPopupContent = function() {
return ''
}
TextEditor.prototype.configureComment = function(popup) {
if (!this.propertyDefinition.description) {
return
}
var descriptionElement = $(popup).find('p.inspector-field-comment')
descriptionElement.text(this.propertyDefinition.description)
}
TextEditor.prototype.configurePopup = function(popup) {
var $textarea = $(popup).find('textarea'),
value = this.inspector.getPropertyValue(this.propertyDefinition.property)
if (this.propertyDefinition.placeholder) {
$textarea.attr('placeholder', this.propertyDefinition.placeholder)
}
if (value === undefined) {
value = this.propertyDefinition.default
}
$textarea.val(value)
$textarea.focus()
this.configureComment(popup)
}
TextEditor.prototype.handleSubmit = function($form) {
var $textarea = $form.find('textarea'),
link = this.getLink(),
value = $.trim($textarea.val())
this.inspector.setPropertyValue(this.propertyDefinition.property, value)
}
$.oc.inspector.propertyEditors.text = TextEditor
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/controls/inspector/inspector.engine.js
================================================
/*
* Inspector engine helpers.
*
* The helpers are used mostly by the Inspector Surface.
*
*/
+function ($) { "use strict";
// NAMESPACES
// ============================
if ($.oc === undefined)
$.oc = {}
if ($.oc.inspector === undefined)
$.oc.inspector = {}
$.oc.inspector.engine = {}
// CLASS DEFINITION
// ============================
function findGroup(group, properties) {
for (var i = 0, len = properties.length; i < len; i++) {
var property = properties[i]
if (property.itemType !== undefined && property.itemType == 'group' && property.title == group) {
return property
}
}
return null
}
$.oc.inspector.engine.processPropertyGroups = function(properties) {
var fields = [],
result = {
hasGroups: false,
properties: []
},
groupIndex = 0
for (var i = 0, len = properties.length; i < len; i++) {
var property = properties[i]
if (property['sortOrder'] === undefined) {
property['sortOrder'] = (i+1)*20
}
}
properties.sort(function(a, b){
return a['sortOrder'] - b['sortOrder']
})
for (var i = 0, len = properties.length; i < len; i++) {
var property = properties[i]
property.itemType = 'property'
if (property.group === undefined) {
fields.push(property)
}
else {
var group = findGroup(property.group, fields)
if (!group) {
group = {
itemType: 'group',
title: property.group,
properties: [],
groupIndex: groupIndex
}
groupIndex++
fields.push(group)
}
property.groupIndex = group.groupIndex
group.properties.push(property)
}
}
for (var i = 0, len = fields.length; i < len; i++) {
var property = fields[i]
result.properties.push(property)
if (property.itemType == 'group') {
result.hasGroups = true
for (var j = 0, propertiesLen = property.properties.length; j < propertiesLen; j++) {
result.properties.push(property.properties[j])
}
delete property.properties
}
}
return result
}
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/controls/inspector/inspector.externalparametereditor.js
================================================
/*
* External parameter editor for Inspector.
*
* The external parameter editor allows to use URL and
* other external parameters as values for the inspectable
* properties.
*
*/
+function ($) { "use strict";
// NAMESPACES
// ============================
if ($.oc === undefined)
$.oc = {}
if ($.oc.inspector === undefined)
$.oc.inspector = {}
// CLASS DEFINITION
// ============================
var Base = $.oc.foundation.base,
BaseProto = Base.prototype
var ExternalParameterEditor = function(inspector, propertyDefinition, containerCell, initialValue) {
this.inspector = inspector
this.propertyDefinition = propertyDefinition
this.containerCell = containerCell
this.initialValue = initialValue
Base.call(this)
this.init()
}
ExternalParameterEditor.prototype = Object.create(BaseProto)
ExternalParameterEditor.prototype.constructor = Base
ExternalParameterEditor.prototype.dispose = function() {
this.disposeControls()
this.unregisterHandlers()
this.inspector = null
this.propertyDefinition = null
this.containerCell = null
this.initialValue = null
BaseProto.dispose.call(this)
}
ExternalParameterEditor.prototype.init = function() {
this.tooltipText = 'Click to enter the external parameter name to load the property value from'
this.build()
this.registerHandlers()
this.setInitialValue()
}
/**
* Builds the external parameter editor markup:
*
*
').appendTo(placeholder);
if (options.legend.backgroundOpacity != 0.0) {
// put in the transparent background
// separately to avoid blended labels and
// label boxes
var c = options.legend.backgroundColor;
if (c == null) {
c = options.grid.backgroundColor;
if (c && typeof c == "string")
c = $.color.parse(c);
else
c = $.color.extract(legend, 'background-color');
c.a = 1;
c = c.toString();
}
var div = legend.children();
$('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
}
}
}
// interactive features
var highlights = [],
redrawTimeout = null;
// returns the data item the mouse is over, or null if none is found
function findNearbyItem(mouseX, mouseY, seriesFilter) {
var maxDistance = options.grid.mouseActiveRadius,
smallestDistance = maxDistance * maxDistance + 1,
item = null, foundPoint = false, i, j, ps;
for (i = series.length - 1; i >= 0; --i) {
if (!seriesFilter(series[i]))
continue;
var s = series[i],
axisx = s.xaxis,
axisy = s.yaxis,
points = s.datapoints.points,
mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
my = axisy.c2p(mouseY),
maxx = maxDistance / axisx.scale,
maxy = maxDistance / axisy.scale;
ps = s.datapoints.pointsize;
// with inverse transforms, we can't use the maxx/maxy
// optimization, sadly
if (axisx.options.inverseTransform)
maxx = Number.MAX_VALUE;
if (axisy.options.inverseTransform)
maxy = Number.MAX_VALUE;
if (s.lines.show || s.points.show) {
for (j = 0; j < points.length; j += ps) {
var x = points[j], y = points[j + 1];
if (x == null)
continue;
// For points and lines, the cursor must be within a
// certain distance to the data point
if (x - mx > maxx || x - mx < -maxx ||
y - my > maxy || y - my < -maxy)
continue;
// We have to calculate distances in pixels, not in
// data units, because the scales of the axes may be different
var dx = Math.abs(axisx.p2c(x) - mouseX),
dy = Math.abs(axisy.p2c(y) - mouseY),
dist = dx * dx + dy * dy; // we save the sqrt
// use <= to ensure last point takes precedence
// (last generally means on top of)
if (dist < smallestDistance) {
smallestDistance = dist;
item = [i, j / ps];
}
}
}
if (s.bars.show && !item) { // no other point can be nearby
var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2,
barRight = barLeft + s.bars.barWidth;
for (j = 0; j < points.length; j += ps) {
var x = points[j], y = points[j + 1], b = points[j + 2];
if (x == null)
continue;
// for a bar graph, the cursor must be inside the bar
if (series[i].bars.horizontal ?
(mx <= Math.max(b, x) && mx >= Math.min(b, x) &&
my >= y + barLeft && my <= y + barRight) :
(mx >= x + barLeft && mx <= x + barRight &&
my >= Math.min(b, y) && my <= Math.max(b, y)))
item = [i, j / ps];
}
}
}
if (item) {
i = item[0];
j = item[1];
ps = series[i].datapoints.pointsize;
return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
dataIndex: j,
series: series[i],
seriesIndex: i };
}
return null;
}
function onMouseMove(e) {
if (options.grid.hoverable)
triggerClickHoverEvent("plothover", e,
function (s) { return s["hoverable"] != false; });
}
function onMouseLeave(e) {
if (options.grid.hoverable)
triggerClickHoverEvent("plothover", e,
function (s) { return false; });
}
function onClick(e) {
triggerClickHoverEvent("plotclick", e,
function (s) { return s["clickable"] != false; });
}
// trigger click or hover event (they send the same parameters
// so we share their code)
function triggerClickHoverEvent(eventname, event, seriesFilter) {
var offset = eventHolder.offset(),
canvasX = event.pageX - offset.left - plotOffset.left,
canvasY = event.pageY - offset.top - plotOffset.top,
pos = canvasToAxisCoords({ left: canvasX, top: canvasY });
pos.pageX = event.pageX;
pos.pageY = event.pageY;
var item = findNearbyItem(canvasX, canvasY, seriesFilter);
if (item) {
// fill in mouse pos for any listeners out there
item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10);
item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10);
}
if (options.grid.autoHighlight) {
// clear auto-highlights
for (var i = 0; i < highlights.length; ++i) {
var h = highlights[i];
if (h.auto == eventname &&
!(item && h.series == item.series &&
h.point[0] == item.datapoint[0] &&
h.point[1] == item.datapoint[1]))
unhighlight(h.series, h.point);
}
if (item)
highlight(item.series, item.datapoint, eventname);
}
placeholder.trigger(eventname, [ pos, item ]);
}
function triggerRedrawOverlay() {
var t = options.interaction.redrawOverlayInterval;
if (t == -1) { // skip event queue
drawOverlay();
return;
}
if (!redrawTimeout)
redrawTimeout = setTimeout(drawOverlay, t);
}
function drawOverlay() {
redrawTimeout = null;
// draw highlights
octx.save();
overlay.clear();
octx.translate(plotOffset.left, plotOffset.top);
var i, hi;
for (i = 0; i < highlights.length; ++i) {
hi = highlights[i];
if (hi.series.bars.show)
drawBarHighlight(hi.series, hi.point);
else
drawPointHighlight(hi.series, hi.point);
}
octx.restore();
executeHooks(hooks.drawOverlay, [octx]);
}
function highlight(s, point, auto) {
if (typeof s == "number")
s = series[s];
if (typeof point == "number") {
var ps = s.datapoints.pointsize;
point = s.datapoints.points.slice(ps * point, ps * (point + 1));
}
var i = indexOfHighlight(s, point);
if (i == -1) {
highlights.push({ series: s, point: point, auto: auto });
triggerRedrawOverlay();
}
else if (!auto)
highlights[i].auto = false;
}
function unhighlight(s, point) {
if (s == null && point == null) {
highlights = [];
triggerRedrawOverlay();
return;
}
if (typeof s == "number")
s = series[s];
if (typeof point == "number") {
var ps = s.datapoints.pointsize;
point = s.datapoints.points.slice(ps * point, ps * (point + 1));
}
var i = indexOfHighlight(s, point);
if (i != -1) {
highlights.splice(i, 1);
triggerRedrawOverlay();
}
}
function indexOfHighlight(s, p) {
for (var i = 0; i < highlights.length; ++i) {
var h = highlights[i];
if (h.series == s && h.point[0] == p[0]
&& h.point[1] == p[1])
return i;
}
return -1;
}
function drawPointHighlight(series, point) {
var x = point[0], y = point[1],
axisx = series.xaxis, axisy = series.yaxis,
highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString();
if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
return;
var pointRadius = series.points.radius + series.points.lineWidth / 2;
octx.lineWidth = pointRadius;
octx.strokeStyle = highlightColor;
var radius = 1.5 * pointRadius;
x = axisx.p2c(x);
y = axisy.p2c(y);
octx.beginPath();
if (series.points.symbol == "circle")
octx.arc(x, y, radius, 0, 2 * Math.PI, false);
else
series.points.symbol(octx, x, y, radius, false);
octx.closePath();
octx.stroke();
}
function drawBarHighlight(series, point) {
var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(),
fillStyle = highlightColor,
barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
octx.lineWidth = series.bars.lineWidth;
octx.strokeStyle = highlightColor;
drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,
0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);
}
function getColorOrGradient(spec, bottom, top, defaultColor) {
if (typeof spec == "string")
return spec;
else {
// assume this is a gradient spec; IE currently only
// supports a simple vertical gradient properly, so that's
// what we support too
var gradient = ctx.createLinearGradient(0, top, 0, bottom);
for (var i = 0, l = spec.colors.length; i < l; ++i) {
var c = spec.colors[i];
if (typeof c != "string") {
var co = $.color.parse(defaultColor);
if (c.brightness != null)
co = co.scale('rgb', c.brightness);
if (c.opacity != null)
co.a *= c.opacity;
c = co.toString();
}
gradient.addColorStop(i / (l - 1), c);
}
return gradient;
}
}
}
// Add the plot function to the top level of the jQuery object
$.plot = function(placeholder, data, options) {
//var t0 = new Date();
var plot = new Plot($(placeholder), data, options, $.plot.plugins);
//(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime()));
return plot;
};
$.plot.version = "0.8.2-alpha";
$.plot.plugins = [];
// Also add the plot function as a chainable property
$.fn.plot = function(data, options) {
return this.each(function() {
$.plot(this, data, options);
});
};
// round to nearby lower multiple of base
function floorInBase(n, base) {
return base * Math.floor(n / base);
}
})(jQuery);
================================================
FILE: modules/backend/assets/foundation/migrate/vendor/flot/jquery.flot.navigate.js
================================================
/* Flot plugin for adding the ability to pan and zoom the plot.
Copyright (c) 2007-2013 IOLA and Ole Laursen.
Licensed under the MIT license.
The default behaviour is double click and scrollwheel up/down to zoom in, drag
to pan. The plugin defines plot.zoom({ center }), plot.zoomOut() and
plot.pan( offset ) so you easily can add custom controls. It also fires
"plotpan" and "plotzoom" events, useful for synchronizing plots.
The plugin supports these options:
zoom: {
interactive: false
trigger: "dblclick" // or "click" for single click
amount: 1.5 // 2 = 200% (zoom in), 0.5 = 50% (zoom out)
}
pan: {
interactive: false
cursor: "move" // CSS mouse cursor value used when dragging, e.g. "pointer"
frameRate: 20
}
xaxis, yaxis, x2axis, y2axis: {
zoomRange: null // or [ number, number ] (min range, max range) or false
panRange: null // or [ number, number ] (min, max) or false
}
"interactive" enables the built-in drag/click behaviour. If you enable
interactive for pan, then you'll have a basic plot that supports moving
around; the same for zoom.
"amount" specifies the default amount to zoom in (so 1.5 = 150%) relative to
the current viewport.
"cursor" is a standard CSS mouse cursor string used for visual feedback to the
user when dragging.
"frameRate" specifies the maximum number of times per second the plot will
update itself while the user is panning around on it (set to null to disable
intermediate pans, the plot will then not update until the mouse button is
released).
"zoomRange" is the interval in which zooming can happen, e.g. with zoomRange:
[1, 100] the zoom will never scale the axis so that the difference between min
and max is smaller than 1 or larger than 100. You can set either end to null
to ignore, e.g. [1, null]. If you set zoomRange to false, zooming on that axis
will be disabled.
"panRange" confines the panning to stay within a range, e.g. with panRange:
[-10, 20] panning stops at -10 in one end and at 20 in the other. Either can
be null, e.g. [-10, null]. If you set panRange to false, panning on that axis
will be disabled.
Example API usage:
plot = $.plot(...);
// zoom default amount in on the pixel ( 10, 20 )
plot.zoom({ center: { left: 10, top: 20 } });
// zoom out again
plot.zoomOut({ center: { left: 10, top: 20 } });
// zoom 200% in on the pixel (10, 20)
plot.zoom({ amount: 2, center: { left: 10, top: 20 } });
// pan 100 pixels to the left and 20 down
plot.pan({ left: -100, top: 20 })
Here, "center" specifies where the center of the zooming should happen. Note
that this is defined in pixel space, not the space of the data points (you can
use the p2c helpers on the axes in Flot to help you convert between these).
"amount" is the amount to zoom the viewport relative to the current range, so
1 is 100% (i.e. no change), 1.5 is 150% (zoom in), 0.7 is 70% (zoom out). You
can set the default in the options.
*/
// First two dependencies, jquery.event.drag.js and
// jquery.mousewheel.js, we put them inline here to save people the
// effort of downloading them.
/*
jquery.event.drag.js ~ v1.5 ~ Copyright (c) 2008, Three Dub Media (http://threedubmedia.com)
Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-LICENSE.txt
*/
(function(a){function e(h){var k,j=this,l=h.data||{};if(l.elem)j=h.dragTarget=l.elem,h.dragProxy=d.proxy||j,h.cursorOffsetX=l.pageX-l.left,h.cursorOffsetY=l.pageY-l.top,h.offsetX=h.pageX-h.cursorOffsetX,h.offsetY=h.pageY-h.cursorOffsetY;else if(d.dragging||l.which>0&&h.which!=l.which||a(h.target).is(l.not))return;switch(h.type){case"mousedown":return a.extend(l,a(j).offset(),{elem:j,target:h.target,pageX:h.pageX,pageY:h.pageY}),b.add(document,"mousemove mouseup",e,l),i(j,!1),d.dragging=null,!1;case!d.dragging&&"mousemove":if(g(h.pageX-l.pageX)+g(h.pageY-l.pageY) max) {
// make sure min < max
var tmp = min;
min = max;
max = tmp;
}
//Check that we are in panRange
if (pr) {
if (pr[0] != null && min < pr[0]) {
min = pr[0];
}
if (pr[1] != null && max > pr[1]) {
max = pr[1];
}
}
var range = max - min;
if (zr &&
((zr[0] != null && range < zr[0]) ||
(zr[1] != null && range > zr[1])))
return;
opts.min = min;
opts.max = max;
});
plot.setupGrid();
plot.draw();
if (!args.preventEvent)
plot.getPlaceholder().trigger("plotzoom", [ plot, args ]);
};
plot.pan = function (args) {
var delta = {
x: +args.left,
y: +args.top
};
if (isNaN(delta.x))
delta.x = 0;
if (isNaN(delta.y))
delta.y = 0;
$.each(plot.getAxes(), function (_, axis) {
var opts = axis.options,
min, max, d = delta[axis.direction];
min = axis.c2p(axis.p2c(axis.min) + d),
max = axis.c2p(axis.p2c(axis.max) + d);
var pr = opts.panRange;
if (pr === false) // no panning on this axis
return;
if (pr) {
// check whether we hit the wall
if (pr[0] != null && pr[0] > min) {
d = pr[0] - min;
min += d;
max += d;
}
if (pr[1] != null && pr[1] < max) {
d = pr[1] - max;
min += d;
max += d;
}
}
opts.min = min;
opts.max = max;
});
plot.setupGrid();
plot.draw();
if (!args.preventEvent)
plot.getPlaceholder().trigger("plotpan", [ plot, args ]);
};
function shutdown(plot, eventHolder) {
eventHolder.unbind(plot.getOptions().zoom.trigger, onZoomClick);
eventHolder.unbind("mousewheel", onMouseWheel);
eventHolder.unbind("dragstart", onDragStart);
eventHolder.unbind("drag", onDrag);
eventHolder.unbind("dragend", onDragEnd);
if (panTimeout)
clearTimeout(panTimeout);
}
plot.hooks.bindEvents.push(bindEvents);
plot.hooks.shutdown.push(shutdown);
}
$.plot.plugins.push({
init: init,
options: options,
name: 'navigate',
version: '1.3'
});
})(jQuery);
================================================
FILE: modules/backend/assets/foundation/migrate/vendor/flot/jquery.flot.pie.js
================================================
/* Flot plugin for rendering pie charts.
Copyright (c) 2007-2013 IOLA and Ole Laursen.
Licensed under the MIT license.
The plugin assumes that each series has a single data value, and that each
value is a positive integer or zero. Negative numbers don't make sense for a
pie chart, and have unpredictable results. The values do NOT need to be
passed in as percentages; the plugin will calculate the total and per-slice
percentages internally.
* Created by Brian Medendorp
* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars
The plugin supports these options:
series: {
pie: {
show: true/false
radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto'
innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect
startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result
tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show)
offset: {
top: integer value to move the pie up or down
left: integer value to move the pie left or right, or 'auto'
},
stroke: {
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#FFF')
width: integer pixel width of the stroke
},
label: {
show: true/false, or 'auto'
formatter: a user-defined function that modifies the text/style of the label text
radius: 0-1 for percentage of fullsize, or a specified pixel length
background: {
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#000')
opacity: 0-1
},
threshold: 0-1 for the percentage value at which to hide labels (if they're too small)
},
combine: {
threshold: 0-1 for the percentage value at which to combine slices (if they're too small)
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined
label: any text value of what the combined slice should be labeled
}
highlight: {
opacity: 0-1
}
}
}
More detail and specific examples can be found in the included HTML file.
*/
(function($) {
// Maximum redraw attempts when fitting labels within the plot
var REDRAW_ATTEMPTS = 10;
// Factor by which to shrink the pie when fitting labels within the plot
var REDRAW_SHRINK = 0.95;
function init(plot) {
var canvas = null,
target = null,
maxRadius = null,
centerLeft = null,
centerTop = null,
processed = false,
ctx = null;
// interactive variables
var highlights = [];
// add hook to determine if pie plugin in enabled, and then perform necessary operations
plot.hooks.processOptions.push(function(plot, options) {
if (options.series.pie.show) {
options.grid.show = false;
// set labels.show
if (options.series.pie.label.show == "auto") {
if (options.legend.show) {
options.series.pie.label.show = false;
} else {
options.series.pie.label.show = true;
}
}
// set radius
if (options.series.pie.radius == "auto") {
if (options.series.pie.label.show) {
options.series.pie.radius = 3/4;
} else {
options.series.pie.radius = 1;
}
}
// ensure sane tilt
if (options.series.pie.tilt > 1) {
options.series.pie.tilt = 1;
} else if (options.series.pie.tilt < 0) {
options.series.pie.tilt = 0;
}
}
});
plot.hooks.bindEvents.push(function(plot, eventHolder) {
var options = plot.getOptions();
if (options.series.pie.show) {
if (options.grid.hoverable) {
eventHolder.unbind("mousemove").mousemove(onMouseMove);
}
if (options.grid.clickable) {
eventHolder.unbind("click").click(onClick);
}
}
});
plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) {
var options = plot.getOptions();
if (options.series.pie.show) {
processDatapoints(plot, series, data, datapoints);
}
});
plot.hooks.drawOverlay.push(function(plot, octx) {
var options = plot.getOptions();
if (options.series.pie.show) {
drawOverlay(plot, octx);
}
});
plot.hooks.draw.push(function(plot, newCtx) {
var options = plot.getOptions();
if (options.series.pie.show) {
draw(plot, newCtx);
}
});
function processDatapoints(plot, series, datapoints) {
if (!processed) {
processed = true;
canvas = plot.getCanvas();
target = $(canvas).parent();
options = plot.getOptions();
plot.setData(combine(plot.getData()));
}
}
function combine(data) {
var total = 0,
combined = 0,
numCombined = 0,
color = options.series.pie.combine.color,
newdata = [];
// Fix up the raw data from Flot, ensuring the data is numeric
for (var i = 0; i < data.length; ++i) {
var value = data[i].data;
// If the data is an array, we'll assume that it's a standard
// Flot x-y pair, and are concerned only with the second value.
// Note how we use the original array, rather than creating a
// new one; this is more efficient and preserves any extra data
// that the user may have stored in higher indexes.
if ($.isArray(value) && value.length == 1) {
value = value[0];
}
if ($.isArray(value)) {
// Equivalent to $.isNumeric() but compatible with jQuery < 1.7
if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) {
value[1] = +value[1];
} else {
value[1] = 0;
}
} else if (!isNaN(parseFloat(value)) && isFinite(value)) {
value = [1, +value];
} else {
value = [1, 0];
}
data[i].data = [value];
}
// Sum up all the slices, so we can calculate percentages for each
for (var i = 0; i < data.length; ++i) {
total += data[i].data[0][1];
}
// Count the number of slices with percentages below the combine
// threshold; if it turns out to be just one, we won't combine.
for (var i = 0; i < data.length; ++i) {
var value = data[i].data[0][1];
if (value / total <= options.series.pie.combine.threshold) {
combined += value;
numCombined++;
if (!color) {
color = data[i].color;
}
}
}
for (var i = 0; i < data.length; ++i) {
var value = data[i].data[0][1];
if (numCombined < 2 || value / total > options.series.pie.combine.threshold) {
newdata.push({
data: [[1, value]],
color: data[i].color,
label: data[i].label,
angle: value * Math.PI * 2 / total,
percent: value / (total / 100)
});
}
}
if (numCombined > 1) {
newdata.push({
data: [[1, combined]],
color: color,
label: options.series.pie.combine.label,
angle: combined * Math.PI * 2 / total,
percent: combined / (total / 100)
});
}
return newdata;
}
function draw(plot, newCtx) {
if (!target) {
return; // if no series were passed
}
var canvasWidth = plot.getPlaceholder().width(),
canvasHeight = plot.getPlaceholder().height(),
legendWidth = target.children().filter(".legend").children().width() || 0;
ctx = newCtx;
// WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE!
// When combining smaller slices into an 'other' slice, we need to
// add a new series. Since Flot gives plugins no way to modify the
// list of series, the pie plugin uses a hack where the first call
// to processDatapoints results in a call to setData with the new
// list of series, then subsequent processDatapoints do nothing.
// The plugin-global 'processed' flag is used to control this hack;
// it starts out false, and is set to true after the first call to
// processDatapoints.
// Unfortunately this turns future setData calls into no-ops; they
// call processDatapoints, the flag is true, and nothing happens.
// To fix this we'll set the flag back to false here in draw, when
// all series have been processed, so the next sequence of calls to
// processDatapoints once again starts out with a slice-combine.
// This is really a hack; in 0.9 we need to give plugins a proper
// way to modify series before any processing begins.
processed = false;
// calculate maximum radius and center point
maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2;
centerTop = canvasHeight / 2 + options.series.pie.offset.top;
centerLeft = canvasWidth / 2;
if (options.series.pie.offset.left == "auto") {
if (options.legend.position.match("w")) {
centerLeft += legendWidth / 2;
} else {
centerLeft -= legendWidth / 2;
}
} else {
centerLeft += options.series.pie.offset.left;
}
if (centerLeft < maxRadius) {
centerLeft = maxRadius;
} else if (centerLeft > canvasWidth - maxRadius) {
centerLeft = canvasWidth - maxRadius;
}
var slices = plot.getData(),
attempts = 0;
// Keep shrinking the pie's radius until drawPie returns true,
// indicating that all the labels fit, or we try too many times.
do {
if (attempts > 0) {
maxRadius *= REDRAW_SHRINK;
}
attempts += 1;
clear();
if (options.series.pie.tilt <= 0.8) {
drawShadow();
}
} while (!drawPie() && attempts < REDRAW_ATTEMPTS)
if (attempts >= REDRAW_ATTEMPTS) {
clear();
target.prepend("
Could not draw pie with labels contained inside canvas
");
}
if (plot.setSeries && plot.insertLegend) {
plot.setSeries(slices);
plot.insertLegend();
}
// we're actually done at this point, just defining internal functions at this point
function clear() {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
target.children().filter(".pieLabel, .pieLabelBackground").remove();
}
function drawShadow() {
var shadowLeft = options.series.pie.shadow.left;
var shadowTop = options.series.pie.shadow.top;
var edge = 10;
var alpha = options.series.pie.shadow.alpha;
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) {
return; // shadow would be outside canvas, so don't draw it
}
ctx.save();
ctx.translate(shadowLeft,shadowTop);
ctx.globalAlpha = alpha;
ctx.fillStyle = "#000";
// center and rotate to starting position
ctx.translate(centerLeft,centerTop);
ctx.scale(1, options.series.pie.tilt);
//radius -= edge;
for (var i = 1; i <= edge; i++) {
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2, false);
ctx.fill();
radius -= i;
}
ctx.restore();
}
function drawPie() {
var startAngle = Math.PI * options.series.pie.startAngle;
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
// center and rotate to starting position
ctx.save();
ctx.translate(centerLeft,centerTop);
ctx.scale(1, options.series.pie.tilt);
//ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera
// draw slices
ctx.save();
var currentAngle = startAngle;
for (var i = 0; i < slices.length; ++i) {
slices[i].startAngle = currentAngle;
drawSlice(slices[i].angle, slices[i].color, true);
}
ctx.restore();
// draw slice outlines
if (options.series.pie.stroke.width > 0) {
ctx.save();
ctx.lineWidth = options.series.pie.stroke.width;
currentAngle = startAngle;
for (var i = 0; i < slices.length; ++i) {
drawSlice(slices[i].angle, options.series.pie.stroke.color, false);
}
ctx.restore();
}
// draw donut hole
drawDonutHole(ctx);
ctx.restore();
// Draw the labels, returning true if they fit within the plot
if (options.series.pie.label.show) {
return drawLabels();
} else return true;
function drawSlice(angle, color, fill) {
if (angle <= 0 || isNaN(angle)) {
return;
}
if (fill) {
ctx.fillStyle = color;
} else {
ctx.strokeStyle = color;
ctx.lineJoin = "round";
}
ctx.beginPath();
if (Math.abs(angle - Math.PI * 2) > 0.000000001) {
ctx.moveTo(0, 0); // Center of the pie
}
//ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera
ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false);
ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false);
ctx.closePath();
//ctx.rotate(angle); // This doesn't work properly in Opera
currentAngle += angle;
if (fill) {
ctx.fill();
} else {
ctx.stroke();
}
}
function drawLabels() {
var currentAngle = startAngle;
var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius;
for (var i = 0; i < slices.length; ++i) {
if (slices[i].percent >= options.series.pie.label.threshold * 100) {
if (!drawLabel(slices[i], currentAngle, i)) {
return false;
}
}
currentAngle += slices[i].angle;
}
return true;
function drawLabel(slice, startAngle, index) {
if (slice.data[0][1] == 0) {
return true;
}
// format label text
var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter;
if (lf) {
text = lf(slice.label, slice);
} else {
text = slice.label;
}
if (plf) {
text = plf(text, slice);
}
var halfAngle = ((startAngle + slice.angle) + startAngle) / 2;
var x = centerLeft + Math.round(Math.cos(halfAngle) * radius);
var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt;
var html = "" + text + "";
target.append(html);
var label = target.children("#pieLabel" + index);
var labelTop = (y - label.height() / 2);
var labelLeft = (x - label.width() / 2);
label.css("top", labelTop);
label.css("left", labelLeft);
// check to make sure that the label is not outside the canvas
if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) {
return false;
}
if (options.series.pie.label.background.opacity != 0) {
// put in the transparent background separately to avoid blended labels and label boxes
var c = options.series.pie.label.background.color;
if (c == null) {
c = slice.color;
}
var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;";
$("")
.css("opacity", options.series.pie.label.background.opacity)
.insertBefore(label);
}
return true;
} // end individual label function
} // end drawLabels function
} // end drawPie function
} // end draw function
// Placed here because it needs to be accessed from multiple locations
function drawDonutHole(layer) {
if (options.series.pie.innerRadius > 0) {
// subtract the center
layer.save();
var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius;
layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color
layer.beginPath();
layer.fillStyle = options.series.pie.stroke.color;
layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false);
layer.fill();
layer.closePath();
layer.restore();
// add inner stroke
layer.save();
layer.beginPath();
layer.strokeStyle = options.series.pie.stroke.color;
layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false);
layer.stroke();
layer.closePath();
layer.restore();
// TODO: add extra shadow inside hole (with a mask) if the pie is tilted.
}
}
//-- Additional Interactive related functions --
function isPointInPoly(poly, pt) {
for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i)
((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1]))
&& (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0])
&& (c = !c);
return c;
}
function findNearbySlice(mouseX, mouseY) {
var slices = plot.getData(),
options = plot.getOptions(),
radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius,
x, y;
for (var i = 0; i < slices.length; ++i) {
var s = slices[i];
if (s.pie.show) {
ctx.save();
ctx.beginPath();
ctx.moveTo(0, 0); // Center of the pie
//ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here.
ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false);
ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false);
ctx.closePath();
x = mouseX - centerLeft;
y = mouseY - centerTop;
if (ctx.isPointInPath) {
if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) {
ctx.restore();
return {
datapoint: [s.percent, s.data],
dataIndex: 0,
series: s,
seriesIndex: i
};
}
} else {
// excanvas for IE doesn;t support isPointInPath, this is a workaround.
var p1X = radius * Math.cos(s.startAngle),
p1Y = radius * Math.sin(s.startAngle),
p2X = radius * Math.cos(s.startAngle + s.angle / 4),
p2Y = radius * Math.sin(s.startAngle + s.angle / 4),
p3X = radius * Math.cos(s.startAngle + s.angle / 2),
p3Y = radius * Math.sin(s.startAngle + s.angle / 2),
p4X = radius * Math.cos(s.startAngle + s.angle / 1.5),
p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5),
p5X = radius * Math.cos(s.startAngle + s.angle),
p5Y = radius * Math.sin(s.startAngle + s.angle),
arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]],
arrPoint = [x, y];
// TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt?
if (isPointInPoly(arrPoly, arrPoint)) {
ctx.restore();
return {
datapoint: [s.percent, s.data],
dataIndex: 0,
series: s,
seriesIndex: i
};
}
}
ctx.restore();
}
}
return null;
}
function onMouseMove(e) {
triggerClickHoverEvent("plothover", e);
}
function onClick(e) {
triggerClickHoverEvent("plotclick", e);
}
// trigger click or hover event (they send the same parameters so we share their code)
function triggerClickHoverEvent(eventname, e) {
var offset = plot.offset();
var canvasX = parseInt(e.pageX - offset.left);
var canvasY = parseInt(e.pageY - offset.top);
var item = findNearbySlice(canvasX, canvasY);
if (options.grid.autoHighlight) {
// clear auto-highlights
for (var i = 0; i < highlights.length; ++i) {
var h = highlights[i];
if (h.auto == eventname && !(item && h.series == item.series)) {
unhighlight(h.series);
}
}
}
// highlight the slice
if (item) {
highlight(item.series, eventname);
}
// trigger any hover bind events
var pos = { pageX: e.pageX, pageY: e.pageY };
target.trigger(eventname, [pos, item]);
}
function highlight(s, auto) {
//if (typeof s == "number") {
// s = series[s];
//}
var i = indexOfHighlight(s);
if (i == -1) {
highlights.push({ series: s, auto: auto });
plot.triggerRedrawOverlay();
} else if (!auto) {
highlights[i].auto = false;
}
}
function unhighlight(s) {
if (s == null) {
highlights = [];
plot.triggerRedrawOverlay();
}
//if (typeof s == "number") {
// s = series[s];
//}
var i = indexOfHighlight(s);
if (i != -1) {
highlights.splice(i, 1);
plot.triggerRedrawOverlay();
}
}
function indexOfHighlight(s) {
for (var i = 0; i < highlights.length; ++i) {
var h = highlights[i];
if (h.series == s)
return i;
}
return -1;
}
function drawOverlay(plot, octx) {
var options = plot.getOptions();
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
octx.save();
octx.translate(centerLeft, centerTop);
octx.scale(1, options.series.pie.tilt);
for (var i = 0; i < highlights.length; ++i) {
drawHighlight(highlights[i].series);
}
drawDonutHole(octx);
octx.restore();
function drawHighlight(series) {
if (series.angle <= 0 || isNaN(series.angle)) {
return;
}
//octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString();
octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor
octx.beginPath();
if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) {
octx.moveTo(0, 0); // Center of the pie
}
octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false);
octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false);
octx.closePath();
octx.fill();
}
}
} // end init (plugin body)
// define pie specific options and their default values
var options = {
series: {
pie: {
show: false,
radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value)
innerRadius: 0, /* for donut */
startAngle: 3/2,
tilt: 1,
shadow: {
left: 5, // shadow left offset
top: 15, // shadow top offset
alpha: 0.02 // shadow alpha
},
offset: {
top: 0,
left: "auto"
},
stroke: {
color: "#fff",
width: 1
},
label: {
show: "auto",
formatter: function(label, slice) {
return "
* * “Catmull-Rom curveto†is a not standard SVG command and added in 2.0 to make life easier.
* Note: there is a special case when path consist of just three commands: “M10,10R…zâ€. In this case path will smoothly connects to its beginning.
> Usage
| var c = paper.path("M10 10L90 90");
| // draw a diagonal line:
| // move to 10,10, line to 90,90
* For example of path strings, check out these icons: http://raphaeljs.com/icons/
\*/
paperproto.path = function (pathString) {
pathString && !R.is(pathString, string) && !R.is(pathString[0], array) && (pathString += E);
var out = R._engine.path(R.format[apply](R, arguments), this);
this.__set__ && this.__set__.push(out);
return out;
};
/*\
* Paper.image
[ method ]
**
* Embeds an image into the surface.
**
> Parameters
**
- src (string) URI of the source image
- x (number) x coordinate position
- y (number) y coordinate position
- width (number) width of the image
- height (number) height of the image
= (object) Raphaël element object with type “imageâ€
**
> Usage
| var c = paper.image("apple.png", 10, 10, 80, 80);
\*/
paperproto.image = function (src, x, y, w, h) {
var out = R._engine.image(this, src || "about:blank", x || 0, y || 0, w || 0, h || 0);
this.__set__ && this.__set__.push(out);
return out;
};
/*\
* Paper.text
[ method ]
**
* Draws a text string. If you need line breaks, put “\n†in the string.
**
> Parameters
**
- x (number) x coordinate position
- y (number) y coordinate position
- text (string) The text string to draw
= (object) Raphaël element object with type “textâ€
**
> Usage
| var t = paper.text(50, 50, "Raphaël\nkicks\nbutt!");
\*/
paperproto.text = function (x, y, text) {
var out = R._engine.text(this, x || 0, y || 0, Str(text));
this.__set__ && this.__set__.push(out);
return out;
};
/*\
* Paper.set
[ method ]
**
* Creates array-like object to keep and operate several elements at once.
* Warning: it doesn’t create any elements for itself in the page, it just groups existing elements.
* Sets act as pseudo elements — all methods available to an element can be used on a set.
= (object) array-like object that represents set of elements
**
> Usage
| var st = paper.set();
| st.push(
| paper.circle(10, 10, 5),
| paper.circle(30, 10, 5)
| );
| st.attr({fill: "red"}); // changes the fill of both circles
\*/
paperproto.set = function (itemsArray) {
!R.is(itemsArray, "array") && (itemsArray = Array.prototype.splice.call(arguments, 0, arguments.length));
var out = new Set(itemsArray);
this.__set__ && this.__set__.push(out);
out["paper"] = this;
out["type"] = "set";
return out;
};
/*\
* Paper.setStart
[ method ]
**
* Creates @Paper.set. All elements that will be created after calling this method and before calling
* @Paper.setFinish will be added to the set.
**
> Usage
| paper.setStart();
| paper.circle(10, 10, 5),
| paper.circle(30, 10, 5)
| var st = paper.setFinish();
| st.attr({fill: "red"}); // changes the fill of both circles
\*/
paperproto.setStart = function (set) {
this.__set__ = set || this.set();
};
/*\
* Paper.setFinish
[ method ]
**
* See @Paper.setStart. This method finishes catching and returns resulting set.
**
= (object) set
\*/
paperproto.setFinish = function (set) {
var out = this.__set__;
delete this.__set__;
return out;
};
/*\
* Paper.getSize
[ method ]
**
* Obtains current paper actual size.
**
= (object)
\*/
paperproto.getSize = function () {
var container = this.canvas.parentNode;
return {
width: container.offsetWidth,
height: container.offsetHeight
};
};
/*\
* Paper.setSize
[ method ]
**
* If you need to change dimensions of the canvas call this method
**
> Parameters
**
- width (number) new width of the canvas
- height (number) new height of the canvas
\*/
paperproto.setSize = function (width, height) {
return R._engine.setSize.call(this, width, height);
};
/*\
* Paper.setViewBox
[ method ]
**
* Sets the view box of the paper. Practically it gives you ability to zoom and pan whole paper surface by
* specifying new boundaries.
**
> Parameters
**
- x (number) new x position, default is `0`
- y (number) new y position, default is `0`
- w (number) new width of the canvas
- h (number) new height of the canvas
- fit (boolean) `true` if you want graphics to fit into new boundary box
\*/
paperproto.setViewBox = function (x, y, w, h, fit) {
return R._engine.setViewBox.call(this, x, y, w, h, fit);
};
/*\
* Paper.top
[ property ]
**
* Points to the topmost element on the paper
\*/
/*\
* Paper.bottom
[ property ]
**
* Points to the bottom element on the paper
\*/
paperproto.top = paperproto.bottom = null;
/*\
* Paper.raphael
[ property ]
**
* Points to the @Raphael object/function
\*/
paperproto.raphael = R;
var getOffset = function (elem) {
var box = elem.getBoundingClientRect(),
doc = elem.ownerDocument,
body = doc.body,
docElem = doc.documentElement,
clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0,
top = box.top + (g.win.pageYOffset || docElem.scrollTop || body.scrollTop ) - clientTop,
left = box.left + (g.win.pageXOffset || docElem.scrollLeft || body.scrollLeft) - clientLeft;
return {
y: top,
x: left
};
};
/*\
* Paper.getElementByPoint
[ method ]
**
* Returns you topmost element under given point.
**
= (object) Raphaël element object
> Parameters
**
- x (number) x coordinate from the top left corner of the window
- y (number) y coordinate from the top left corner of the window
> Usage
| paper.getElementByPoint(mouseX, mouseY).attr({stroke: "#f00"});
\*/
paperproto.getElementByPoint = function (x, y) {
var paper = this,
svg = paper.canvas,
target = g.doc.elementFromPoint(x, y);
if (g.win.opera && target.tagName == "svg") {
var so = getOffset(svg),
sr = svg.createSVGRect();
sr.x = x - so.x;
sr.y = y - so.y;
sr.width = sr.height = 1;
var hits = svg.getIntersectionList(sr, null);
if (hits.length) {
target = hits[hits.length - 1];
}
}
if (!target) {
return null;
}
while (target.parentNode && target != svg.parentNode && !target.raphael) {
target = target.parentNode;
}
target == paper.canvas.parentNode && (target = svg);
target = target && target.raphael ? paper.getById(target.raphaelid) : null;
return target;
};
/*\
* Paper.getElementsByBBox
[ method ]
**
* Returns set of elements that have an intersecting bounding box
**
> Parameters
**
- bbox (object) bbox to check with
= (object) @Set
\*/
paperproto.getElementsByBBox = function (bbox) {
var set = this.set();
this.forEach(function (el) {
if (R.isBBoxIntersect(el.getBBox(), bbox)) {
set.push(el);
}
});
return set;
};
/*\
* Paper.getById
[ method ]
**
* Returns you element by its internal ID.
**
> Parameters
**
- id (number) id
= (object) Raphaël element object
\*/
paperproto.getById = function (id) {
var bot = this.bottom;
while (bot) {
if (bot.id == id) {
return bot;
}
bot = bot.next;
}
return null;
};
/*\
* Paper.forEach
[ method ]
**
* Executes given function for each element on the paper
*
* If callback function returns `false` it will stop loop running.
**
> Parameters
**
- callback (function) function to run
- thisArg (object) context object for the callback
= (object) Paper object
> Usage
| paper.forEach(function (el) {
| el.attr({ stroke: "blue" });
| });
\*/
paperproto.forEach = function (callback, thisArg) {
var bot = this.bottom;
while (bot) {
if (callback.call(thisArg, bot) === false) {
return this;
}
bot = bot.next;
}
return this;
};
/*\
* Paper.getElementsByPoint
[ method ]
**
* Returns set of elements that have common point inside
**
> Parameters
**
- x (number) x coordinate of the point
- y (number) y coordinate of the point
= (object) @Set
\*/
paperproto.getElementsByPoint = function (x, y) {
var set = this.set();
this.forEach(function (el) {
if (el.isPointInside(x, y)) {
set.push(el);
}
});
return set;
};
function x_y() {
return this.x + S + this.y;
}
function x_y_w_h() {
return this.x + S + this.y + S + this.width + " \xd7 " + this.height;
}
/*\
* Element.isPointInside
[ method ]
**
* Determine if given point is inside this element’s shape
**
> Parameters
**
- x (number) x coordinate of the point
- y (number) y coordinate of the point
= (boolean) `true` if point inside the shape
\*/
elproto.isPointInside = function (x, y) {
var rp = this.realPath = getPath[this.type](this);
if (this.attr('transform') && this.attr('transform').length) {
rp = R.transformPath(rp, this.attr('transform'));
}
return R.isPointInsidePath(rp, x, y);
};
/*\
* Element.getBBox
[ method ]
**
* Return bounding box for a given element
**
> Parameters
**
- isWithoutTransform (boolean) flag, `true` if you want to have bounding box before transformations. Default is `false`.
= (object) Bounding box object:
o {
o x: (number) top left corner x
o y: (number) top left corner y
o x2: (number) bottom right corner x
o y2: (number) bottom right corner y
o width: (number) width
o height: (number) height
o }
\*/
elproto.getBBox = function (isWithoutTransform) {
if (this.removed) {
return {};
}
var _ = this._;
if (isWithoutTransform) {
if (_.dirty || !_.bboxwt) {
this.realPath = getPath[this.type](this);
_.bboxwt = pathDimensions(this.realPath);
_.bboxwt.toString = x_y_w_h;
_.dirty = 0;
}
return _.bboxwt;
}
if (_.dirty || _.dirtyT || !_.bbox) {
if (_.dirty || !this.realPath) {
_.bboxwt = 0;
this.realPath = getPath[this.type](this);
}
_.bbox = pathDimensions(mapPath(this.realPath, this.matrix));
_.bbox.toString = x_y_w_h;
_.dirty = _.dirtyT = 0;
}
return _.bbox;
};
/*\
* Element.clone
[ method ]
**
= (object) clone of a given element
**
\*/
elproto.clone = function () {
if (this.removed) {
return null;
}
var out = this.paper[this.type]().attr(this.attr());
this.__set__ && this.__set__.push(out);
return out;
};
/*\
* Element.glow
[ method ]
**
* Return set of elements that create glow-like effect around given element. See @Paper.set.
*
* Note: Glow is not connected to the element. If you change element attributes it won’t adjust itself.
**
> Parameters
**
- glow (object) #optional parameters object with all properties optional:
o {
o width (number) size of the glow, default is `10`
o fill (boolean) will it be filled, default is `false`
o opacity (number) opacity, default is `0.5`
o offsetx (number) horizontal offset, default is `0`
o offsety (number) vertical offset, default is `0`
o color (string) glow colour, default is `black`
o }
= (object) @Paper.set of elements that represents glow
\*/
elproto.glow = function (glow) {
if (this.type == "text") {
return null;
}
glow = glow || {};
var s = {
width: (glow.width || 10) + (+this.attr("stroke-width") || 1),
fill: glow.fill || false,
opacity: glow.opacity == null ? .5 : glow.opacity,
offsetx: glow.offsetx || 0,
offsety: glow.offsety || 0,
color: glow.color || "#000"
},
c = s.width / 2,
r = this.paper,
out = r.set(),
path = this.realPath || getPath[this.type](this);
path = this.matrix ? mapPath(path, this.matrix) : path;
for (var i = 1; i < c + 1; i++) {
out.push(r.path(path).attr({
stroke: s.color,
fill: s.fill ? s.color : "none",
"stroke-linejoin": "round",
"stroke-linecap": "round",
"stroke-width": +(s.width / c * i).toFixed(3),
opacity: +(s.opacity / c).toFixed(3)
}));
}
return out.insertBefore(this).translate(s.offsetx, s.offsety);
};
var curveslengths = {},
getPointAtSegmentLength = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, length) {
if (length == null) {
return bezlen(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y);
} else {
return R.findDotsAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, getTatLen(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, length));
}
},
getLengthFactory = function (istotal, subpath) {
return function (path, length, onlystart) {
path = path2curve(path);
var x, y, p, l, sp = "", subpaths = {}, point,
len = 0;
for (var i = 0, ii = path.length; i < ii; i++) {
p = path[i];
if (p[0] == "M") {
x = +p[1];
y = +p[2];
} else {
l = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6]);
if (len + l > length) {
if (subpath && !subpaths.start) {
point = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6], length - len);
sp += ["C" + point.start.x, point.start.y, point.m.x, point.m.y, point.x, point.y];
if (onlystart) {return sp;}
subpaths.start = sp;
sp = ["M" + point.x, point.y + "C" + point.n.x, point.n.y, point.end.x, point.end.y, p[5], p[6]].join();
len += l;
x = +p[5];
y = +p[6];
continue;
}
if (!istotal && !subpath) {
point = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6], length - len);
return {x: point.x, y: point.y, alpha: point.alpha};
}
}
len += l;
x = +p[5];
y = +p[6];
}
sp += p.shift() + p;
}
subpaths.end = sp;
point = istotal ? len : subpath ? subpaths : R.findDotsAtSegment(x, y, p[0], p[1], p[2], p[3], p[4], p[5], 1);
point.alpha && (point = {x: point.x, y: point.y, alpha: point.alpha});
return point;
};
};
var getTotalLength = getLengthFactory(1),
getPointAtLength = getLengthFactory(),
getSubpathsAtLength = getLengthFactory(0, 1);
/*\
* Raphael.getTotalLength
[ method ]
**
* Returns length of the given path in pixels.
**
> Parameters
**
- path (string) SVG path string.
**
= (number) length.
\*/
R.getTotalLength = getTotalLength;
/*\
* Raphael.getPointAtLength
[ method ]
**
* Return coordinates of the point located at the given length on the given path.
**
> Parameters
**
- path (string) SVG path string
- length (number)
**
= (object) representation of the point:
o {
o x: (number) x coordinate
o y: (number) y coordinate
o alpha: (number) angle of derivative
o }
\*/
R.getPointAtLength = getPointAtLength;
/*\
* Raphael.getSubpath
[ method ]
**
* Return subpath of a given path from given length to given length.
**
> Parameters
**
- path (string) SVG path string
- from (number) position of the start of the segment
- to (number) position of the end of the segment
**
= (string) pathstring for the segment
\*/
R.getSubpath = function (path, from, to) {
if (this.getTotalLength(path) - to < 1e-6) {
return getSubpathsAtLength(path, from).end;
}
var a = getSubpathsAtLength(path, to, 1);
return from ? getSubpathsAtLength(a, from).end : a;
};
/*\
* Element.getTotalLength
[ method ]
**
* Returns length of the path in pixels. Only works for element of “path†type.
= (number) length.
\*/
elproto.getTotalLength = function () {
var path = this.getPath();
if (!path) {
return;
}
if (this.node.getTotalLength) {
return this.node.getTotalLength();
}
return getTotalLength(path);
};
/*\
* Element.getPointAtLength
[ method ]
**
* Return coordinates of the point located at the given length on the given path. Only works for element of “path†type.
**
> Parameters
**
- length (number)
**
= (object) representation of the point:
o {
o x: (number) x coordinate
o y: (number) y coordinate
o alpha: (number) angle of derivative
o }
\*/
elproto.getPointAtLength = function (length) {
var path = this.getPath();
if (!path) {
return;
}
return getPointAtLength(path, length);
};
/*\
* Element.getPath
[ method ]
**
* Returns path of the element. Only works for elements of “path†type and simple elements like circle.
= (object) path
**
\*/
elproto.getPath = function () {
var path,
getPath = R._getPath[this.type];
if (this.type == "text" || this.type == "set") {
return;
}
if (getPath) {
path = getPath(this);
}
return path;
};
/*\
* Element.getSubpath
[ method ]
**
* Return subpath of a given element from given length to given length. Only works for element of “path†type.
**
> Parameters
**
- from (number) position of the start of the segment
- to (number) position of the end of the segment
**
= (string) pathstring for the segment
\*/
elproto.getSubpath = function (from, to) {
var path = this.getPath();
if (!path) {
return;
}
return R.getSubpath(path, from, to);
};
/*\
* Raphael.easing_formulas
[ property ]
**
* Object that contains easing formulas for animation. You could extend it with your own. By default it has following list of easing:
#
\*/
var ef = R.easing_formulas = {
linear: function (n) {
return n;
},
"<": function (n) {
return pow(n, 1.7);
},
">": function (n) {
return pow(n, .48);
},
"<>": function (n) {
var q = .48 - n / 1.04,
Q = math.sqrt(.1734 + q * q),
x = Q - q,
X = pow(abs(x), 1 / 3) * (x < 0 ? -1 : 1),
y = -Q - q,
Y = pow(abs(y), 1 / 3) * (y < 0 ? -1 : 1),
t = X + Y + .5;
return (1 - t) * 3 * t * t + t * t * t;
},
backIn: function (n) {
var s = 1.70158;
return n * n * ((s + 1) * n - s);
},
backOut: function (n) {
n = n - 1;
var s = 1.70158;
return n * n * ((s + 1) * n + s) + 1;
},
elastic: function (n) {
if (n == !!n) {
return n;
}
return pow(2, -10 * n) * math.sin((n - .075) * (2 * PI) / .3) + 1;
},
bounce: function (n) {
var s = 7.5625,
p = 2.75,
l;
if (n < (1 / p)) {
l = s * n * n;
} else {
if (n < (2 / p)) {
n -= (1.5 / p);
l = s * n * n + .75;
} else {
if (n < (2.5 / p)) {
n -= (2.25 / p);
l = s * n * n + .9375;
} else {
n -= (2.625 / p);
l = s * n * n + .984375;
}
}
}
return l;
}
};
ef.easeIn = ef["ease-in"] = ef["<"];
ef.easeOut = ef["ease-out"] = ef[">"];
ef.easeInOut = ef["ease-in-out"] = ef["<>"];
ef["back-in"] = ef.backIn;
ef["back-out"] = ef.backOut;
var animationElements = [],
requestAnimFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
setTimeout(callback, 16);
},
animation = function () {
var Now = +new Date,
l = 0;
for (; l < animationElements.length; l++) {
var e = animationElements[l];
if (e.el.removed || e.paused) {
continue;
}
var time = Now - e.start,
ms = e.ms,
easing = e.easing,
from = e.from,
diff = e.diff,
to = e.to,
t = e.t,
that = e.el,
set = {},
now,
init = {},
key;
if (e.initstatus) {
time = (e.initstatus * e.anim.top - e.prev) / (e.percent - e.prev) * ms;
e.status = e.initstatus;
delete e.initstatus;
e.stop && animationElements.splice(l--, 1);
} else {
e.status = (e.prev + (e.percent - e.prev) * (time / ms)) / e.anim.top;
}
if (time < 0) {
continue;
}
if (time < ms) {
var pos = easing(time / ms);
for (var attr in from) if (from[has](attr)) {
switch (availableAnimAttrs[attr]) {
case nu:
now = +from[attr] + pos * ms * diff[attr];
break;
case "colour":
now = "rgb(" + [
upto255(round(from[attr].r + pos * ms * diff[attr].r)),
upto255(round(from[attr].g + pos * ms * diff[attr].g)),
upto255(round(from[attr].b + pos * ms * diff[attr].b))
].join(",") + ")";
break;
case "path":
now = [];
for (var i = 0, ii = from[attr].length; i < ii; i++) {
now[i] = [from[attr][i][0]];
for (var j = 1, jj = from[attr][i].length; j < jj; j++) {
now[i][j] = +from[attr][i][j] + pos * ms * diff[attr][i][j];
}
now[i] = now[i].join(S);
}
now = now.join(S);
break;
case "transform":
if (diff[attr].real) {
now = [];
for (i = 0, ii = from[attr].length; i < ii; i++) {
now[i] = [from[attr][i][0]];
for (j = 1, jj = from[attr][i].length; j < jj; j++) {
now[i][j] = from[attr][i][j] + pos * ms * diff[attr][i][j];
}
}
} else {
var get = function (i) {
return +from[attr][i] + pos * ms * diff[attr][i];
};
// now = [["r", get(2), 0, 0], ["t", get(3), get(4)], ["s", get(0), get(1), 0, 0]];
now = [["m", get(0), get(1), get(2), get(3), get(4), get(5)]];
}
break;
case "csv":
if (attr == "clip-rect") {
now = [];
i = 4;
while (i--) {
now[i] = +from[attr][i] + pos * ms * diff[attr][i];
}
}
break;
default:
var from2 = [][concat](from[attr]);
now = [];
i = that.paper.customAttributes[attr].length;
while (i--) {
now[i] = +from2[i] + pos * ms * diff[attr][i];
}
break;
}
set[attr] = now;
}
that.attr(set);
(function (id, that, anim) {
setTimeout(function () {
eve("raphael.anim.frame." + id, that, anim);
});
})(that.id, that, e.anim);
} else {
(function(f, el, a) {
setTimeout(function() {
eve("raphael.anim.frame." + el.id, el, a);
eve("raphael.anim.finish." + el.id, el, a);
R.is(f, "function") && f.call(el);
});
})(e.callback, that, e.anim);
that.attr(to);
animationElements.splice(l--, 1);
if (e.repeat > 1 && !e.next) {
for (key in to) if (to[has](key)) {
init[key] = e.totalOrigin[key];
}
e.el.attr(init);
runAnimation(e.anim, e.el, e.anim.percents[0], null, e.totalOrigin, e.repeat - 1);
}
if (e.next && !e.stop) {
runAnimation(e.anim, e.el, e.next, null, e.totalOrigin, e.repeat);
}
}
}
animationElements.length && requestAnimFrame(animation);
},
upto255 = function (color) {
return color > 255 ? 255 : color < 0 ? 0 : color;
};
/*\
* Element.animateWith
[ method ]
**
* Acts similar to @Element.animate, but ensure that given animation runs in sync with another given element.
**
> Parameters
**
- el (object) element to sync with
- anim (object) animation to sync with
- params (object) #optional final attributes for the element, see also @Element.attr
- ms (number) #optional number of milliseconds for animation to run
- easing (string) #optional easing type. Accept on of @Raphael.easing_formulas or CSS format: `cubic‐bezier(XX, XX, XX, XX)`
- callback (function) #optional callback function. Will be called at the end of animation.
* or
- element (object) element to sync with
- anim (object) animation to sync with
- animation (object) #optional animation object, see @Raphael.animation
**
= (object) original element
\*/
elproto.animateWith = function (el, anim, params, ms, easing, callback) {
var element = this;
if (element.removed) {
callback && callback.call(element);
return element;
}
var a = params instanceof Animation ? params : R.animation(params, ms, easing, callback),
x, y;
runAnimation(a, element, a.percents[0], null, element.attr());
for (var i = 0, ii = animationElements.length; i < ii; i++) {
if (animationElements[i].anim == anim && animationElements[i].el == el) {
animationElements[ii - 1].start = animationElements[i].start;
break;
}
}
return element;
//
//
// var a = params ? R.animation(params, ms, easing, callback) : anim,
// status = element.status(anim);
// return this.animate(a).status(a, status * anim.ms / a.ms);
};
function CubicBezierAtTime(t, p1x, p1y, p2x, p2y, duration) {
var cx = 3 * p1x,
bx = 3 * (p2x - p1x) - cx,
ax = 1 - cx - bx,
cy = 3 * p1y,
by = 3 * (p2y - p1y) - cy,
ay = 1 - cy - by;
function sampleCurveX(t) {
return ((ax * t + bx) * t + cx) * t;
}
function solve(x, epsilon) {
var t = solveCurveX(x, epsilon);
return ((ay * t + by) * t + cy) * t;
}
function solveCurveX(x, epsilon) {
var t0, t1, t2, x2, d2, i;
for(t2 = x, i = 0; i < 8; i++) {
x2 = sampleCurveX(t2) - x;
if (abs(x2) < epsilon) {
return t2;
}
d2 = (3 * ax * t2 + 2 * bx) * t2 + cx;
if (abs(d2) < 1e-6) {
break;
}
t2 = t2 - x2 / d2;
}
t0 = 0;
t1 = 1;
t2 = x;
if (t2 < t0) {
return t0;
}
if (t2 > t1) {
return t1;
}
while (t0 < t1) {
x2 = sampleCurveX(t2);
if (abs(x2 - x) < epsilon) {
return t2;
}
if (x > x2) {
t0 = t2;
} else {
t1 = t2;
}
t2 = (t1 - t0) / 2 + t0;
}
return t2;
}
return solve(t, 1 / (200 * duration));
}
elproto.onAnimation = function (f) {
f ? eve.on("raphael.anim.frame." + this.id, f) : eve.unbind("raphael.anim.frame." + this.id);
return this;
};
function Animation(anim, ms) {
var percents = [],
newAnim = {};
this.ms = ms;
this.times = 1;
if (anim) {
for (var attr in anim) if (anim[has](attr)) {
newAnim[toFloat(attr)] = anim[attr];
percents.push(toFloat(attr));
}
percents.sort(sortByNumber);
}
this.anim = newAnim;
this.top = percents[percents.length - 1];
this.percents = percents;
}
/*\
* Animation.delay
[ method ]
**
* Creates a copy of existing animation object with given delay.
**
> Parameters
**
- delay (number) number of ms to pass between animation start and actual animation
**
= (object) new altered Animation object
| var anim = Raphael.animation({cx: 10, cy: 20}, 2e3);
| circle1.animate(anim); // run the given animation immediately
| circle2.animate(anim.delay(500)); // run the given animation after 500 ms
\*/
Animation.prototype.delay = function (delay) {
var a = new Animation(this.anim, this.ms);
a.times = this.times;
a.del = +delay || 0;
return a;
};
/*\
* Animation.repeat
[ method ]
**
* Creates a copy of existing animation object with given repetition.
**
> Parameters
**
- repeat (number) number iterations of animation. For infinite animation pass `Infinity`
**
= (object) new altered Animation object
\*/
Animation.prototype.repeat = function (times) {
var a = new Animation(this.anim, this.ms);
a.del = this.del;
a.times = math.floor(mmax(times, 0)) || 1;
return a;
};
function runAnimation(anim, element, percent, status, totalOrigin, times) {
percent = toFloat(percent);
var params,
isInAnim,
isInAnimSet,
percents = [],
next,
prev,
timestamp,
ms = anim.ms,
from = {},
to = {},
diff = {};
if (status) {
for (i = 0, ii = animationElements.length; i < ii; i++) {
var e = animationElements[i];
if (e.el.id == element.id && e.anim == anim) {
if (e.percent != percent) {
animationElements.splice(i, 1);
isInAnimSet = 1;
} else {
isInAnim = e;
}
element.attr(e.totalOrigin);
break;
}
}
} else {
status = +to; // NaN
}
for (var i = 0, ii = anim.percents.length; i < ii; i++) {
if (anim.percents[i] == percent || anim.percents[i] > status * anim.top) {
percent = anim.percents[i];
prev = anim.percents[i - 1] || 0;
ms = ms / anim.top * (percent - prev);
next = anim.percents[i + 1];
params = anim.anim[percent];
break;
} else if (status) {
element.attr(anim.anim[anim.percents[i]]);
}
}
if (!params) {
return;
}
if (!isInAnim) {
for (var attr in params) if (params[has](attr)) {
if (availableAnimAttrs[has](attr) || element.paper.customAttributes[has](attr)) {
from[attr] = element.attr(attr);
(from[attr] == null) && (from[attr] = availableAttrs[attr]);
to[attr] = params[attr];
switch (availableAnimAttrs[attr]) {
case nu:
diff[attr] = (to[attr] - from[attr]) / ms;
break;
case "colour":
from[attr] = R.getRGB(from[attr]);
var toColour = R.getRGB(to[attr]);
diff[attr] = {
r: (toColour.r - from[attr].r) / ms,
g: (toColour.g - from[attr].g) / ms,
b: (toColour.b - from[attr].b) / ms
};
break;
case "path":
var pathes = path2curve(from[attr], to[attr]),
toPath = pathes[1];
from[attr] = pathes[0];
diff[attr] = [];
for (i = 0, ii = from[attr].length; i < ii; i++) {
diff[attr][i] = [0];
for (var j = 1, jj = from[attr][i].length; j < jj; j++) {
diff[attr][i][j] = (toPath[i][j] - from[attr][i][j]) / ms;
}
}
break;
case "transform":
var _ = element._,
eq = equaliseTransform(_[attr], to[attr]);
if (eq) {
from[attr] = eq.from;
to[attr] = eq.to;
diff[attr] = [];
diff[attr].real = true;
for (i = 0, ii = from[attr].length; i < ii; i++) {
diff[attr][i] = [from[attr][i][0]];
for (j = 1, jj = from[attr][i].length; j < jj; j++) {
diff[attr][i][j] = (to[attr][i][j] - from[attr][i][j]) / ms;
}
}
} else {
var m = (element.matrix || new Matrix),
to2 = {
_: {transform: _.transform},
getBBox: function () {
return element.getBBox(1);
}
};
from[attr] = [
m.a,
m.b,
m.c,
m.d,
m.e,
m.f
];
extractTransform(to2, to[attr]);
to[attr] = to2._.transform;
diff[attr] = [
(to2.matrix.a - m.a) / ms,
(to2.matrix.b - m.b) / ms,
(to2.matrix.c - m.c) / ms,
(to2.matrix.d - m.d) / ms,
(to2.matrix.e - m.e) / ms,
(to2.matrix.f - m.f) / ms
];
// from[attr] = [_.sx, _.sy, _.deg, _.dx, _.dy];
// var to2 = {_:{}, getBBox: function () { return element.getBBox(); }};
// extractTransform(to2, to[attr]);
// diff[attr] = [
// (to2._.sx - _.sx) / ms,
// (to2._.sy - _.sy) / ms,
// (to2._.deg - _.deg) / ms,
// (to2._.dx - _.dx) / ms,
// (to2._.dy - _.dy) / ms
// ];
}
break;
case "csv":
var values = Str(params[attr])[split](separator),
from2 = Str(from[attr])[split](separator);
if (attr == "clip-rect") {
from[attr] = from2;
diff[attr] = [];
i = from2.length;
while (i--) {
diff[attr][i] = (values[i] - from[attr][i]) / ms;
}
}
to[attr] = values;
break;
default:
values = [][concat](params[attr]);
from2 = [][concat](from[attr]);
diff[attr] = [];
i = element.paper.customAttributes[attr].length;
while (i--) {
diff[attr][i] = ((values[i] || 0) - (from2[i] || 0)) / ms;
}
break;
}
}
}
var easing = params.easing,
easyeasy = R.easing_formulas[easing];
if (!easyeasy) {
easyeasy = Str(easing).match(bezierrg);
if (easyeasy && easyeasy.length == 5) {
var curve = easyeasy;
easyeasy = function (t) {
return CubicBezierAtTime(t, +curve[1], +curve[2], +curve[3], +curve[4], ms);
};
} else {
easyeasy = pipe;
}
}
timestamp = params.start || anim.start || +new Date;
e = {
anim: anim,
percent: percent,
timestamp: timestamp,
start: timestamp + (anim.del || 0),
status: 0,
initstatus: status || 0,
stop: false,
ms: ms,
easing: easyeasy,
from: from,
diff: diff,
to: to,
el: element,
callback: params.callback,
prev: prev,
next: next,
repeat: times || anim.times,
origin: element.attr(),
totalOrigin: totalOrigin
};
animationElements.push(e);
if (status && !isInAnim && !isInAnimSet) {
e.stop = true;
e.start = new Date - ms * status;
if (animationElements.length == 1) {
return animation();
}
}
if (isInAnimSet) {
e.start = new Date - e.ms * status;
}
animationElements.length == 1 && requestAnimFrame(animation);
} else {
isInAnim.initstatus = status;
isInAnim.start = new Date - isInAnim.ms * status;
}
eve("raphael.anim.start." + element.id, element, anim);
}
/*\
* Raphael.animation
[ method ]
**
* Creates an animation object that can be passed to the @Element.animate or @Element.animateWith methods.
* See also @Animation.delay and @Animation.repeat methods.
**
> Parameters
**
- params (object) final attributes for the element, see also @Element.attr
- ms (number) number of milliseconds for animation to run
- easing (string) #optional easing type. Accept one of @Raphael.easing_formulas or CSS format: `cubic‐bezier(XX, XX, XX, XX)`
- callback (function) #optional callback function. Will be called at the end of animation.
**
= (object) @Animation
\*/
R.animation = function (params, ms, easing, callback) {
if (params instanceof Animation) {
return params;
}
if (R.is(easing, "function") || !easing) {
callback = callback || easing || null;
easing = null;
}
params = Object(params);
ms = +ms || 0;
var p = {},
json,
attr;
for (attr in params) if (params[has](attr) && toFloat(attr) != attr && toFloat(attr) + "%" != attr) {
json = true;
p[attr] = params[attr];
}
if (!json) {
// if percent-like syntax is used and end-of-all animation callback used
if(callback){
// find the last one
var lastKey = 0;
for(var i in params){
var percent = toInt(i);
if(params[has](i) && percent > lastKey){
lastKey = percent;
}
}
lastKey += '%';
// if already defined callback in the last keyframe, skip
!params[lastKey].callback && (params[lastKey].callback = callback);
}
return new Animation(params, ms);
} else {
easing && (p.easing = easing);
callback && (p.callback = callback);
return new Animation({100: p}, ms);
}
};
/*\
* Element.animate
[ method ]
**
* Creates and starts animation for given element.
**
> Parameters
**
- params (object) final attributes for the element, see also @Element.attr
- ms (number) number of milliseconds for animation to run
- easing (string) #optional easing type. Accept one of @Raphael.easing_formulas or CSS format: `cubic‐bezier(XX, XX, XX, XX)`
- callback (function) #optional callback function. Will be called at the end of animation.
* or
- animation (object) animation object, see @Raphael.animation
**
= (object) original element
\*/
elproto.animate = function (params, ms, easing, callback) {
var element = this;
if (element.removed) {
callback && callback.call(element);
return element;
}
var anim = params instanceof Animation ? params : R.animation(params, ms, easing, callback);
runAnimation(anim, element, anim.percents[0], null, element.attr());
return element;
};
/*\
* Element.setTime
[ method ]
**
* Sets the status of animation of the element in milliseconds. Similar to @Element.status method.
**
> Parameters
**
- anim (object) animation object
- value (number) number of milliseconds from the beginning of the animation
**
= (object) original element if `value` is specified
* Note, that during animation following events are triggered:
*
* On each animation frame event `anim.frame.`, on start `anim.start.` and on end `anim.finish.`.
\*/
elproto.setTime = function (anim, value) {
if (anim && value != null) {
this.status(anim, mmin(value, anim.ms) / anim.ms);
}
return this;
};
/*\
* Element.status
[ method ]
**
* Gets or sets the status of animation of the element.
**
> Parameters
**
- anim (object) #optional animation object
- value (number) #optional 0 – 1. If specified, method works like a setter and sets the status of a given animation to the value. This will cause animation to jump to the given position.
**
= (number) status
* or
= (array) status if `anim` is not specified. Array of objects in format:
o {
o anim: (object) animation object
o status: (number) status
o }
* or
= (object) original element if `value` is specified
\*/
elproto.status = function (anim, value) {
var out = [],
i = 0,
len,
e;
if (value != null) {
runAnimation(anim, this, -1, mmin(value, 1));
return this;
} else {
len = animationElements.length;
for (; i < len; i++) {
e = animationElements[i];
if (e.el.id == this.id && (!anim || e.anim == anim)) {
if (anim) {
return e.status;
}
out.push({
anim: e.anim,
status: e.status
});
}
}
if (anim) {
return 0;
}
return out;
}
};
/*\
* Element.pause
[ method ]
**
* Stops animation of the element with ability to resume it later on.
**
> Parameters
**
- anim (object) #optional animation object
**
= (object) original element
\*/
elproto.pause = function (anim) {
for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) {
if (eve("raphael.anim.pause." + this.id, this, animationElements[i].anim) !== false) {
animationElements[i].paused = true;
}
}
return this;
};
/*\
* Element.resume
[ method ]
**
* Resumes animation if it was paused with @Element.pause method.
**
> Parameters
**
- anim (object) #optional animation object
**
= (object) original element
\*/
elproto.resume = function (anim) {
for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) {
var e = animationElements[i];
if (eve("raphael.anim.resume." + this.id, this, e.anim) !== false) {
delete e.paused;
this.status(e.anim, e.status);
}
}
return this;
};
/*\
* Element.stop
[ method ]
**
* Stops animation of the element.
**
> Parameters
**
- anim (object) #optional animation object
**
= (object) original element
\*/
elproto.stop = function (anim) {
for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) {
if (eve("raphael.anim.stop." + this.id, this, animationElements[i].anim) !== false) {
animationElements.splice(i--, 1);
}
}
return this;
};
function stopAnimation(paper) {
for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.paper == paper) {
animationElements.splice(i--, 1);
}
}
eve.on("raphael.remove", stopAnimation);
eve.on("raphael.clear", stopAnimation);
elproto.toString = function () {
return "Rapha\xebl\u2019s object";
};
// Set
var Set = function (items) {
this.items = [];
this.length = 0;
this.type = "set";
if (items) {
for (var i = 0, ii = items.length; i < ii; i++) {
if (items[i] && (items[i].constructor == elproto.constructor || items[i].constructor == Set)) {
this[this.items.length] = this.items[this.items.length] = items[i];
this.length++;
}
}
}
},
setproto = Set.prototype;
/*\
* Set.push
[ method ]
**
* Adds each argument to the current set.
= (object) original element
\*/
setproto.push = function () {
var item,
len;
for (var i = 0, ii = arguments.length; i < ii; i++) {
item = arguments[i];
if (item && (item.constructor == elproto.constructor || item.constructor == Set)) {
len = this.items.length;
this[len] = this.items[len] = item;
this.length++;
}
}
return this;
};
/*\
* Set.pop
[ method ]
**
* Removes last element and returns it.
= (object) element
\*/
setproto.pop = function () {
this.length && delete this[this.length--];
return this.items.pop();
};
/*\
* Set.forEach
[ method ]
**
* Executes given function for each element in the set.
*
* If function returns `false` it will stop loop running.
**
> Parameters
**
- callback (function) function to run
- thisArg (object) context object for the callback
= (object) Set object
\*/
setproto.forEach = function (callback, thisArg) {
for (var i = 0, ii = this.items.length; i < ii; i++) {
if (callback.call(thisArg, this.items[i], i) === false) {
return this;
}
}
return this;
};
for (var method in elproto) if (elproto[has](method)) {
setproto[method] = (function (methodname) {
return function () {
var arg = arguments;
return this.forEach(function (el) {
el[methodname][apply](el, arg);
});
};
})(method);
}
setproto.attr = function (name, value) {
if (name && R.is(name, array) && R.is(name[0], "object")) {
for (var j = 0, jj = name.length; j < jj; j++) {
this.items[j].attr(name[j]);
}
} else {
for (var i = 0, ii = this.items.length; i < ii; i++) {
this.items[i].attr(name, value);
}
}
return this;
};
/*\
* Set.clear
[ method ]
**
* Removes all elements from the set
\*/
setproto.clear = function () {
while (this.length) {
this.pop();
}
};
/*\
* Set.splice
[ method ]
**
* Removes given element from the set
**
> Parameters
**
- index (number) position of the deletion
- count (number) number of element to remove
- insertion… (object) #optional elements to insert
= (object) set elements that were deleted
\*/
setproto.splice = function (index, count, insertion) {
index = index < 0 ? mmax(this.length + index, 0) : index;
count = mmax(0, mmin(this.length - index, count));
var tail = [],
todel = [],
args = [],
i;
for (i = 2; i < arguments.length; i++) {
args.push(arguments[i]);
}
for (i = 0; i < count; i++) {
todel.push(this[index + i]);
}
for (; i < this.length - index; i++) {
tail.push(this[index + i]);
}
var arglen = args.length;
for (i = 0; i < arglen + tail.length; i++) {
this.items[index + i] = this[index + i] = i < arglen ? args[i] : tail[i - arglen];
}
i = this.items.length = this.length -= count - arglen;
while (this[i]) {
delete this[i++];
}
return new Set(todel);
};
/*\
* Set.exclude
[ method ]
**
* Removes given element from the set
**
> Parameters
**
- element (object) element to remove
= (boolean) `true` if object was found & removed from the set
\*/
setproto.exclude = function (el) {
for (var i = 0, ii = this.length; i < ii; i++) if (this[i] == el) {
this.splice(i, 1);
return true;
}
};
setproto.animate = function (params, ms, easing, callback) {
(R.is(easing, "function") || !easing) && (callback = easing || null);
var len = this.items.length,
i = len,
item,
set = this,
collector;
if (!len) {
return this;
}
callback && (collector = function () {
!--len && callback.call(set);
});
easing = R.is(easing, string) ? easing : collector;
var anim = R.animation(params, ms, easing, collector);
item = this.items[--i].animate(anim);
while (i--) {
this.items[i] && !this.items[i].removed && this.items[i].animateWith(item, anim, anim);
(this.items[i] && !this.items[i].removed) || len--;
}
return this;
};
setproto.insertAfter = function (el) {
var i = this.items.length;
while (i--) {
this.items[i].insertAfter(el);
}
return this;
};
setproto.getBBox = function () {
var x = [],
y = [],
x2 = [],
y2 = [];
for (var i = this.items.length; i--;) if (!this.items[i].removed) {
var box = this.items[i].getBBox();
x.push(box.x);
y.push(box.y);
x2.push(box.x + box.width);
y2.push(box.y + box.height);
}
x = mmin[apply](0, x);
y = mmin[apply](0, y);
x2 = mmax[apply](0, x2);
y2 = mmax[apply](0, y2);
return {
x: x,
y: y,
x2: x2,
y2: y2,
width: x2 - x,
height: y2 - y
};
};
setproto.clone = function (s) {
s = this.paper.set();
for (var i = 0, ii = this.items.length; i < ii; i++) {
s.push(this.items[i].clone());
}
return s;
};
setproto.toString = function () {
return "Rapha\xebl\u2018s set";
};
setproto.glow = function(glowConfig) {
var ret = this.paper.set();
this.forEach(function(shape, index){
var g = shape.glow(glowConfig);
if(g != null){
g.forEach(function(shape2, index2){
ret.push(shape2);
});
}
});
return ret;
};
/*\
* Set.isPointInside
[ method ]
**
* Determine if given point is inside this set’s elements
**
> Parameters
**
- x (number) x coordinate of the point
- y (number) y coordinate of the point
= (boolean) `true` if point is inside any of the set's elements
\*/
setproto.isPointInside = function (x, y) {
var isPointInside = false;
this.forEach(function (el) {
if (el.isPointInside(x, y)) {
isPointInside = true;
return false; // stop loop
}
});
return isPointInside;
};
/*\
* Raphael.registerFont
[ method ]
**
* Adds given font to the registered set of fonts for Raphaël. Should be used as an internal call from within Cufón’s font file.
* Returns original parameter, so it could be used with chaining.
# More about Cufón and how to convert your font form TTF, OTF, etc to JavaScript file.
**
> Parameters
**
- font (object) the font to register
= (object) the font you passed in
> Usage
| Cufon.registerFont(Raphael.registerFont({…}));
\*/
R.registerFont = function (font) {
if (!font.face) {
return font;
}
this.fonts = this.fonts || {};
var fontcopy = {
w: font.w,
face: {},
glyphs: {}
},
family = font.face["font-family"];
for (var prop in font.face) if (font.face[has](prop)) {
fontcopy.face[prop] = font.face[prop];
}
if (this.fonts[family]) {
this.fonts[family].push(fontcopy);
} else {
this.fonts[family] = [fontcopy];
}
if (!font.svg) {
fontcopy.face["units-per-em"] = toInt(font.face["units-per-em"], 10);
for (var glyph in font.glyphs) if (font.glyphs[has](glyph)) {
var path = font.glyphs[glyph];
fontcopy.glyphs[glyph] = {
w: path.w,
k: {},
d: path.d && "M" + path.d.replace(/[mlcxtrv]/g, function (command) {
return {l: "L", c: "C", x: "z", t: "m", r: "l", v: "c"}[command] || "M";
}) + "z"
};
if (path.k) {
for (var k in path.k) if (path[has](k)) {
fontcopy.glyphs[glyph].k[k] = path.k[k];
}
}
}
}
return font;
};
/*\
* Paper.getFont
[ method ]
**
* Finds font object in the registered fonts by given parameters. You could specify only one word from the font name, like “Myriad†for “Myriad Proâ€.
**
> Parameters
**
- family (string) font family name or any word from it
- weight (string) #optional font weight
- style (string) #optional font style
- stretch (string) #optional font stretch
= (object) the font object
> Usage
| paper.print(100, 100, "Test string", paper.getFont("Times", 800), 30);
\*/
paperproto.getFont = function (family, weight, style, stretch) {
stretch = stretch || "normal";
style = style || "normal";
weight = +weight || {normal: 400, bold: 700, lighter: 300, bolder: 800}[weight] || 400;
if (!R.fonts) {
return;
}
var font = R.fonts[family];
if (!font) {
var name = new RegExp("(^|\\s)" + family.replace(/[^\w\d\s+!~.:_-]/g, E) + "(\\s|$)", "i");
for (var fontName in R.fonts) if (R.fonts[has](fontName)) {
if (name.test(fontName)) {
font = R.fonts[fontName];
break;
}
}
}
var thefont;
if (font) {
for (var i = 0, ii = font.length; i < ii; i++) {
thefont = font[i];
if (thefont.face["font-weight"] == weight && (thefont.face["font-style"] == style || !thefont.face["font-style"]) && thefont.face["font-stretch"] == stretch) {
break;
}
}
}
return thefont;
};
/*\
* Paper.print
[ method ]
**
* Creates path that represent given text written using given font at given position with given size.
* Result of the method is path element that contains whole text as a separate path.
**
> Parameters
**
- x (number) x position of the text
- y (number) y position of the text
- string (string) text to print
- font (object) font object, see @Paper.getFont
- size (number) #optional size of the font, default is `16`
- origin (string) #optional could be `"baseline"` or `"middle"`, default is `"middle"`
- letter_spacing (number) #optional number in range `-1..1`, default is `0`
- line_spacing (number) #optional number in range `1..3`, default is `1`
= (object) resulting path element, which consist of all letters
> Usage
| var txt = r.print(10, 50, "print", r.getFont("Museo"), 30).attr({fill: "#fff"});
\*/
paperproto.print = function (x, y, string, font, size, origin, letter_spacing, line_spacing) {
origin = origin || "middle"; // baseline|middle
letter_spacing = mmax(mmin(letter_spacing || 0, 1), -1);
line_spacing = mmax(mmin(line_spacing || 1, 3), 1);
var letters = Str(string)[split](E),
shift = 0,
notfirst = 0,
path = E,
scale;
R.is(font, "string") && (font = this.getFont(font));
if (font) {
scale = (size || 16) / font.face["units-per-em"];
var bb = font.face.bbox[split](separator),
top = +bb[0],
lineHeight = bb[3] - bb[1],
shifty = 0,
height = +bb[1] + (origin == "baseline" ? lineHeight + (+font.face.descent) : lineHeight / 2);
for (var i = 0, ii = letters.length; i < ii; i++) {
if (letters[i] == "\n") {
shift = 0;
curr = 0;
notfirst = 0;
shifty += lineHeight * line_spacing;
} else {
var prev = notfirst && font.glyphs[letters[i - 1]] || {},
curr = font.glyphs[letters[i]];
shift += notfirst ? (prev.w || font.w) + (prev.k && prev.k[letters[i]] || 0) + (font.w * letter_spacing) : 0;
notfirst = 1;
}
if (curr && curr.d) {
path += R.transformPath(curr.d, ["t", shift * scale, shifty * scale, "s", scale, scale, top, height, "t", (x - top) / scale, (y - height) / scale]);
}
}
}
return this.path(path).attr({
fill: "#000",
stroke: "none"
});
};
/*\
* Paper.add
[ method ]
**
* Imports elements in JSON array in format `{type: type, }`
**
> Parameters
**
- json (array)
= (object) resulting set of imported elements
> Usage
| paper.add([
| {
| type: "circle",
| cx: 10,
| cy: 10,
| r: 5
| },
| {
| type: "rect",
| x: 10,
| y: 10,
| width: 10,
| height: 10,
| fill: "#fc0"
| }
| ]);
\*/
paperproto.add = function (json) {
if (R.is(json, "array")) {
var res = this.set(),
i = 0,
ii = json.length,
j;
for (; i < ii; i++) {
j = json[i] || {};
elements[has](j.type) && res.push(this[j.type]().attr(j));
}
}
return res;
};
/*\
* Raphael.format
[ method ]
**
* Simple format function. Replaces construction of type “`{}`†to the corresponding argument.
**
> Parameters
**
- token (string) string to format
- … (string) rest of arguments will be treated as parameters for replacement
= (string) formated string
> Usage
| var x = 10,
| y = 20,
| width = 40,
| height = 50;
| // this will draw a rectangular shape equivalent to "M10,20h40v50h-40z"
| paper.path(Raphael.format("M{0},{1}h{2}v{3}h{4}z", x, y, width, height, -width));
\*/
R.format = function (token, params) {
var args = R.is(params, array) ? [0][concat](params) : arguments;
token && R.is(token, string) && args.length - 1 && (token = token.replace(formatrg, function (str, i) {
return args[++i] == null ? E : args[i];
}));
return token || E;
};
/*\
* Raphael.fullfill
[ method ]
**
* A little bit more advanced format function than @Raphael.format. Replaces construction of type “`{}`†to the corresponding argument.
**
> Parameters
**
- token (string) string to format
- json (object) object which properties will be used as a replacement
= (string) formated string
> Usage
| // this will draw a rectangular shape equivalent to "M10,20h40v50h-40z"
| paper.path(Raphael.fullfill("M{x},{y}h{dim.width}v{dim.height}h{dim['negative width']}z", {
| x: 10,
| y: 20,
| dim: {
| width: 40,
| height: 50,
| "negative width": -40
| }
| }));
\*/
R.fullfill = (function () {
var tokenRegex = /\{([^\}]+)\}/g,
objNotationRegex = /(?:(?:^|\.)(.+?)(?=\[|\.|$|\()|\[('|")(.+?)\2\])(\(\))?/g, // matches .xxxxx or ["xxxxx"] to run over object properties
replacer = function (all, key, obj) {
var res = obj;
key.replace(objNotationRegex, function (all, name, quote, quotedName, isFunc) {
name = name || quotedName;
if (res) {
if (name in res) {
res = res[name];
}
typeof res == "function" && isFunc && (res = res());
}
});
res = (res == null || res == obj ? all : res) + "";
return res;
};
return function (str, obj) {
return String(str).replace(tokenRegex, function (all, key) {
return replacer(all, key, obj);
});
};
})();
/*\
* Raphael.ninja
[ method ]
**
* If you want to leave no trace of Raphaël (Well, Raphaël creates only one global variable `Raphael`, but anyway.) You can use `ninja` method.
* Beware, that in this case plugins could stop working, because they are depending on global variable existence.
**
= (object) Raphael object
> Usage
| (function (local_raphael) {
| var paper = local_raphael(10, 10, 320, 200);
| …
| })(Raphael.ninja());
\*/
R.ninja = function () {
if (oldRaphael.was) {
g.win.Raphael = oldRaphael.is;
} else {
// IE8 raises an error when deleting window property
window.Raphael = undefined;
try {
delete window.Raphael;
} catch(e) {}
}
return R;
};
/*\
* Raphael.st
[ property (object) ]
**
* You can add your own method to elements and sets. It is wise to add a set method for each element method
* you added, so you will be able to call the same method on sets too.
**
* See also @Raphael.el.
> Usage
| Raphael.el.red = function () {
| this.attr({fill: "#f00"});
| };
| Raphael.st.red = function () {
| this.forEach(function (el) {
| el.red();
| });
| };
| // then use it
| paper.set(paper.circle(100, 100, 20), paper.circle(110, 100, 20)).red();
\*/
R.st = setproto;
eve.on("raphael.DOMload", function () {
loaded = true;
});
// Firefox <3.6 fix: http://webreflection.blogspot.com/2009/11/195-chars-to-help-lazy-loading.html
(function (doc, loaded, f) {
if (doc.readyState == null && doc.addEventListener){
doc.addEventListener(loaded, f = function () {
doc.removeEventListener(loaded, f, false);
doc.readyState = "complete";
}, false);
doc.readyState = "loading";
}
function isLoaded() {
(/in/).test(doc.readyState) ? setTimeout(isLoaded, 9) : R.eve("raphael.DOMload");
}
isLoaded();
})(document, "DOMContentLoaded");
return R;
}.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
/***/ },
/* 2 */
/***/ function(module, exports, __webpack_require__) {
var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ┌────────────────────────────────────────────────────────────┠\\
// │ Eve 0.5.0 - JavaScript Events Library │ \\
// ├────────────────────────────────────────────────────────────┤ \\
// │ Author Dmitry Baranovskiy (http://dmitry.baranovskiy.com/) │ \\
// └────────────────────────────────────────────────────────────┘ \\
(function (glob) {
var version = "0.5.0",
has = "hasOwnProperty",
separator = /[\.\/]/,
comaseparator = /\s*,\s*/,
wildcard = "*",
fun = function () {},
numsort = function (a, b) {
return a - b;
},
current_event,
stop,
events = {n: {}},
firstDefined = function () {
for (var i = 0, ii = this.length; i < ii; i++) {
if (typeof this[i] != "undefined") {
return this[i];
}
}
},
lastDefined = function () {
var i = this.length;
while (--i) {
if (typeof this[i] != "undefined") {
return this[i];
}
}
},
objtos = Object.prototype.toString,
Str = String,
isArray = Array.isArray || function (ar) {
return ar instanceof Array || objtos.call(ar) == "[object Array]";
};
/*\
* eve
[ method ]
* Fires event with given `name`, given scope and other parameters.
> Arguments
- name (string) name of the *event*, dot (`.`) or slash (`/`) separated
- scope (object) context for the event handlers
- varargs (...) the rest of arguments will be sent to event handlers
= (object) array of returned values from the listeners. Array has two methods `.firstDefined()` and `.lastDefined()` to get first or last not `undefined` value.
\*/
eve = function (name, scope) {
var e = events,
oldstop = stop,
args = Array.prototype.slice.call(arguments, 2),
listeners = eve.listeners(name),
z = 0,
f = false,
l,
indexed = [],
queue = {},
out = [],
ce = current_event,
errors = [];
out.firstDefined = firstDefined;
out.lastDefined = lastDefined;
current_event = name;
stop = 0;
for (var i = 0, ii = listeners.length; i < ii; i++) if ("zIndex" in listeners[i]) {
indexed.push(listeners[i].zIndex);
if (listeners[i].zIndex < 0) {
queue[listeners[i].zIndex] = listeners[i];
}
}
indexed.sort(numsort);
while (indexed[z] < 0) {
l = queue[indexed[z++]];
out.push(l.apply(scope, args));
if (stop) {
stop = oldstop;
return out;
}
}
for (i = 0; i < ii; i++) {
l = listeners[i];
if ("zIndex" in l) {
if (l.zIndex == indexed[z]) {
out.push(l.apply(scope, args));
if (stop) {
break;
}
do {
z++;
l = queue[indexed[z]];
l && out.push(l.apply(scope, args));
if (stop) {
break;
}
} while (l)
} else {
queue[l.zIndex] = l;
}
} else {
out.push(l.apply(scope, args));
if (stop) {
break;
}
}
}
stop = oldstop;
current_event = ce;
return out;
};
// Undocumented. Debug only.
eve._events = events;
/*\
* eve.listeners
[ method ]
* Internal method which gives you array of all event handlers that will be triggered by the given `name`.
> Arguments
- name (string) name of the event, dot (`.`) or slash (`/`) separated
= (array) array of event handlers
\*/
eve.listeners = function (name) {
var names = isArray(name) ? name : name.split(separator),
e = events,
item,
items,
k,
i,
ii,
j,
jj,
nes,
es = [e],
out = [];
for (i = 0, ii = names.length; i < ii; i++) {
nes = [];
for (j = 0, jj = es.length; j < jj; j++) {
e = es[j].n;
items = [e[names[i]], e[wildcard]];
k = 2;
while (k--) {
item = items[k];
if (item) {
nes.push(item);
out = out.concat(item.f || []);
}
}
}
es = nes;
}
return out;
};
/*\
* eve.separator
[ method ]
* If for some reasons you don’t like default separators (`.` or `/`) you can specify yours
* here. Be aware that if you pass a string longer than one character it will be treated as
* a list of characters.
- separator (string) new separator. Empty string resets to default: `.` or `/`.
\*/
eve.separator = function (sep) {
if (sep) {
sep = Str(sep).replace(/(?=[\.\^\]\[\-])/g, "\\");
sep = "[" + sep + "]";
separator = new RegExp(sep);
} else {
separator = /[\.\/]/;
}
};
/*\
* eve.on
[ method ]
**
* Binds given event handler with a given name. You can use wildcards “`*`†for the names:
| eve.on("*.under.*", f);
| eve("mouse.under.floor"); // triggers f
* Use @eve to trigger the listener.
**
- name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards
- f (function) event handler function
**
- name (array) if you don’t want to use separators, you can use array of strings
- f (function) event handler function
**
= (function) returned function accepts a single numeric parameter that represents z-index of the handler. It is an optional feature and only used when you need to ensure that some subset of handlers will be invoked in a given order, despite of the order of assignment.
> Example:
| eve.on("mouse", eatIt)(2);
| eve.on("mouse", scream);
| eve.on("mouse", catchIt)(1);
* This will ensure that `catchIt` function will be called before `eatIt`.
*
* If you want to put your handler before non-indexed handlers, specify a negative value.
* Note: I assume most of the time you don’t need to worry about z-index, but it’s nice to have this feature “just in caseâ€.
\*/
eve.on = function (name, f) {
if (typeof f != "function") {
return function () {};
}
var names = isArray(name) ? (isArray(name[0]) ? name : [name]) : Str(name).split(comaseparator);
for (var i = 0, ii = names.length; i < ii; i++) {
(function (name) {
var names = isArray(name) ? name : Str(name).split(separator),
e = events,
exist;
for (var i = 0, ii = names.length; i < ii; i++) {
e = e.n;
e = e.hasOwnProperty(names[i]) && e[names[i]] || (e[names[i]] = {n: {}});
}
e.f = e.f || [];
for (i = 0, ii = e.f.length; i < ii; i++) if (e.f[i] == f) {
exist = true;
break;
}
!exist && e.f.push(f);
}(names[i]));
}
return function (zIndex) {
if (+zIndex == +zIndex) {
f.zIndex = +zIndex;
}
};
};
/*\
* eve.f
[ method ]
**
* Returns function that will fire given event with optional arguments.
* Arguments that will be passed to the result function will be also
* concated to the list of final arguments.
| el.onclick = eve.f("click", 1, 2);
| eve.on("click", function (a, b, c) {
| console.log(a, b, c); // 1, 2, [event object]
| });
> Arguments
- event (string) event name
- varargs (…) and any other arguments
= (function) possible event handler function
\*/
eve.f = function (event) {
var attrs = [].slice.call(arguments, 1);
return function () {
eve.apply(null, [event, null].concat(attrs).concat([].slice.call(arguments, 0)));
};
};
/*\
* eve.stop
[ method ]
**
* Is used inside an event handler to stop the event, preventing any subsequent listeners from firing.
\*/
eve.stop = function () {
stop = 1;
};
/*\
* eve.nt
[ method ]
**
* Could be used inside event handler to figure out actual name of the event.
**
> Arguments
**
- subname (string) #optional subname of the event
**
= (string) name of the event, if `subname` is not specified
* or
= (boolean) `true`, if current event’s name contains `subname`
\*/
eve.nt = function (subname) {
var cur = isArray(current_event) ? current_event.join(".") : current_event;
if (subname) {
return new RegExp("(?:\\.|\\/|^)" + subname + "(?:\\.|\\/|$)").test(cur);
}
return cur;
};
/*\
* eve.nts
[ method ]
**
* Could be used inside event handler to figure out actual name of the event.
**
**
= (array) names of the event
\*/
eve.nts = function () {
return isArray(current_event) ? current_event : current_event.split(separator);
};
/*\
* eve.off
[ method ]
**
* Removes given function from the list of event listeners assigned to given name.
* If no arguments specified all the events will be cleared.
**
> Arguments
**
- name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards
- f (function) event handler function
\*/
/*\
* eve.unbind
[ method ]
**
* See @eve.off
\*/
eve.off = eve.unbind = function (name, f) {
if (!name) {
eve._events = events = {n: {}};
return;
}
var names = isArray(name) ? (isArray(name[0]) ? name : [name]) : Str(name).split(comaseparator);
if (names.length > 1) {
for (var i = 0, ii = names.length; i < ii; i++) {
eve.off(names[i], f);
}
return;
}
names = isArray(name) ? name : Str(name).split(separator);
var e,
key,
splice,
i, ii, j, jj,
cur = [events];
for (i = 0, ii = names.length; i < ii; i++) {
for (j = 0; j < cur.length; j += splice.length - 2) {
splice = [j, 1];
e = cur[j].n;
if (names[i] != wildcard) {
if (e[names[i]]) {
splice.push(e[names[i]]);
}
} else {
for (key in e) if (e[has](key)) {
splice.push(e[key]);
}
}
cur.splice.apply(cur, splice);
}
}
for (i = 0, ii = cur.length; i < ii; i++) {
e = cur[i];
while (e.n) {
if (f) {
if (e.f) {
for (j = 0, jj = e.f.length; j < jj; j++) if (e.f[j] == f) {
e.f.splice(j, 1);
break;
}
!e.f.length && delete e.f;
}
for (key in e.n) if (e.n[has](key) && e.n[key].f) {
var funcs = e.n[key].f;
for (j = 0, jj = funcs.length; j < jj; j++) if (funcs[j] == f) {
funcs.splice(j, 1);
break;
}
!funcs.length && delete e.n[key].f;
}
} else {
delete e.f;
for (key in e.n) if (e.n[has](key) && e.n[key].f) {
delete e.n[key].f;
}
}
e = e.n;
}
}
};
/*\
* eve.once
[ method ]
**
* Binds given event handler with a given name to only run once then unbind itself.
| eve.once("login", f);
| eve("login"); // triggers f
| eve("login"); // no listeners
* Use @eve to trigger the listener.
**
> Arguments
**
- name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards
- f (function) event handler function
**
= (function) same return function as @eve.on
\*/
eve.once = function (name, f) {
var f2 = function () {
eve.off(name, f2);
return f.apply(this, arguments);
};
return eve.on(name, f2);
};
/*\
* eve.version
[ property (string) ]
**
* Current version of the library.
\*/
eve.version = version;
eve.toString = function () {
return "You are running Eve " + version;
};
(typeof module != "undefined" && module.exports) ? (module.exports = eve) : ( true ? (!(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_RESULT__ = function() { return eve; }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__))) : (glob.eve = eve));
})(this);
/***/ },
/* 3 */
/***/ function(module, exports, __webpack_require__) {
var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(1)], __WEBPACK_AMD_DEFINE_RESULT__ = function(R) {
if (R && !R.svg) {
return;
}
var has = "hasOwnProperty",
Str = String,
toFloat = parseFloat,
toInt = parseInt,
math = Math,
mmax = math.max,
abs = math.abs,
pow = math.pow,
separator = /[, ]+/,
eve = R.eve,
E = "",
S = " ";
var xlink = "http://www.w3.org/1999/xlink",
markers = {
block: "M5,0 0,2.5 5,5z",
classic: "M5,0 0,2.5 5,5 3.5,3 3.5,2z",
diamond: "M2.5,0 5,2.5 2.5,5 0,2.5z",
open: "M6,1 1,3.5 6,6",
oval: "M2.5,0A2.5,2.5,0,0,1,2.5,5 2.5,2.5,0,0,1,2.5,0z"
},
markerCounter = {};
R.toString = function () {
return "Your browser supports SVG.\nYou are running Rapha\xebl " + this.version;
};
var $ = function (el, attr) {
if (attr) {
if (typeof el == "string") {
el = $(el);
}
for (var key in attr) if (attr[has](key)) {
if (key.substring(0, 6) == "xlink:") {
el.setAttributeNS(xlink, key.substring(6), Str(attr[key]));
} else {
el.setAttribute(key, Str(attr[key]));
}
}
} else {
el = R._g.doc.createElementNS("http://www.w3.org/2000/svg", el);
el.style && (el.style.webkitTapHighlightColor = "rgba(0,0,0,0)");
}
return el;
},
addGradientFill = function (element, gradient) {
var type = "linear",
id = element.id + gradient,
fx = .5, fy = .5,
o = element.node,
SVG = element.paper,
s = o.style,
el = R._g.doc.getElementById(id);
if (!el) {
gradient = Str(gradient).replace(R._radial_gradient, function (all, _fx, _fy) {
type = "radial";
if (_fx && _fy) {
fx = toFloat(_fx);
fy = toFloat(_fy);
var dir = ((fy > .5) * 2 - 1);
pow(fx - .5, 2) + pow(fy - .5, 2) > .25 &&
(fy = math.sqrt(.25 - pow(fx - .5, 2)) * dir + .5) &&
fy != .5 &&
(fy = fy.toFixed(5) - 1e-5 * dir);
}
return E;
});
gradient = gradient.split(/\s*\-\s*/);
if (type == "linear") {
var angle = gradient.shift();
angle = -toFloat(angle);
if (isNaN(angle)) {
return null;
}
var vector = [0, 0, math.cos(R.rad(angle)), math.sin(R.rad(angle))],
max = 1 / (mmax(abs(vector[2]), abs(vector[3])) || 1);
vector[2] *= max;
vector[3] *= max;
if (vector[2] < 0) {
vector[0] = -vector[2];
vector[2] = 0;
}
if (vector[3] < 0) {
vector[1] = -vector[3];
vector[3] = 0;
}
}
var dots = R._parseDots(gradient);
if (!dots) {
return null;
}
id = id.replace(/[\(\)\s,\xb0#]/g, "_");
if (element.gradient && id != element.gradient.id) {
SVG.defs.removeChild(element.gradient);
delete element.gradient;
}
if (!element.gradient) {
el = $(type + "Gradient", {id: id});
element.gradient = el;
$(el, type == "radial" ? {
fx: fx,
fy: fy
} : {
x1: vector[0],
y1: vector[1],
x2: vector[2],
y2: vector[3],
gradientTransform: element.matrix.invert()
});
SVG.defs.appendChild(el);
for (var i = 0, ii = dots.length; i < ii; i++) {
el.appendChild($("stop", {
offset: dots[i].offset ? dots[i].offset : i ? "100%" : "0%",
"stop-color": dots[i].color || "#fff",
"stop-opacity": isFinite(dots[i].opacity) ? dots[i].opacity : 1
}));
}
}
}
$(o, {
fill: fillurl(id),
opacity: 1,
"fill-opacity": 1
});
s.fill = E;
s.opacity = 1;
s.fillOpacity = 1;
return 1;
},
isIE9or10 = function () {
var mode = document.documentMode;
return mode && (mode === 9 || mode === 10);
},
fillurl = function (id) {
if (isIE9or10()) {
return "url('#" + id + "')";
}
var location = document.location;
var locationString = (
location.protocol + '//' +
location.host +
location.pathname +
location.search
);
return "url('" + locationString + "#" + id + "')";
},
updatePosition = function (o) {
var bbox = o.getBBox(1);
$(o.pattern, {patternTransform: o.matrix.invert() + " translate(" + bbox.x + "," + bbox.y + ")"});
},
addArrow = function (o, value, isEnd) {
if (o.type == "path") {
var values = Str(value).toLowerCase().split("-"),
p = o.paper,
se = isEnd ? "end" : "start",
node = o.node,
attrs = o.attrs,
stroke = attrs["stroke-width"],
i = values.length,
type = "classic",
from,
to,
dx,
refX,
attr,
w = 3,
h = 3,
t = 5;
while (i--) {
switch (values[i]) {
case "block":
case "classic":
case "oval":
case "diamond":
case "open":
case "none":
type = values[i];
break;
case "wide": h = 5; break;
case "narrow": h = 2; break;
case "long": w = 5; break;
case "short": w = 2; break;
}
}
if (type == "open") {
w += 2;
h += 2;
t += 2;
dx = 1;
refX = isEnd ? 4 : 1;
attr = {
fill: "none",
stroke: attrs.stroke
};
} else {
refX = dx = w / 2;
attr = {
fill: attrs.stroke,
stroke: "none"
};
}
if (o._.arrows) {
if (isEnd) {
o._.arrows.endPath && markerCounter[o._.arrows.endPath]--;
o._.arrows.endMarker && markerCounter[o._.arrows.endMarker]--;
} else {
o._.arrows.startPath && markerCounter[o._.arrows.startPath]--;
o._.arrows.startMarker && markerCounter[o._.arrows.startMarker]--;
}
} else {
o._.arrows = {};
}
if (type != "none") {
var pathId = "raphael-marker-" + type,
markerId = "raphael-marker-" + se + type + w + h + "-obj" + o.id;
if (!R._g.doc.getElementById(pathId)) {
p.defs.appendChild($($("path"), {
"stroke-linecap": "round",
d: markers[type],
id: pathId
}));
markerCounter[pathId] = 1;
} else {
markerCounter[pathId]++;
}
var marker = R._g.doc.getElementById(markerId),
use;
if (!marker) {
marker = $($("marker"), {
id: markerId,
markerHeight: h,
markerWidth: w,
orient: "auto",
refX: refX,
refY: h / 2
});
use = $($("use"), {
"xlink:href": "#" + pathId,
transform: (isEnd ? "rotate(180 " + w / 2 + " " + h / 2 + ") " : E) + "scale(" + w / t + "," + h / t + ")",
"stroke-width": (1 / ((w / t + h / t) / 2)).toFixed(4)
});
marker.appendChild(use);
p.defs.appendChild(marker);
markerCounter[markerId] = 1;
} else {
markerCounter[markerId]++;
use = marker.getElementsByTagName("use")[0];
}
$(use, attr);
var delta = dx * (type != "diamond" && type != "oval");
if (isEnd) {
from = o._.arrows.startdx * stroke || 0;
to = R.getTotalLength(attrs.path) - delta * stroke;
} else {
from = delta * stroke;
to = R.getTotalLength(attrs.path) - (o._.arrows.enddx * stroke || 0);
}
attr = {};
attr["marker-" + se] = "url(#" + markerId + ")";
if (to || from) {
attr.d = R.getSubpath(attrs.path, from, to);
}
$(node, attr);
o._.arrows[se + "Path"] = pathId;
o._.arrows[se + "Marker"] = markerId;
o._.arrows[se + "dx"] = delta;
o._.arrows[se + "Type"] = type;
o._.arrows[se + "String"] = value;
} else {
if (isEnd) {
from = o._.arrows.startdx * stroke || 0;
to = R.getTotalLength(attrs.path) - from;
} else {
from = 0;
to = R.getTotalLength(attrs.path) - (o._.arrows.enddx * stroke || 0);
}
o._.arrows[se + "Path"] && $(node, {d: R.getSubpath(attrs.path, from, to)});
delete o._.arrows[se + "Path"];
delete o._.arrows[se + "Marker"];
delete o._.arrows[se + "dx"];
delete o._.arrows[se + "Type"];
delete o._.arrows[se + "String"];
}
for (attr in markerCounter) if (markerCounter[has](attr) && !markerCounter[attr]) {
var item = R._g.doc.getElementById(attr);
item && item.parentNode.removeChild(item);
}
}
},
dasharray = {
"-": [3, 1],
".": [1, 1],
"-.": [3, 1, 1, 1],
"-..": [3, 1, 1, 1, 1, 1],
". ": [1, 3],
"- ": [4, 3],
"--": [8, 3],
"- .": [4, 3, 1, 3],
"--.": [8, 3, 1, 3],
"--..": [8, 3, 1, 3, 1, 3]
},
addDashes = function (o, value, params) {
value = dasharray[Str(value).toLowerCase()];
if (value) {
var width = o.attrs["stroke-width"] || "1",
butt = {round: width, square: width, butt: 0}[o.attrs["stroke-linecap"] || params["stroke-linecap"]] || 0,
dashes = [],
i = value.length;
while (i--) {
dashes[i] = value[i] * width + ((i % 2) ? 1 : -1) * butt;
}
$(o.node, {"stroke-dasharray": dashes.join(",")});
}
else {
$(o.node, {"stroke-dasharray": "none"});
}
},
setFillAndStroke = function (o, params) {
var node = o.node,
attrs = o.attrs,
vis = node.style.visibility;
node.style.visibility = "hidden";
for (var att in params) {
if (params[has](att)) {
if (!R._availableAttrs[has](att)) {
continue;
}
var value = params[att];
attrs[att] = value;
switch (att) {
case "blur":
o.blur(value);
break;
case "title":
var title = node.getElementsByTagName("title");
// Use the existing .
if (title.length && (title = title[0])) {
title.firstChild.nodeValue = value;
} else {
title = $("title");
var val = R._g.doc.createTextNode(value);
title.appendChild(val);
node.appendChild(title);
}
break;
case "href":
case "target":
var pn = node.parentNode;
if (pn.tagName.toLowerCase() != "a") {
var hl = $("a");
pn.insertBefore(hl, node);
hl.appendChild(node);
pn = hl;
}
if (att == "target") {
pn.setAttributeNS(xlink, "show", value == "blank" ? "new" : value);
} else {
pn.setAttributeNS(xlink, att, value);
}
break;
case "cursor":
node.style.cursor = value;
break;
case "transform":
o.transform(value);
break;
case "arrow-start":
addArrow(o, value);
break;
case "arrow-end":
addArrow(o, value, 1);
break;
case "clip-rect":
var rect = Str(value).split(separator);
if (rect.length == 4) {
o.clip && o.clip.parentNode.parentNode.removeChild(o.clip.parentNode);
var el = $("clipPath"),
rc = $("rect");
el.id = R.createUUID();
$(rc, {
x: rect[0],
y: rect[1],
width: rect[2],
height: rect[3]
});
el.appendChild(rc);
o.paper.defs.appendChild(el);
$(node, {"clip-path": "url(#" + el.id + ")"});
o.clip = rc;
}
if (!value) {
var path = node.getAttribute("clip-path");
if (path) {
var clip = R._g.doc.getElementById(path.replace(/(^url\(#|\)$)/g, E));
clip && clip.parentNode.removeChild(clip);
$(node, {"clip-path": E});
delete o.clip;
}
}
break;
case "path":
if (o.type == "path") {
$(node, {d: value ? attrs.path = R._pathToAbsolute(value) : "M0,0"});
o._.dirty = 1;
if (o._.arrows) {
"startString" in o._.arrows && addArrow(o, o._.arrows.startString);
"endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1);
}
}
break;
case "width":
node.setAttribute(att, value);
o._.dirty = 1;
if (attrs.fx) {
att = "x";
value = attrs.x;
} else {
break;
}
case "x":
if (attrs.fx) {
value = -attrs.x - (attrs.width || 0);
}
case "rx":
if (att == "rx" && o.type == "rect") {
break;
}
case "cx":
node.setAttribute(att, value);
o.pattern && updatePosition(o);
o._.dirty = 1;
break;
case "height":
node.setAttribute(att, value);
o._.dirty = 1;
if (attrs.fy) {
att = "y";
value = attrs.y;
} else {
break;
}
case "y":
if (attrs.fy) {
value = -attrs.y - (attrs.height || 0);
}
case "ry":
if (att == "ry" && o.type == "rect") {
break;
}
case "cy":
node.setAttribute(att, value);
o.pattern && updatePosition(o);
o._.dirty = 1;
break;
case "r":
if (o.type == "rect") {
$(node, {rx: value, ry: value});
} else {
node.setAttribute(att, value);
}
o._.dirty = 1;
break;
case "src":
if (o.type == "image") {
node.setAttributeNS(xlink, "href", value);
}
break;
case "stroke-width":
if (o._.sx != 1 || o._.sy != 1) {
value /= mmax(abs(o._.sx), abs(o._.sy)) || 1;
}
node.setAttribute(att, value);
if (attrs["stroke-dasharray"]) {
addDashes(o, attrs["stroke-dasharray"], params);
}
if (o._.arrows) {
"startString" in o._.arrows && addArrow(o, o._.arrows.startString);
"endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1);
}
break;
case "stroke-dasharray":
addDashes(o, value, params);
break;
case "fill":
var isURL = Str(value).match(R._ISURL);
if (isURL) {
el = $("pattern");
var ig = $("image");
el.id = R.createUUID();
$(el, {x: 0, y: 0, patternUnits: "userSpaceOnUse", height: 1, width: 1});
$(ig, {x: 0, y: 0, "xlink:href": isURL[1]});
el.appendChild(ig);
(function (el) {
R._preload(isURL[1], function () {
var w = this.offsetWidth,
h = this.offsetHeight;
$(el, {width: w, height: h});
$(ig, {width: w, height: h});
});
})(el);
o.paper.defs.appendChild(el);
$(node, {fill: "url(#" + el.id + ")"});
o.pattern = el;
o.pattern && updatePosition(o);
break;
}
var clr = R.getRGB(value);
if (!clr.error) {
delete params.gradient;
delete attrs.gradient;
!R.is(attrs.opacity, "undefined") &&
R.is(params.opacity, "undefined") &&
$(node, {opacity: attrs.opacity});
!R.is(attrs["fill-opacity"], "undefined") &&
R.is(params["fill-opacity"], "undefined") &&
$(node, {"fill-opacity": attrs["fill-opacity"]});
} else if ((o.type == "circle" || o.type == "ellipse" || Str(value).charAt() != "r") && addGradientFill(o, value)) {
if ("opacity" in attrs || "fill-opacity" in attrs) {
var gradient = R._g.doc.getElementById(node.getAttribute("fill").replace(/^url\(#|\)$/g, E));
if (gradient) {
var stops = gradient.getElementsByTagName("stop");
$(stops[stops.length - 1], {"stop-opacity": ("opacity" in attrs ? attrs.opacity : 1) * ("fill-opacity" in attrs ? attrs["fill-opacity"] : 1)});
}
}
attrs.gradient = value;
attrs.fill = "none";
break;
}
clr[has]("opacity") && $(node, {"fill-opacity": clr.opacity > 1 ? clr.opacity / 100 : clr.opacity});
case "stroke":
clr = R.getRGB(value);
node.setAttribute(att, clr.hex);
att == "stroke" && clr[has]("opacity") && $(node, {"stroke-opacity": clr.opacity > 1 ? clr.opacity / 100 : clr.opacity});
if (att == "stroke" && o._.arrows) {
"startString" in o._.arrows && addArrow(o, o._.arrows.startString);
"endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1);
}
break;
case "gradient":
(o.type == "circle" || o.type == "ellipse" || Str(value).charAt() != "r") && addGradientFill(o, value);
break;
case "opacity":
if (attrs.gradient && !attrs[has]("stroke-opacity")) {
$(node, {"stroke-opacity": value > 1 ? value / 100 : value});
}
// fall
case "fill-opacity":
if (attrs.gradient) {
gradient = R._g.doc.getElementById(node.getAttribute("fill").replace(/^url\(#|\)$/g, E));
if (gradient) {
stops = gradient.getElementsByTagName("stop");
$(stops[stops.length - 1], {"stop-opacity": value});
}
break;
}
default:
att == "font-size" && (value = toInt(value, 10) + "px");
var cssrule = att.replace(/(\-.)/g, function (w) {
return w.substring(1).toUpperCase();
});
node.style[cssrule] = value;
o._.dirty = 1;
node.setAttribute(att, value);
break;
}
}
}
tuneText(o, params);
node.style.visibility = vis;
},
leading = 1.2,
tuneText = function (el, params) {
if (el.type != "text" || !(params[has]("text") || params[has]("font") || params[has]("font-size") || params[has]("x") || params[has]("y"))) {
return;
}
var a = el.attrs,
node = el.node,
fontSize = node.firstChild ? toInt(R._g.doc.defaultView.getComputedStyle(node.firstChild, E).getPropertyValue("font-size"), 10) : 10;
if (params[has]("text")) {
a.text = params.text;
while (node.firstChild) {
node.removeChild(node.firstChild);
}
var texts = Str(params.text).split("\n"),
tspans = [],
tspan;
for (var i = 0, ii = texts.length; i < ii; i++) {
tspan = $("tspan");
i && $(tspan, {dy: fontSize * leading, x: a.x});
tspan.appendChild(R._g.doc.createTextNode(texts[i]));
node.appendChild(tspan);
tspans[i] = tspan;
}
} else {
tspans = node.getElementsByTagName("tspan");
for (i = 0, ii = tspans.length; i < ii; i++) if (i) {
$(tspans[i], {dy: fontSize * leading, x: a.x});
} else {
$(tspans[0], {dy: 0});
}
}
$(node, {x: a.x, y: a.y});
el._.dirty = 1;
var bb = el._getBBox(),
dif = a.y - (bb.y + bb.height / 2);
dif && R.is(dif, "finite") && $(tspans[0], {dy: dif});
},
getRealNode = function (node) {
if (node.parentNode && node.parentNode.tagName.toLowerCase() === "a") {
return node.parentNode;
} else {
return node;
}
},
Element = function (node, svg) {
var X = 0,
Y = 0;
/*\
* Element.node
[ property (object) ]
**
* Gives you a reference to the DOM object, so you can assign event handlers or just mess around.
**
* Note: Don’t mess with it.
> Usage
| // draw a circle at coordinate 10,10 with radius of 10
| var c = paper.circle(10, 10, 10);
| c.node.onclick = function () {
| c.attr("fill", "red");
| };
\*/
this[0] = this.node = node;
/*\
* Element.raphael
[ property (object) ]
**
* Internal reference to @Raphael object. In case it is not available.
> Usage
| Raphael.el.red = function () {
| var hsb = this.paper.raphael.rgb2hsb(this.attr("fill"));
| hsb.h = 1;
| this.attr({fill: this.paper.raphael.hsb2rgb(hsb).hex});
| }
\*/
node.raphael = true;
/*\
* Element.id
[ property (number) ]
**
* Unique id of the element. Especially useful when you want to listen to events of the element,
* because all events are fired in format `..`. Also useful for @Paper.getById method.
\*/
this.id = guid();
node.raphaelid = this.id;
/**
* Method that returns a 5 letter/digit id, enough for 36^5 = 60466176 elements
* @returns {string} id
*/
function guid() {
return ("0000" + (Math.random()*Math.pow(36,5) << 0).toString(36)).slice(-5);
}
this.matrix = R.matrix();
this.realPath = null;
/*\
* Element.paper
[ property (object) ]
**
* Internal reference to “paper†where object drawn. Mainly for use in plugins and element extensions.
> Usage
| Raphael.el.cross = function () {
| this.attr({fill: "red"});
| this.paper.path("M10,10L50,50M50,10L10,50")
| .attr({stroke: "red"});
| }
\*/
this.paper = svg;
this.attrs = this.attrs || {};
this._ = {
transform: [],
sx: 1,
sy: 1,
deg: 0,
dx: 0,
dy: 0,
dirty: 1
};
!svg.bottom && (svg.bottom = this);
/*\
* Element.prev
[ property (object) ]
**
* Reference to the previous element in the hierarchy.
\*/
this.prev = svg.top;
svg.top && (svg.top.next = this);
svg.top = this;
/*\
* Element.next
[ property (object) ]
**
* Reference to the next element in the hierarchy.
\*/
this.next = null;
},
elproto = R.el;
Element.prototype = elproto;
elproto.constructor = Element;
R._engine.path = function (pathString, SVG) {
var el = $("path");
SVG.canvas && SVG.canvas.appendChild(el);
var p = new Element(el, SVG);
p.type = "path";
setFillAndStroke(p, {
fill: "none",
stroke: "#000",
path: pathString
});
return p;
};
/*\
* Element.rotate
[ method ]
**
* Deprecated! Use @Element.transform instead.
* Adds rotation by given angle around given point to the list of
* transformations of the element.
> Parameters
- deg (number) angle in degrees
- cx (number) #optional x coordinate of the centre of rotation
- cy (number) #optional y coordinate of the centre of rotation
* If cx & cy aren’t specified centre of the shape is used as a point of rotation.
= (object) @Element
\*/
elproto.rotate = function (deg, cx, cy) {
if (this.removed) {
return this;
}
deg = Str(deg).split(separator);
if (deg.length - 1) {
cx = toFloat(deg[1]);
cy = toFloat(deg[2]);
}
deg = toFloat(deg[0]);
(cy == null) && (cx = cy);
if (cx == null || cy == null) {
var bbox = this.getBBox(1);
cx = bbox.x + bbox.width / 2;
cy = bbox.y + bbox.height / 2;
}
this.transform(this._.transform.concat([["r", deg, cx, cy]]));
return this;
};
/*\
* Element.scale
[ method ]
**
* Deprecated! Use @Element.transform instead.
* Adds scale by given amount relative to given point to the list of
* transformations of the element.
> Parameters
- sx (number) horisontal scale amount
- sy (number) vertical scale amount
- cx (number) #optional x coordinate of the centre of scale
- cy (number) #optional y coordinate of the centre of scale
* If cx & cy aren’t specified centre of the shape is used instead.
= (object) @Element
\*/
elproto.scale = function (sx, sy, cx, cy) {
if (this.removed) {
return this;
}
sx = Str(sx).split(separator);
if (sx.length - 1) {
sy = toFloat(sx[1]);
cx = toFloat(sx[2]);
cy = toFloat(sx[3]);
}
sx = toFloat(sx[0]);
(sy == null) && (sy = sx);
(cy == null) && (cx = cy);
if (cx == null || cy == null) {
var bbox = this.getBBox(1);
}
cx = cx == null ? bbox.x + bbox.width / 2 : cx;
cy = cy == null ? bbox.y + bbox.height / 2 : cy;
this.transform(this._.transform.concat([["s", sx, sy, cx, cy]]));
return this;
};
/*\
* Element.translate
[ method ]
**
* Deprecated! Use @Element.transform instead.
* Adds translation by given amount to the list of transformations of the element.
> Parameters
- dx (number) horisontal shift
- dy (number) vertical shift
= (object) @Element
\*/
elproto.translate = function (dx, dy) {
if (this.removed) {
return this;
}
dx = Str(dx).split(separator);
if (dx.length - 1) {
dy = toFloat(dx[1]);
}
dx = toFloat(dx[0]) || 0;
dy = +dy || 0;
this.transform(this._.transform.concat([["t", dx, dy]]));
return this;
};
/*\
* Element.transform
[ method ]
**
* Adds transformation to the element which is separate to other attributes,
* i.e. translation doesn’t change `x` or `y` of the rectange. The format
* of transformation string is similar to the path string syntax:
| "t100,100r30,100,100s2,2,100,100r45s1.5"
* Each letter is a command. There are four commands: `t` is for translate, `r` is for rotate, `s` is for
* scale and `m` is for matrix.
*
* There are also alternative “absolute†translation, rotation and scale: `T`, `R` and `S`. They will not take previous transformation into account. For example, `...T100,0` will always move element 100 px horisontally, while `...t100,0` could move it vertically if there is `r90` before. Just compare results of `r90t100,0` and `r90T100,0`.
*
* So, the example line above could be read like “translate by 100, 100; rotate 30° around 100, 100; scale twice around 100, 100;
* rotate 45° around centre; scale 1.5 times relative to centreâ€. As you can see rotate and scale commands have origin
* coordinates as optional parameters, the default is the centre point of the element.
* Matrix accepts six parameters.
> Usage
| var el = paper.rect(10, 20, 300, 200);
| // translate 100, 100, rotate 45°, translate -100, 0
| el.transform("t100,100r45t-100,0");
| // if you want you can append or prepend transformations
| el.transform("...t50,50");
| el.transform("s2...");
| // or even wrap
| el.transform("t50,50...t-50-50");
| // to reset transformation call method with empty string
| el.transform("");
| // to get current value call it without parameters
| console.log(el.transform());
> Parameters
- tstr (string) #optional transformation string
* If tstr isn’t specified
= (string) current transformation string
* else
= (object) @Element
\*/
elproto.transform = function (tstr) {
var _ = this._;
if (tstr == null) {
return _.transform;
}
R._extractTransform(this, tstr);
this.clip && $(this.clip, {transform: this.matrix.invert()});
this.pattern && updatePosition(this);
this.node && $(this.node, {transform: this.matrix});
if (_.sx != 1 || _.sy != 1) {
var sw = this.attrs[has]("stroke-width") ? this.attrs["stroke-width"] : 1;
this.attr({"stroke-width": sw});
}
return this;
};
/*\
* Element.hide
[ method ]
**
* Makes element invisible. See @Element.show.
= (object) @Element
\*/
elproto.hide = function () {
if(!this.removed) this.node.style.display = "none";
return this;
};
/*\
* Element.show
[ method ]
**
* Makes element visible. See @Element.hide.
= (object) @Element
\*/
elproto.show = function () {
if(!this.removed) this.node.style.display = "";
return this;
};
/*\
* Element.remove
[ method ]
**
* Removes element from the paper.
\*/
elproto.remove = function () {
var node = getRealNode(this.node);
if (this.removed || !node.parentNode) {
return;
}
var paper = this.paper;
paper.__set__ && paper.__set__.exclude(this);
eve.unbind("raphael.*.*." + this.id);
if (this.gradient) {
paper.defs.removeChild(this.gradient);
}
R._tear(this, paper);
node.parentNode.removeChild(node);
// Remove custom data for element
this.removeData();
for (var i in this) {
this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null;
}
this.removed = true;
};
elproto._getBBox = function () {
if (this.node.style.display == "none") {
this.show();
var hide = true;
}
var canvasHidden = false,
containerStyle;
if (this.paper.canvas.parentElement) {
containerStyle = this.paper.canvas.parentElement.style;
} //IE10+ can't find parentElement
else if (this.paper.canvas.parentNode) {
containerStyle = this.paper.canvas.parentNode.style;
}
if(containerStyle && containerStyle.display == "none") {
canvasHidden = true;
containerStyle.display = "";
}
var bbox = {};
try {
bbox = this.node.getBBox();
} catch(e) {
// Firefox 3.0.x, 25.0.1 (probably more versions affected) play badly here - possible fix
bbox = {
x: this.node.clientLeft,
y: this.node.clientTop,
width: this.node.clientWidth,
height: this.node.clientHeight
}
} finally {
bbox = bbox || {};
if(canvasHidden){
containerStyle.display = "none";
}
}
hide && this.hide();
return bbox;
};
/*\
* Element.attr
[ method ]
**
* Sets the attributes of the element.
> Parameters
- attrName (string) attribute’s name
- value (string) value
* or
- params (object) object of name/value pairs
* or
- attrName (string) attribute’s name
* or
- attrNames (array) in this case method returns array of current values for given attribute names
= (object) @Element if attrsName & value or params are passed in.
= (...) value of the attribute if only attrsName is passed in.
= (array) array of values of the attribute if attrsNames is passed in.
= (object) object of attributes if nothing is passed in.
> Possible parameters
#
Please refer to the SVG specification for an explanation of these parameters.
o arrow-end (string) arrowhead on the end of the path. The format for string is `[-[-]]`. Possible types: `classic`, `block`, `open`, `oval`, `diamond`, `none`, width: `wide`, `narrow`, `medium`, length: `long`, `short`, `midium`.
o clip-rect (string) comma or space separated values: x, y, width and height
o cursor (string) CSS type of the cursor
o cx (number) the x-axis coordinate of the center of the circle, or ellipse
o cy (number) the y-axis coordinate of the center of the circle, or ellipse
o fill (string) colour, gradient or image
o fill-opacity (number)
o font (string)
o font-family (string)
o font-size (number) font size in pixels
o font-weight (string)
o height (number)
o href (string) URL, if specified element behaves as hyperlink
o opacity (number)
o path (string) SVG path string format
o r (number) radius of the circle, ellipse or rounded corner on the rect
o rx (number) horisontal radius of the ellipse
o ry (number) vertical radius of the ellipse
o src (string) image URL, only works for @Element.image element
o stroke (string) stroke colour
o stroke-dasharray (string) [“â€, “noneâ€, “`-`â€, “`.`â€, “`-.`â€, “`-..`â€, “`. `â€, “`- `â€, “`--`â€, “`- .`â€, “`--.`â€, “`--..`â€]
o stroke-linecap (string) [“`butt`â€, “`square`â€, “`round`â€]
o stroke-linejoin (string) [“`bevel`â€, “`round`â€, “`miter`â€]
o stroke-miterlimit (number)
o stroke-opacity (number)
o stroke-width (number) stroke width in pixels, default is '1'
o target (string) used with href
o text (string) contents of the text element. Use `\n` for multiline text
o text-anchor (string) [“`start`â€, “`middle`â€, “`end`â€], default is “`middle`â€
o title (string) will create tooltip with a given text
o transform (string) see @Element.transform
o width (number)
o x (number)
o y (number)
> Gradients
* Linear gradient format: “`‹angle›-‹colour›[-‹colour›[:‹offset›]]*-‹colour›`â€, example: “`90-#fff-#000`†– 90°
* gradient from white to black or “`0-#fff-#f00:20-#000`†– 0° gradient from white via red (at 20%) to black.
*
* radial gradient: “`r[(‹fx›, ‹fy›)]‹colour›[-‹colour›[:‹offset›]]*-‹colour›`â€, example: “`r#fff-#000`†–
* gradient from white to black or “`r(0.25, 0.75)#fff-#000`†– gradient from white to black with focus point
* at 0.25, 0.75. Focus point coordinates are in 0..1 range. Radial gradients can only be applied to circles and ellipses.
> Path String
#
## JavaScript API
The `sortable()` method must be invoked on valid containers, meaning they must match the containerSelector option.
`.sortable('enable')`
Enable all instantiated sortables in the set of matched elements
`.sortable('disable')`
Disable all instantiated sortables in the set of matched elements
`.sortable('refresh')`
Reset all cached element dimensions
`.sortable('destroy')`
Remove the sortable plugin from the set of matched elements
`.sortable('serialize')`
Serialize all selected containers. Returns a jQuery object . Use .get() to retrieve the array, if needed.
### Supported options
- `useAnimation`: Use animation when an item is removed or inserted into the tree.
- `usePlaceholderClone`: Placeholder should be a clone of the item being dragged.
- `afterMove`: This is executed after the placeholder has been moved. $closestItemOrContainer contains the closest item, the placeholder has been put at or the closest empty Container, the placeholder has been appended to.
- `containerPath`: The exact css path between the container and its items, e.g. "> tbody"
- `containerSelector`: The css selector of the containers
- `distance`: Distance the mouse has to travel to start dragging
- `delay`: Time in milliseconds after mousedown until dragging should start. This option can be used to prevent unwanted drags when clicking on an element.
- `handle`: The css selector of the drag handle
- `itemPath`: The exact css path between the item and its subcontainers. It should only match the immediate items of a container. No item of a subcontainer should be matched. E.g. for ol>div>li the itemPath is "> div"
- `itemSelector`: The css selector of the items
- `bodyClass`: The class given to "body" while an item is being dragged
- `draggedClass`: The class giving to an item while being dragged
- `isValidTarget`: Check if the dragged item may be inside the container. Use with care, since the search for a valid container entails a depth first search and may be quite expensive.
- `onCancel`: Executed before onDrop if placeholder is detached. This happens if pullPlaceholder is set to false and the drop occurs outside a container.
- `onDrag`: Executed at the beginning of a mouse move event. The Placeholder has not been moved yet.
- `onDragStart`: Called after the drag has been started, that is the mouse button is being held down and the mouse is moving. The container is the closest initialized container. Therefore it might not be the container, that actually contains the item.
- `onDrop`: Called when the mouse button is being released
- `onMousedown`: Called on mousedown. If falsy value is returned, the dragging will not start. Ignore if element clicked is input, select or textarea
- `placeholderClass`: The class of the placeholder (must match placeholder option markup)
- `placeholder`: Template for the placeholder. Can be any valid jQuery input e.g. a string, a DOM element. The placeholder must have the class "placeholder"
- `pullPlaceholder`: If true, the position of the placeholder is calculated on every mousemove. If false, it is only calculated when the mouse is above a container.
- `serialize`: Specifies serialization of the container group. The pair $parent/$children is either container/items or item/subcontainers.
- `tolerance`: Set tolerance while dragging. Positive values decrease sensitivity, negative values increase it.
### Supported options (container specific)
- `drag`: If true, items can be dragged from this container
- `drop`: If true, items can be droped onto this container
- `exclude`: Exclude items from being draggable, if the selector matches the item
- `nested`: If true, search for nested containers within an item.If you nest containers, either the original selector with which you call the plugin must only match the top containers, or you need to specify a group (see the bootstrap nav example)
- `vertical`: If true, the items are assumed to be arranged vertically
================================================
FILE: modules/backend/assets/foundation/scripts/drag/drag.scroll.js
================================================
/*
* Allows to scroll an element content in the horizontal or horizontal directions. This script doesn't use
* absolute positioning and rely on the scrollLeft/scrollTop DHTML properties. The element width should be
* fixed with the CSS or JavaScript.
*
* Events triggered on the element:
* - start.oc.dragScroll
* - drag.oc.dragScroll
* - stop.oc.dragScroll
*
* Options:
* - start - callback function to execute when the drag starts
* - drag - callback function to execute when the element is dragged
* - stop - callback function to execute when the drag ends
* - vertical - determines if the scroll direction is vertical, true by default
* - scrollClassContainer - if specified, specifies an element or element selector to apply the 'scroll-before' and 'scroll-after' CSS classes,
* depending on whether the scrollable area is in its start or end
* - scrollMarkerContainer - if specified, specifies an element or element selector to inject scroll markers (span elements that con
* contain the ellipses icon, indicating whether scrolling is possible)
* - useDrag - determines if dragging is allowed support, true by default
* - useNative - if native CSS is enabled via "mobile" on the HTML tag, false by default
* - useScroll - determines if the mouse wheel scrolling is allowed, true by default
* - useComboScroll - determines if horizontal scroll should act as vertical, and vice versa, true by default
* - dragSelector - restrict drag events to this selector
* - scrollSelector - restrict scroll events to this selector
*
* Methods:
* - isStart - determines if the scrollable area is in its start (left or top)
* - isEnd - determines if the scrollable area is in its end (right or bottom)
* - goToStart - moves the scrollable area to the start (left or top)
* - goToElement - moves the scrollable area to an element
*
* Require:
* - mousewheel/mousewheel
*/
+(function($) {
'use strict';
var Base = $.oc.foundation.base,
BaseProto = Base.prototype;
var DragScroll = function(element, options) {
this.options = $.extend({}, DragScroll.DEFAULTS, options);
this.touchDragStarted = false;
this.onTouchMove = onTouchMove;
var $el = $(element),
el = $el.get(0),
dragStart = 0,
startOffset = 0,
self = this,
dragging = false,
eventElementName = this.options.vertical ? 'pageY' : 'pageX',
isNative = this.options.useNative && $('html').hasClass('mobile');
this.el = $el;
this.scrollClassContainer = this.options.scrollClassContainer ? $(this.options.scrollClassContainer) : $el;
this.isScrollable = true;
Base.call(this);
// Inject scroll markers
if (this.options.scrollMarkerContainer) {
$(this.options.scrollMarkerContainer).append(
$('')
);
}
// Bind events
var $scrollSelect = this.options.scrollSelector ? $(this.options.scrollSelector, $el) : $el;
$scrollSelect.mousewheel(function(event) {
if (!self.options.useScroll || self.paused) {
return;
}
var offset,
offsetX = event.deltaFactor * event.deltaX,
offsetY = event.deltaFactor * event.deltaY;
if (!offsetX && self.options.useComboScroll) {
offset = offsetY * -1;
}
else if (!offsetY && self.options.useComboScroll) {
offset = offsetX;
}
else {
offset = self.options.vertical ? offsetY * -1 : offsetX;
}
var scrolled = scrollWheel(offset);
if (!scrolled && self.options.noOverScroll) {
event.preventDefault();
event.stopPropagation();
}
return !scrolled
});
if (this.options.useDrag) {
$el.on('mousedown.dragScroll', this.options.dragSelector, function(event) {
if (self.paused) {
return;
}
// Don't prevent clicking inputs in the toolbar
if (event.target && event.target.tagName === 'INPUT') {
return;
}
if (!self.isScrollable) {
return;
}
startDrag(event);
return false;
});
}
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
$el.on('touchstart.dragScroll', this.options.dragSelector, function (event) {
if (self.paused) {
return;
}
var touchEvent = event.originalEvent;
if (touchEvent.touches.length == 1) {
startDrag(touchEvent.touches[0]);
self.touchDragStarted = true;
event.stopPropagation();
}
});
window.addEventListener('touchmove', self.onTouchMove, { passive: false })
}
$el.on('click.dragScroll', function() {
// Do not handle item clicks while dragging
if ($(document.body).hasClass(self.options.dragClass)) {
return false;
}
});
if (!this.options.noScrollClasses) {
$(document).on('ready', this.proxy(this.fixScrollClasses));
$(window).on('resize', this.proxy(this.fixScrollClasses));
this.el.on('scroll', this.proxy(this.fixScrollClasses));
}
/*
* Internal event, drag has started
*/
function startDrag(event) {
if (self.paused) {
return;
}
dragStart = event[eventElementName];
startOffset = self.options.vertical ? $el.scrollTop() : $el.scrollLeft();
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
$(window).on('touchend.dragScroll', function(event) {
stopDrag();
});
}
$(window).on('mousemove.dragScroll', function(event) {
moveDrag(event);
return false;
});
$(window).on('mouseup.dragScroll', function(mouseUpEvent) {
var isClick = event.pageX == mouseUpEvent.pageX && event.pageY == mouseUpEvent.pageY;
stopDrag(isClick);
return false;
});
}
function onTouchMove(event) {
if (!self.touchDragStarted) {
return;
}
var touchEvent = event
moveDrag(touchEvent.touches[0])
if (!isNative) {
event.preventDefault()
}
}
/*
* Internal event, drag is active
*/
function moveDrag(event) {
var current = event[eventElementName],
offset = dragStart - current;
if (Math.abs(offset) > 3) {
if (!dragging) {
dragging = true;
$el.trigger('start.oc.dragScroll');
self.options.start();
$(document.body).addClass(self.options.dragClass);
}
if (!isNative) {
self.options.vertical ? $el.scrollTop(startOffset + offset) : $el.scrollLeft(startOffset + offset);
}
self.fixScrollClasses(true);
$el.trigger('drag.oc.dragScroll');
self.options.drag();
}
}
/*
* Internal event, drag has ended
*/
function stopDrag(click) {
$(window).off('.dragScroll');
self.touchDragStarted = false;
dragging = false;
if (click) {
$(document.body).removeClass(self.options.dragClass);
}
else {
self.fixScrollClasses();
}
window.setTimeout(function() {
if (!click) {
$(document.body).removeClass(self.options.dragClass);
$el.trigger('stop.oc.dragScroll');
self.options.stop();
self.fixScrollClasses();
}
}, 100);
}
/*
* Scroll wheel has moved by supplied offset
*/
function scrollWheel(offset) {
if (self.paused) {
return;
}
startOffset = self.options.vertical ? el.scrollTop : el.scrollLeft;
self.options.vertical ? $el.scrollTop(startOffset + offset) : $el.scrollLeft(startOffset + offset);
var scrolled = self.options.vertical ? el.scrollTop != startOffset : el.scrollLeft != startOffset;
$el.trigger('drag.oc.dragScroll');
self.options.drag();
if (scrolled) {
if (self.wheelUpdateTimer !== undefined && self.wheelUpdateTimer !== false)
window.clearInterval(self.wheelUpdateTimer);
self.wheelUpdateTimer = window.setTimeout(function() {
self.wheelUpdateTimer = false;
self.fixScrollClasses();
}, 100);
}
return scrolled;
}
this.fixScrollClasses();
};
DragScroll.prototype.dispose = function() {
clearTimeout(this.fixScrollClassesIntervalId);
this.scrollClassContainer = null;
if (!this.options.noScrollClasses) {
$(document).off('ready', this.proxy(this.fixScrollClasses));
$(window).off('resize', this.proxy(this.fixScrollClasses));
this.el.off('scroll', this.proxy(this.fixScrollClasses));
}
this.el.off('.dragScroll');
this.el.removeData('oc.dragScroll');
window.removeEventListener('touchmove', this.onTouchMove, {passive: false})
this.el = null;
BaseProto.dispose.call(this);
};
DragScroll.prototype = Object.create(BaseProto);
DragScroll.prototype.constructor = DragScroll;
DragScroll.DEFAULTS = {
vertical: false,
useDrag: true,
useScroll: true,
useNative: false,
useComboScroll: true,
scrollClassContainer: false,
scrollMarkerContainer: false,
scrollSelector: null,
dragSelector: null,
noOverScroll: false,
dragClass: 'drag',
start: function() {},
drag: function() {},
stop: function() {}
};
DragScroll.prototype.fixScrollClasses = function(isThrottle) {
if (this.options.noScrollClasses) {
return;
}
if (this.fixScrollClassesIntervalId) {
if (isThrottle) {
return;
}
clearTimeout(this.fixScrollClassesIntervalId);
this.fixScrollClassesIntervalId = null;
}
var that = this;
this.fixScrollClassesIntervalId = window.setTimeout(function() {
that.fixScrollClassesIntervalId = null;
var isStart = that.isStart(),
isEnd = that.isEnd();
that.scrollClassContainer.toggleClass('scroll-before', !isStart);
that.scrollClassContainer.toggleClass('scroll-after', !isEnd);
that.scrollClassContainer.toggleClass('scroll-active-before', that.isActiveBefore());
that.scrollClassContainer.toggleClass('scroll-active-after', that.isActiveAfter());
that.isScrollable = !isStart || !isEnd;
}, 30);
};
DragScroll.prototype.isStart = function() {
if (!this.options.vertical) {
return this.el.scrollLeft() <= 0;
}
else {
return this.el.scrollTop() <= 0;
}
};
DragScroll.prototype.isEnd = function() {
// Fudge factor for retina displays
var offset = 1;
if (!this.options.vertical) {
return this.el[0].scrollWidth - (this.el.scrollLeft() + this.el.outerWidth()) - offset <= 0;
}
else {
return this.el[0].scrollHeight - (this.el.scrollTop() + this.el.outerHeight()) - offset <= 0;
}
};
DragScroll.prototype.goToStart = function() {
if (!this.options.vertical) {
return this.el.scrollLeft(0);
}
else {
return this.el.scrollTop(0);
}
};
/*
* Determines if the element with the class 'active' is hidden before the viewport -
* on the left or on the top, depending on whether the scrollbar is horizontal or vertical.
*/
DragScroll.prototype.isActiveAfter = function() {
var activeElement = $('.active', this.el);
if (activeElement.length == 0) {
return false;
}
if (!this.options.vertical) {
return activeElement.get(0).offsetLeft > this.el.scrollLeft() + this.el.width();
}
else {
return activeElement.get(0).offsetTop > this.el.scrollTop() + this.el.height();
}
};
/*
* Determines if the element with the class 'active' is hidden after the viewport -
* on the right or on the bottom, depending on whether the scrollbar is horizontal or vertical.
*/
DragScroll.prototype.isActiveBefore = function() {
var activeElement = $('.active', this.el);
if (activeElement.length == 0) {
return false;
}
if (!this.options.vertical) {
return activeElement.get(0).offsetLeft + activeElement.width() < this.el.scrollLeft();
}
else {
return activeElement.get(0).offsetTop + activeElement.height() < this.el.scrollTop();
}
};
DragScroll.prototype.goToElement = function(element, callback, options) {
var $el = $(element);
if (!$el.length) return;
var self = this,
params = {
duration: 300,
queue: false,
complete: function() {
self.fixScrollClasses();
if (callback !== undefined) callback();
}
};
params = $.extend(params, options || {});
var offset = 0,
animated = false;
if (!this.options.vertical) {
offset = $el.get(0).offsetLeft - this.el.scrollLeft();
if (offset < 0) {
this.el.animate({ scrollLeft: $el.get(0).offsetLeft }, params);
animated = true;
}
else {
offset = $el.get(0).offsetLeft + $el.width() - (this.el.scrollLeft() + this.el.width());
if (offset > 0) {
this.el.animate({ scrollLeft: $el.get(0).offsetLeft + $el.width() - this.el.width() }, params);
animated = true;
}
}
}
else {
offset = $el.get(0).offsetTop - this.el.scrollTop();
if (offset < 0) {
this.el.animate({ scrollTop: $el.get(0).offsetTop }, params);
animated = true;
}
else {
var heightOffset = 0;
if (params.alignBottom) {
heightOffset = $el.height();
}
offset = $el.get(0).offsetTop + heightOffset - (this.el.scrollTop() + this.el.height());
if (offset > 0) {
this.el.animate(
{ scrollTop: $el.get(0).offsetTop + $el.height() - this.el.height() + heightOffset },
params
);
animated = true;
}
}
}
if (!animated && callback !== undefined) {
callback();
}
};
DragScroll.prototype.pause = function() {
this.paused = true;
};
DragScroll.prototype.resume = function() {
this.paused = false;
};
// DRAGSCROLL PLUGIN DEFINITION
// ============================
var old = $.fn.dragScroll;
$.fn.dragScroll = function(option) {
var args = arguments;
return this.each(function() {
var $this = $(this);
var data = $this.data('oc.dragScroll');
var options = typeof option == 'object' && option;
if (!data) $this.data('oc.dragScroll', (data = new DragScroll(this, options)));
if (typeof option == 'string') {
var methodArgs = [];
for (var i = 1; i < args.length; i++) methodArgs.push(args[i]);
data[option].apply(data, methodArgs);
}
});
};
$.fn.dragScroll.Constructor = DragScroll;
// DRAGSCROLL NO CONFLICT
// =================
$.fn.dragScroll.noConflict = function() {
$.fn.dragScroll = old;
return this;
};
})(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/scripts/drag/drag.sort.js
================================================
/*
* Sortable plugin.
*
* Documentation: ../docs/drag-sort.md
*
* Require:
* - sortable/jquery-sortable
*/
+function ($) { "use strict";
var Base = $.oc.foundation.base,
BaseProto = Base.prototype
var Sortable = function (element, options) {
this.$el = $(element)
this.options = options || {}
this.cursorAdjustment = null
$.oc.foundation.controlUtils.markDisposable(element)
Base.call(this)
this.init()
}
Sortable.prototype = Object.create(BaseProto)
Sortable.prototype.constructor = Sortable
Sortable.prototype.init = function() {
this.$el.one('dispose-control', this.proxy(this.dispose))
var
self = this,
sortableOverrides = {},
sortableDefaults = {
onDragStart: this.proxy(this.onDragStart),
onDrag: this.proxy(this.onDrag),
onDrop: this.proxy(this.onDrop)
}
/*
* Override _super object for each option/event
*/
if (this.options.onDragStart) {
sortableOverrides.onDragStart = function ($item, container, _super, event) {
self.options.onDragStart($item, container, sortableDefaults.onDragStart, event)
}
}
if (this.options.onDrag) {
sortableOverrides.onDrag = function ($item, position, _super, event) {
self.options.onDrag($item, position, sortableDefaults.onDrag, event)
}
}
if (this.options.onDrop) {
sortableOverrides.onDrop = function ($item, container, _super, event) {
self.options.onDrop($item, container, sortableDefaults.onDrop, event)
}
}
this.$el.jqSortable($.extend({}, sortableDefaults, this.options, sortableOverrides))
}
Sortable.prototype.dispose = function() {
this.$el.jqSortable('destroy')
this.$el.off('dispose-control', this.proxy(this.dispose))
this.$el.removeData('oc.sortable')
this.$el = null
this.options = null
this.cursorAdjustment = null
BaseProto.dispose.call(this)
}
Sortable.prototype.onDragStart = function ($item, container, _super, event) {
/*
* Relative cursor position
*/
var offset = $item.offset(),
pointer = container.rootGroup.pointer
if (pointer) {
this.cursorAdjustment = {
left: pointer.left - offset.left,
top: pointer.top - offset.top
}
}
else {
this.cursorAdjustment = null
}
if (this.options.tweakCursorAdjustment) {
this.cursorAdjustment = this.options.tweakCursorAdjustment(this.cursorAdjustment)
}
$item.css({
height: $item.height(),
width: $item.width()
})
$item.addClass('dragged')
$('body').addClass('dragging')
this.$el.addClass('dragging')
/*
* Use animation
*/
if (this.options.useAnimation) {
$item.data('oc.animated', true)
}
/*
* Placeholder clone
*/
if (this.options.usePlaceholderClone) {
$(container.rootGroup.placeholder).html($item.html())
}
if (!this.options.useDraggingClone) {
$item.hide()
}
}
Sortable.prototype.onDrag = function ($item, position, _super, event) {
if (this.cursorAdjustment) {
/*
* Relative cursor position
*/
$item.css({
left: position.left - this.cursorAdjustment.left,
top: position.top - this.cursorAdjustment.top
})
}
else {
/*
* Default behavior
*/
$item.css(position)
}
}
Sortable.prototype.onDrop = function ($item, container, _super, event) {
$item.removeClass('dragged').removeAttr('style')
$('body').removeClass('dragging')
this.$el.removeClass('dragging')
if ($item.data('oc.animated')) {
$item
.hide()
.slideDown(200)
}
}
//
// Proxy API
//
Sortable.prototype.enable = function() {
this.$el.jqSortable('enable')
}
Sortable.prototype.disable = function() {
this.$el.jqSortable('disable')
}
Sortable.prototype.refresh = function() {
this.$el.jqSortable('refresh')
}
Sortable.prototype.serialize = function() {
this.$el.jqSortable('serialize')
}
Sortable.prototype.destroy = function() {
this.dispose()
}
// External solution for group persistence
// See https://github.com/johnny/jquery-sortable/pull/122
Sortable.prototype.destroyGroup = function() {
var jqSortable = this.$el.data('jqSortable')
if (jqSortable.group) {
jqSortable.group._destroy()
}
}
Sortable.DEFAULTS = {
useAnimation: false,
usePlaceholderClone: false,
useDraggingClone: true,
tweakCursorAdjustment: null
}
// PLUGIN DEFINITION
// ============================
var old = $.fn.sortable
$.fn.sortable = function (option) {
var args = arguments;
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.sortable')
var options = $.extend({}, Sortable.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.sortable', (data = new Sortable(this, options)))
if (typeof option == 'string') data[option].apply(data, args)
})
}
$.fn.sortable.Constructor = Sortable
$.fn.sortable.noConflict = function () {
$.fn.sortable = old
return this
}
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/scripts/drag/drag.value.js
================================================
/*
* Drag Value plugin
*
* Uses native dragging to allow elements to be dragged in to inputs, textareas, etc
*
* Data attributes:
* - data-control="dragvalue" - enables the plugin on an element
* - data-text-value="text to include" - text value to include when dragging
* - data-drag-click="false" - allow click event, tries to cache the last active element
* and insert the text at the current cursor position
*
* JavaScript API:
* $('a#someElement').dragValue({ textValue: 'insert this text' })
*
*/
+function ($) { "use strict";
// DRAG VALUE CLASS DEFINITION
// ============================
var DragValue = function(element, options) {
this.options = options
this.$el = $(element)
// Init
this.init()
}
DragValue.DEFAULTS = {
dragClick: false
}
DragValue.prototype.init = function() {
this.$el.prop('draggable', true)
this.textValue = this.$el.data('textValue')
this.$el.on('dragstart', $.proxy(this.handleDragStart, this))
this.$el.on('drop', $.proxy(this.handleDrop, this))
this.$el.on('dragend', $.proxy(this.handleDragEnd, this))
if (this.options.dragClick) {
this.$el.on('click', $.proxy(this.handleClick, this))
this.$el.on('mouseover', $.proxy(this.handleMouseOver, this))
}
}
//
// Drag events
//
DragValue.prototype.handleDragStart = function(event) {
var e = event.originalEvent
e.dataTransfer.effectAllowed = 'all'
e.dataTransfer.setData('text/plain', this.textValue)
this.$el
.css({ opacity: 0.5 })
.addClass('dragvalue-dragging')
}
DragValue.prototype.handleDrop = function(event) {
event.stopPropagation()
return false
}
DragValue.prototype.handleDragEnd = function(event) {
this.$el
.css({ opacity: 1 })
.removeClass('dragvalue-dragging')
}
//
// Click events
//
DragValue.prototype.handleMouseOver = function(event) {
var el = document.activeElement
if (!el) return
if (el.isContentEditable || (
el.tagName.toLowerCase() == 'input' &&
el.type == 'text' ||
el.tagName.toLowerCase() == 'textarea'
)) {
this.lastElement = el
}
}
DragValue.prototype.handleClick = function(event) {
if (!this.lastElement) return
var $el = $(this.lastElement)
if ($el.hasClass('ace_text-input'))
return this.handleClickCodeEditor(event, $el)
if (this.lastElement.isContentEditable)
return this.handleClickContentEditable()
this.insertAtCaret(this.lastElement, this.textValue)
}
DragValue.prototype.handleClickCodeEditor = function(event, $el) {
var $editorArea = $el.closest('[data-control=codeeditor]')
if (!$editorArea.length) return
$editorArea.codeEditor('getEditorObject').insert(this.textValue)
}
DragValue.prototype.handleClickContentEditable = function() {
var sel, range, html;
if (window.getSelection) {
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0);
range.deleteContents();
range.insertNode( document.createTextNode(this.textValue) );
}
}
else if (document.selection && document.selection.createRange) {
document.selection.createRange().text = this.textValue;
}
}
//
// Helpers
//
DragValue.prototype.insertAtCaret = function(el, insertValue) {
// IE
if (document.selection) {
el.focus()
var sel = document.selection.createRange()
sel.text = insertValue
el.focus()
}
// Real browsers
else if (el.selectionStart || el.selectionStart == '0') {
var startPos = el.selectionStart, endPos = el.selectionEnd, scrollTop = el.scrollTop
el.value = el.value.substring(0, startPos) + insertValue + el.value.substring(endPos, el.value.length)
el.focus()
el.selectionStart = startPos + insertValue.length
el.selectionEnd = startPos + insertValue.length
el.scrollTop = scrollTop
}
else {
el.value += insertValue
el.focus()
}
}
// DRAG VALUE PLUGIN DEFINITION
// ============================
var old = $.fn.dragValue
$.fn.dragValue = function (option) {
var args = Array.prototype.slice.call(arguments, 1), result
this.each(function () {
var $this = $(this)
var data = $this.data('oc.dragvalue')
var options = $.extend({}, DragValue.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.dragvalue', (data = new DragValue(this, options)))
if (typeof option == 'string') result = data[option].apply(data, args)
if (typeof result != 'undefined') return false
})
return result ? result : this
}
$.fn.dragValue.Constructor = DragValue
// DRAG VALUE NO CONFLICT
// =================
$.fn.dragValue.noConflict = function () {
$.fn.dragValue = old
return this
}
// DRAG VALUE DATA-API
// ===============
$(document).render(function() {
$('[data-control="dragvalue"]').dragValue()
});
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/scripts/foundation/README.md
================================================
# Foundation
The foundation libraries are the core base of all scripts and controls. The goals of this library are:
- Well structured and readable code.
- Don't leave references to DOM elements.
- Unbind all event handlers.
- Write high-performance code (in cases when it's needed).
That's especially important on pages where users spend much time interacting with the page, like the CMS and Pages sections, but all back-end controls should follow these rules, because we never know when they are used.
## Why it's important to release the memory, DOM references and event handlers
A typical JavaScript control class instance consists of the following parts:
1. JavaScript object representing the control.
1. A reference to the corresponding DOM element. Usually it's the control's root element containing a tree with the control HTML markup.
1. A number of event handlers to handle user's interaction with the control.
If any of that components are not released we have these problems:
1. Non-released JavaScript objects increase the memory footprint. The more memory the application uses, the slower it works. Eventually it could result in a crashed tab or entire browser.
1. Non-released references to DOM elements could result in detached DOM trees. That, in turn, could result in thousands of invisible DOM elements living in a page, increasing the memory footprint and making the application less responsive.
1. Unbound event handlers usually result in non-released DOM elements, which is bad by itself, and also in the code which executes when the user interacts with the application and which should not be executed. That affects the performance.
## This is how to deal with those problems:
1. Remove the JavaScript object - usually by removing the data from the control's root element: `this.$el.removeData('oc.myControl')`
Clean all references to DOM elements. Usually it's done by assigning NULL to corresponding object properties.
1. Watch for any references caught by closures (or - better do not use closures, see below).
1. Unbind event handlers.
October Storm provides everything we need to meet the goals. Please read on to learn more!
## How to write quality code
OOP approach and prototypes should be used in all places. This approach automatically deals with closures that could retain references to scope variables. Typical class code template:
```js
function ($) { "use strict";
var SomeClass = function() {
this.init()
}
SomeClass.prototype.init = function (){
...
}
}
```
## Basics of writing disposable classes
If a class should be disposable (all UI controls should be disposable), the class should extend `$.oc.foundation.base` class. That class has two useful methods: `proxy(method)` and `dispose()`.
`proxy()` method is an alternative to jQuery's `$.proxy`, but as `$.oc.foundation.base` implements OOP approach, passing this parameter to the method is not required. This method is good for three reasons.
1. It's code is very simple and easily controllable and debuggable.
1. It caches bound functions and doesn't create new function as `$.proxy` does.
1. It automatically removes all cached bound functions when the object is disposed with dispose() method.
`dispose()` method in the base class cleans up bound methods cached by `proxy()` method and provides a common API for disposing objects. All classes that are supposed to do clean-up work, should override that method, do their own clean-up and call the base `dispose()` method.
Example of a disposable class:
```js
+function ($) { "use strict";
var Base = $.oc.foundation.base,
BaseProto = Base.prototype
var SomeDisposableClass = function(element) {
this.$el = $(element)
Base.call(this)
this.init()
}
SomeDisposableClass.prototype = Object.create(BaseProto)
SomeDisposableClass.prototype.constructor = SomeDisposableClass
SomeDisposableClass.prototype.init = function () {
}
SomeDisposableClass.prototype.dispose = function () {
this.$el = null
BaseProto.dispose.call(this)
}
}
```
A couple of important things to note:
1. The class constructor should call Base.call(this).
1. The class prototype should be replaced with a copy of the Base class prototype, and its constructor reference should be restored back to the class constructor. It should be done right after the class constructor and before any method is defined in the class prototype.
## Binding and unbinding events
When binding events, use this.proxy() to make references to event handlers. Always unbind events in dispose() method:
```js
+function ($) { "use strict";
var Base = $.oc.foundation.base,
BaseProto = Base.prototype
var SomeDisposableClass = function(element) {
this.$el = $(element)
Base.call(this)
this.init()
}
[...]
SomeDisposableClass.prototype.init = function () {
this.$el.on('click', this.proxy(this.onClick))
}
SomeDisposableClass.prototype.dispose = function () {
this.$el.off('click', this.proxy(this.onClick))
this.$el = null
BaseProto.dispose.call(this)
}
}
```
## Making disposable controls
UI controls should support two ways of disposing - with calling their `dispose()` method and with invoking the dispose-control handler. Also, disposable controls should mark their corresponding DOM elements as disposable, with October foundation API. Example:
```js
+function ($) { "use strict";
var Base = $.oc.foundation.base,
BaseProto = Base.prototype
var SomeDisposableControl = function(element) {
this.$el = $(element)
$.oc.foundation.controlUtils.markDisposable(element)
Base.call(this)
this.init()
}
...
SomeDisposableControl.prototype.init = function () {
this.$el.one('dispose-control', this.proxy(this.dispose))
}
SomeDisposableControl.prototype.dispose = function () {
this.$el.off('dispose-control', this.proxy(this.dispose))
this.$el = null
BaseProto.dispose.call(this)
}
}
```
`$.oc.foundation.controlUtils.markDisposable(element)` call in the constructor adds `data-disposable` attribute to the DOM element, allowing the framework to find all disposable elements in a container and dispose them by calling their dispose-control handler when it's required.
## Full example of a jQuery plugin that creates a disposable control
We already have a boilerplate code for jQuery code. Disposable controls approach just extends it. Don't forget to remove the data associated with controls from their DOM elements.
```js
+function ($) { "use strict";
var Base = $.oc.foundation.base,
BaseProto = Base.prototype
var SomeDisposableControl = function (element, options) {
this.$el = $(element)
this.options = options || {}
$.oc.foundation.controlUtils.markDisposable(element)
Base.call(this)
this.init()
}
SomeDisposableControl.prototype = Object.create(BaseProto)
SomeDisposableControl.prototype.constructor = SomeDisposableControl
SomeDisposableControl.prototype.init = function() {
this.$el.on('click', this.proxy(this.onClick))
this.$el.one('dispose-control', this.proxy(this.dispose))
}
SomeDisposableControl.prototype.dispose = function() {
this.$el.off('click', this.proxy(this.onClick))
this.$el.off('dispose-control', this.proxy(this.dispose))
this.$el.removeData('oc.someDisposableControl')
this.$el = null
// In some cases options could contain callbacks,
// so it's better to clean them up too.
this.options = null
BaseProto.dispose.call(this)
}
SomeDisposableControl.DEFAULTS = {
someParam: null
}
// PLUGIN DEFINITION
// ============================
var old = $.fn.someDisposableControl
$.fn.someDisposableControl = function (option) {
var args = Array.prototype.slice.call(arguments, 1), items, result
items = this.each(function () {
var $this = $(this)
var data = $this.data('oc.someDisposableControl')
var options = $.extend({}, SomeDisposableControl.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.someDisposableControl', (data = new SomeDisposableControl(this, options)))
if (typeof option == 'string') result = data[option].apply(data, args)
if (typeof result != 'undefined') return false
})
return result ? result : items
}
$.fn.someDisposableControl.Constructor = SomeDisposableControl
$.fn.someDisposableControl.noConflict = function () {
$.fn.someDisposableControl = old
return this
}
// Add this only if required
$(document).render(function (){
$('[data-some-disposable-control]').someDisposableControl()
})
}(window.jQuery);
```
================================================
FILE: modules/backend/assets/foundation/scripts/foundation/foundation.baseclass.js
================================================
/*
* October JavaScript foundation library.
*
* Base class for OctoberCMS back-end classes.
*
* The class defines base functionality for dealing with memory management
* and cleaning up bound (proxied) methods.
*
* The base class defines the dispose method that cleans up proxied methods.
* If child classes implement their own dispose() method, they should call
* the base class dispose method (see the example below).
*
* Use the simple parasitic combination inheritance pattern to create child classes:
*
* var Base = $.oc.foundation.base,
* BaseProto = Base.prototype
*
* var SubClass = function(params) {
* // Call the parent constructor
* Base.call(this)
* }
*
* SubClass.prototype = Object.create(BaseProto)
* SubClass.prototype.constructor = SubClass
*
* // Child class methods can be defined only after the
* // prototype is updated in the two previous lines
*
* SubClass.prototype.dispose = function() {
* // Call the parent method
* BaseProto.dispose.call(this)
* };
*
* See:
*
* - https://developers.google.com/speed/articles/optimizing-javascript
* - http://javascriptissexy.com/oop-in-javascript-what-you-need-to-know/
* - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Introduction_to_Object-Oriented_JavaScript
*
*/
+function ($) { "use strict";
if ($.oc === undefined)
$.oc = {}
if ($.oc.foundation === undefined)
$.oc.foundation = {}
$.oc.foundation._proxyCounter = 0
var Base = function() {
this.proxiedMethods = {}
}
Base.prototype.dispose = function() {
for (var key in this.proxiedMethods) {
this.proxiedMethods[key] = null
}
this.proxiedMethods = null
}
/*
* Creates a proxied method reference or returns an existing proxied method.
*/
Base.prototype.proxy = function(method) {
if (method.ocProxyId === undefined) {
$.oc.foundation._proxyCounter++
method.ocProxyId = $.oc.foundation._proxyCounter
}
if (this.proxiedMethods[method.ocProxyId] !== undefined)
return this.proxiedMethods[method.ocProxyId]
this.proxiedMethods[method.ocProxyId] = method.bind(this)
return this.proxiedMethods[method.ocProxyId]
}
$.oc.foundation.base = Base;
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/scripts/foundation/foundation.controlutils.js
================================================
/*
* October JavaScript foundation library.
*
* Utility functions for working back-end client-side UI controls.
*
* Usage examples:
*
* $.oc.foundation.controlUtils.markDisposable(el)
* $.oc.foundation.controlUtils.disposeControls(container)
*
*/
+function ($) { "use strict";
if ($.oc === undefined)
$.oc = {}
if ($.oc.foundation === undefined)
$.oc.foundation = {}
var ControlUtils = {
markDisposable: function(el) {
el.setAttribute('data-disposable', '')
},
/*
* Destroys all disposable controls in a container.
* The disposable controls should watch the dispose-control
* event.
*/
disposeControls: function(container) {
if (container === document) {
container = document.documentElement;
}
var controls = container.querySelectorAll('[data-disposable]')
for (var i=0, len=controls.length; i= elementPosition.left && point.x <= elementRight
&& point.y >= elementPosition.top && point.y <= elementBottom
}
}
$.oc.foundation.element = Element;
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/scripts/foundation/foundation.event.js
================================================
/*
* October JavaScript foundation library.
*
* Light-weight utility functions for working with native DOM events. The functions
* work with events directly, without jQuery, using the native JavaScript and DOM
* features.
*
* Usage examples:
*
* $.oc.foundation.event.stop(ev)
*
*/
+function ($) { "use strict";
if ($.oc === undefined)
$.oc = {}
if ($.oc.foundation === undefined)
$.oc.foundation = {}
var Event = {
/*
* Returns the event target element.
* If the second argument is provided (string), the function
* will try to find the first parent with the tag name matching
* the argument value.
*/
getTarget: function(ev, tag) {
var target = ev.target ? ev.target : ev.srcElement;
if (tag === undefined) {
return target;
}
var tagName = target.tagName;
while (tagName != tag) {
target = target.parentNode;
if (!target) {
return null;
}
tagName = target.tagName;
}
return target;
},
stop: function(ev) {
if (ev.stopPropagation) {
ev.stopPropagation();
}
else {
ev.cancelBubble = true;
}
if (ev.preventDefault) {
ev.preventDefault();
}
else {
ev.returnValue = false;
}
},
pageCoordinates: function(ev) {
if (ev.pageX || ev.pageY) {
return {
x: ev.pageX,
y: ev.pageY
};
}
else if (ev.clientX || ev.clientY) {
return {
x: (ev.clientX + document.body.scrollLeft + document.documentElement.scrollLeft),
y: (ev.clientY + document.body.scrollTop + document.documentElement.scrollTop)
};
}
return {
x: 0,
y: 0
};
}
}
$.oc.foundation.event = Event;
}(window.jQuery);
================================================
FILE: modules/backend/assets/foundation/scripts/rowlink/README.md
================================================
# Row Link
You may link an entire row by adding the `data-control="rowlink"` attribute to the table element. The first table data (TD) column with an anchor will be used to link the entire row. To bypass this behavior, simply add the `nolink` class to the column.
';
this.parentEl = (options.parentEl && $(options.parentEl).length) ? $(options.parentEl) : $(this.parentEl);
this.container = $(options.template).appendTo(this.parentEl);
//
// handle all the possible options overriding defaults
//
if (typeof options.locale === 'object') {
if (typeof options.locale.direction === 'string')
this.locale.direction = options.locale.direction;
if (typeof options.locale.format === 'string')
this.locale.format = options.locale.format;
if (typeof options.locale.separator === 'string')
this.locale.separator = options.locale.separator;
if (typeof options.locale.daysOfWeek === 'object')
this.locale.daysOfWeek = options.locale.daysOfWeek.slice();
if (typeof options.locale.monthNames === 'object')
this.locale.monthNames = options.locale.monthNames.slice();
if (typeof options.locale.firstDay === 'number')
this.locale.firstDay = options.locale.firstDay;
if (typeof options.locale.applyLabel === 'string')
this.locale.applyLabel = options.locale.applyLabel;
if (typeof options.locale.cancelLabel === 'string')
this.locale.cancelLabel = options.locale.cancelLabel;
if (typeof options.locale.weekLabel === 'string')
this.locale.weekLabel = options.locale.weekLabel;
if (typeof options.locale.customRangeLabel === 'string'){
//Support unicode chars in the custom range name.
var elem = document.createElement('textarea');
elem.innerHTML = options.locale.customRangeLabel;
var rangeHtml = elem.value;
this.locale.customRangeLabel = rangeHtml;
}
}
this.container.addClass(this.locale.direction);
if (typeof options.startDate === 'string')
this.startDate = moment(options.startDate, this.locale.format);
if (typeof options.endDate === 'string')
this.endDate = moment(options.endDate, this.locale.format);
if (typeof options.minDate === 'string')
this.minDate = moment(options.minDate, this.locale.format);
if (typeof options.maxDate === 'string')
this.maxDate = moment(options.maxDate, this.locale.format);
if (typeof options.startDate === 'object')
this.startDate = moment(options.startDate);
if (typeof options.endDate === 'object')
this.endDate = moment(options.endDate);
if (typeof options.minDate === 'object')
this.minDate = moment(options.minDate);
if (typeof options.maxDate === 'object')
this.maxDate = moment(options.maxDate);
// sanity check for bad options
if (this.minDate && this.startDate.isBefore(this.minDate))
this.startDate = this.minDate.clone();
// sanity check for bad options
if (this.maxDate && this.endDate.isAfter(this.maxDate))
this.endDate = this.maxDate.clone();
if (typeof options.applyButtonClasses === 'string')
this.applyButtonClasses = options.applyButtonClasses;
if (typeof options.applyClass === 'string') //backwards compat
this.applyButtonClasses = options.applyClass;
if (typeof options.cancelButtonClasses === 'string')
this.cancelButtonClasses = options.cancelButtonClasses;
if (typeof options.cancelClass === 'string') //backwards compat
this.cancelButtonClasses = options.cancelClass;
if (typeof options.maxSpan === 'object')
this.maxSpan = options.maxSpan;
if (typeof options.dateLimit === 'object') //backwards compat
this.maxSpan = options.dateLimit;
if (typeof options.opens === 'string')
this.opens = options.opens;
if (typeof options.drops === 'string')
this.drops = options.drops;
if (typeof options.showWeekNumbers === 'boolean')
this.showWeekNumbers = options.showWeekNumbers;
if (typeof options.showISOWeekNumbers === 'boolean')
this.showISOWeekNumbers = options.showISOWeekNumbers;
if (typeof options.buttonClasses === 'string')
this.buttonClasses = options.buttonClasses;
if (typeof options.buttonClasses === 'object')
this.buttonClasses = options.buttonClasses.join(' ');
if (typeof options.showDropdowns === 'boolean')
this.showDropdowns = options.showDropdowns;
if (typeof options.minYear === 'number')
this.minYear = options.minYear;
if (typeof options.maxYear === 'number')
this.maxYear = options.maxYear;
if (typeof options.showCustomRangeLabel === 'boolean')
this.showCustomRangeLabel = options.showCustomRangeLabel;
if (typeof options.singleDatePicker === 'boolean') {
this.singleDatePicker = options.singleDatePicker;
if (this.singleDatePicker)
this.endDate = this.startDate.clone();
}
if (typeof options.timePicker === 'boolean')
this.timePicker = options.timePicker;
if (typeof options.timePickerSeconds === 'boolean')
this.timePickerSeconds = options.timePickerSeconds;
if (typeof options.timePickerIncrement === 'number')
this.timePickerIncrement = options.timePickerIncrement;
if (typeof options.timePicker24Hour === 'boolean')
this.timePicker24Hour = options.timePicker24Hour;
if (typeof options.autoApply === 'boolean')
this.autoApply = options.autoApply;
if (typeof options.autoUpdateInput === 'boolean')
this.autoUpdateInput = options.autoUpdateInput;
if (typeof options.linkedCalendars === 'boolean')
this.linkedCalendars = options.linkedCalendars;
if (typeof options.isInvalidDate === 'function')
this.isInvalidDate = options.isInvalidDate;
if (typeof options.isCustomDate === 'function')
this.isCustomDate = options.isCustomDate;
if (typeof options.alwaysShowCalendars === 'boolean')
this.alwaysShowCalendars = options.alwaysShowCalendars;
// update day names order to firstDay
if (this.locale.firstDay != 0) {
var iterator = this.locale.firstDay;
while (iterator > 0) {
this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift());
iterator--;
}
}
var start, end, range;
//if no start/end dates set, check if an input element contains initial values
if (typeof options.startDate === 'undefined' && typeof options.endDate === 'undefined') {
if ($(this.element).is(':text')) {
var val = $(this.element).val(),
split = val.split(this.locale.separator);
start = end = null;
if (split.length == 2) {
start = moment(split[0], this.locale.format);
end = moment(split[1], this.locale.format);
} else if (this.singleDatePicker && val !== "") {
start = moment(val, this.locale.format);
end = moment(val, this.locale.format);
}
if (start !== null && end !== null) {
this.setStartDate(start);
this.setEndDate(end);
}
}
}
if (typeof options.ranges === 'object') {
for (range in options.ranges) {
if (typeof options.ranges[range][0] === 'string')
start = moment(options.ranges[range][0], this.locale.format);
else
start = moment(options.ranges[range][0]);
if (typeof options.ranges[range][1] === 'string')
end = moment(options.ranges[range][1], this.locale.format);
else
end = moment(options.ranges[range][1]);
// If the start or end date exceed those allowed by the minDate or maxSpan
// options, shorten the range to the allowable period.
if (this.minDate && start.isBefore(this.minDate))
start = this.minDate.clone();
var maxDate = this.maxDate;
if (this.maxSpan && maxDate && start.clone().add(this.maxSpan).isAfter(maxDate))
maxDate = start.clone().add(this.maxSpan);
if (maxDate && end.isAfter(maxDate))
end = maxDate.clone();
// If the end of the range is before the minimum or the start of the range is
// after the maximum, don't display this range option at all.
if ((this.minDate && end.isBefore(this.minDate, this.timepicker ? 'minute' : 'day'))
|| (maxDate && start.isAfter(maxDate, this.timepicker ? 'minute' : 'day')))
continue;
//Support unicode chars in the range names.
var elem = document.createElement('textarea');
elem.innerHTML = range;
var rangeHtml = elem.value;
this.ranges[rangeHtml] = [start, end];
}
var list = '
';
for (range in this.ranges) {
list += '
' + range + '
';
}
if (this.showCustomRangeLabel) {
list += '
' + this.locale.customRangeLabel + '
';
}
list += '
';
this.container.find('.ranges').prepend(list);
}
if (typeof cb === 'function') {
this.callback = cb;
}
if (!this.timePicker) {
this.startDate = this.startDate.startOf('day');
this.endDate = this.endDate.endOf('day');
this.container.find('.calendar-time').hide();
}
//can't be used together for now
if (this.timePicker && this.autoApply)
this.autoApply = false;
if (this.autoApply) {
this.container.addClass('auto-apply');
}
if (typeof options.ranges === 'object')
this.container.addClass('show-ranges');
if (this.singleDatePicker) {
this.container.addClass('single');
this.container.find('.drp-calendar.left').addClass('single');
this.container.find('.drp-calendar.left').show();
this.container.find('.drp-calendar.right').hide();
if (!this.timePicker) {
this.container.addClass('auto-apply');
}
}
if ((typeof options.ranges === 'undefined' && !this.singleDatePicker) || this.alwaysShowCalendars) {
this.container.addClass('show-calendar');
}
this.container.addClass('opens' + this.opens);
//apply CSS classes and labels to buttons
this.container.find('.applyBtn, .cancelBtn').addClass(this.buttonClasses);
if (this.applyButtonClasses.length)
this.container.find('.applyBtn').addClass(this.applyButtonClasses);
if (this.cancelButtonClasses.length)
this.container.find('.cancelBtn').addClass(this.cancelButtonClasses);
this.container.find('.applyBtn').html(this.locale.applyLabel);
this.container.find('.cancelBtn').html(this.locale.cancelLabel);
//
// event listeners
//
this.container.find('.drp-calendar')
.on('click.daterangepicker', '.prev', $.proxy(this.clickPrev, this))
.on('click.daterangepicker', '.next', $.proxy(this.clickNext, this))
.on('mousedown.daterangepicker', 'td.available', $.proxy(this.clickDate, this))
.on('mouseenter.daterangepicker', 'td.available', $.proxy(this.hoverDate, this))
.on('change.daterangepicker', 'select.yearselect', $.proxy(this.monthOrYearChanged, this))
.on('change.daterangepicker', 'select.monthselect', $.proxy(this.monthOrYearChanged, this))
.on('change.daterangepicker', 'select.hourselect,select.minuteselect,select.secondselect,select.ampmselect', $.proxy(this.timeChanged, this))
this.container.find('.ranges')
.on('click.daterangepicker', 'li', $.proxy(this.clickRange, this))
this.container.find('.drp-buttons')
.on('click.daterangepicker', 'button.applyBtn', $.proxy(this.clickApply, this))
.on('click.daterangepicker', 'button.cancelBtn', $.proxy(this.clickCancel, this))
if (this.element.is('input') || this.element.is('button')) {
this.element.on({
'click.daterangepicker': $.proxy(this.show, this),
'focus.daterangepicker': $.proxy(this.show, this),
'keyup.daterangepicker': $.proxy(this.elementChanged, this),
'keydown.daterangepicker': $.proxy(this.keydown, this) //IE 11 compatibility
});
} else {
this.element.on('click.daterangepicker', $.proxy(this.toggle, this));
this.element.on('keydown.daterangepicker', $.proxy(this.toggle, this));
}
//
// if attached to a text input, set the initial value
//
this.updateElement();
};
DateRangePicker.prototype = {
constructor: DateRangePicker,
setStartDate: function(startDate) {
if (typeof startDate === 'string')
this.startDate = moment(startDate, this.locale.format);
if (typeof startDate === 'object')
this.startDate = moment(startDate);
if (!this.timePicker)
this.startDate = this.startDate.startOf('day');
if (this.timePicker && this.timePickerIncrement)
this.startDate.minute(Math.round(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement);
if (this.minDate && this.startDate.isBefore(this.minDate)) {
this.startDate = this.minDate.clone();
if (this.timePicker && this.timePickerIncrement)
this.startDate.minute(Math.round(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement);
}
if (this.maxDate && this.startDate.isAfter(this.maxDate)) {
this.startDate = this.maxDate.clone();
if (this.timePicker && this.timePickerIncrement)
this.startDate.minute(Math.floor(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement);
}
if (!this.isShowing)
this.updateElement();
this.updateMonthsInView();
},
setEndDate: function(endDate) {
if (typeof endDate === 'string')
this.endDate = moment(endDate, this.locale.format);
if (typeof endDate === 'object')
this.endDate = moment(endDate);
if (!this.timePicker)
this.endDate = this.endDate.endOf('day');
if (this.timePicker && this.timePickerIncrement)
this.endDate.minute(Math.round(this.endDate.minute() / this.timePickerIncrement) * this.timePickerIncrement);
if (this.endDate.isBefore(this.startDate))
this.endDate = this.startDate.clone();
if (this.maxDate && this.endDate.isAfter(this.maxDate))
this.endDate = this.maxDate.clone();
if (this.maxSpan && this.startDate.clone().add(this.maxSpan).isBefore(this.endDate))
this.endDate = this.startDate.clone().add(this.maxSpan);
this.previousRightTime = this.endDate.clone();
this.container.find('.drp-selected').html(this.startDate.format(this.locale.format) + this.locale.separator + this.endDate.format(this.locale.format));
if (!this.isShowing)
this.updateElement();
this.updateMonthsInView();
},
isInvalidDate: function() {
return false;
},
isCustomDate: function() {
return false;
},
updateView: function() {
if (this.timePicker) {
this.renderTimePicker('left');
this.renderTimePicker('right');
if (!this.endDate) {
this.container.find('.right .calendar-time select').attr('disabled', 'disabled').addClass('disabled');
} else {
this.container.find('.right .calendar-time select').removeAttr('disabled').removeClass('disabled');
}
}
if (this.endDate)
this.container.find('.drp-selected').html(this.startDate.format(this.locale.format) + this.locale.separator + this.endDate.format(this.locale.format));
this.updateMonthsInView();
this.updateCalendars();
this.updateFormInputs();
},
updateMonthsInView: function() {
if (this.endDate) {
//if both dates are visible already, do nothing
if (!this.singleDatePicker && this.leftCalendar.month && this.rightCalendar.month &&
(this.startDate.format('YYYY-MM') == this.leftCalendar.month.format('YYYY-MM') || this.startDate.format('YYYY-MM') == this.rightCalendar.month.format('YYYY-MM'))
&&
(this.endDate.format('YYYY-MM') == this.leftCalendar.month.format('YYYY-MM') || this.endDate.format('YYYY-MM') == this.rightCalendar.month.format('YYYY-MM'))
) {
return;
}
this.leftCalendar.month = this.startDate.clone().date(2);
if (!this.linkedCalendars && (this.endDate.month() != this.startDate.month() || this.endDate.year() != this.startDate.year())) {
this.rightCalendar.month = this.endDate.clone().date(2);
} else {
this.rightCalendar.month = this.startDate.clone().date(2).add(1, 'month');
}
} else {
if (this.leftCalendar.month.format('YYYY-MM') != this.startDate.format('YYYY-MM') && this.rightCalendar.month.format('YYYY-MM') != this.startDate.format('YYYY-MM')) {
this.leftCalendar.month = this.startDate.clone().date(2);
this.rightCalendar.month = this.startDate.clone().date(2).add(1, 'month');
}
}
if (this.maxDate && this.linkedCalendars && !this.singleDatePicker && this.rightCalendar.month > this.maxDate) {
this.rightCalendar.month = this.maxDate.clone().date(2);
this.leftCalendar.month = this.maxDate.clone().date(2).subtract(1, 'month');
}
},
updateCalendars: function() {
if (this.timePicker) {
var hour, minute, second;
if (this.endDate) {
hour = parseInt(this.container.find('.left .hourselect').val(), 10);
minute = parseInt(this.container.find('.left .minuteselect').val(), 10);
if (isNaN(minute)) {
minute = parseInt(this.container.find('.left .minuteselect option:last').val(), 10);
}
second = this.timePickerSeconds ? parseInt(this.container.find('.left .secondselect').val(), 10) : 0;
if (!this.timePicker24Hour) {
var ampm = this.container.find('.left .ampmselect').val();
if (ampm === 'PM' && hour < 12)
hour += 12;
if (ampm === 'AM' && hour === 12)
hour = 0;
}
} else {
hour = parseInt(this.container.find('.right .hourselect').val(), 10);
minute = parseInt(this.container.find('.right .minuteselect').val(), 10);
if (isNaN(minute)) {
minute = parseInt(this.container.find('.right .minuteselect option:last').val(), 10);
}
second = this.timePickerSeconds ? parseInt(this.container.find('.right .secondselect').val(), 10) : 0;
if (!this.timePicker24Hour) {
var ampm = this.container.find('.right .ampmselect').val();
if (ampm === 'PM' && hour < 12)
hour += 12;
if (ampm === 'AM' && hour === 12)
hour = 0;
}
}
this.leftCalendar.month.hour(hour).minute(minute).second(second);
this.rightCalendar.month.hour(hour).minute(minute).second(second);
}
this.renderCalendar('left');
this.renderCalendar('right');
//highlight any predefined range matching the current start and end dates
this.container.find('.ranges li').removeClass('active');
if (this.endDate == null) return;
this.calculateChosenLabel();
},
renderCalendar: function(side) {
//
// Build the matrix of dates that will populate the calendar
//
var calendar = side == 'left' ? this.leftCalendar : this.rightCalendar;
var month = calendar.month.month();
var year = calendar.month.year();
var hour = calendar.month.hour();
var minute = calendar.month.minute();
var second = calendar.month.second();
var daysInMonth = moment([year, month]).daysInMonth();
var firstDay = moment([year, month, 1]);
var lastDay = moment([year, month, daysInMonth]);
var lastMonth = moment(firstDay).subtract(1, 'month').month();
var lastYear = moment(firstDay).subtract(1, 'month').year();
var daysInLastMonth = moment([lastYear, lastMonth]).daysInMonth();
var dayOfWeek = firstDay.day();
//initialize a 6 rows x 7 columns array for the calendar
var calendar = [];
calendar.firstDay = firstDay;
calendar.lastDay = lastDay;
for (var i = 0; i < 6; i++) {
calendar[i] = [];
}
//populate the calendar with date objects
var startDay = daysInLastMonth - dayOfWeek + this.locale.firstDay + 1;
if (startDay > daysInLastMonth)
startDay -= 7;
if (dayOfWeek == this.locale.firstDay)
startDay = daysInLastMonth - 6;
var curDate = moment([lastYear, lastMonth, startDay, 12, minute, second]);
var col, row;
for (var i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(curDate).add(24, 'hour')) {
if (i > 0 && col % 7 === 0) {
col = 0;
row++;
}
calendar[row][col] = curDate.clone().hour(hour).minute(minute).second(second);
curDate.hour(12);
if (this.minDate && calendar[row][col].format('YYYY-MM-DD') == this.minDate.format('YYYY-MM-DD') && calendar[row][col].isBefore(this.minDate) && side == 'left') {
calendar[row][col] = this.minDate.clone();
}
if (this.maxDate && calendar[row][col].format('YYYY-MM-DD') == this.maxDate.format('YYYY-MM-DD') && calendar[row][col].isAfter(this.maxDate) && side == 'right') {
calendar[row][col] = this.maxDate.clone();
}
}
//make the calendar object available to hoverDate/clickDate
if (side == 'left') {
this.leftCalendar.calendar = calendar;
} else {
this.rightCalendar.calendar = calendar;
}
//
// Display the calendar
//
var minDate = side == 'left' ? this.minDate : this.startDate;
var maxDate = this.maxDate;
var selected = side == 'left' ? this.startDate : this.endDate;
var arrow = this.locale.direction == 'ltr' ? {left: 'chevron-left', right: 'chevron-right'} : {left: 'chevron-right', right: 'chevron-left'};
var html = '
';
html += '';
html += '
';
// add empty cell for week number
if (this.showWeekNumbers || this.showISOWeekNumbers)
html += '
';
if ((!minDate || minDate.isBefore(calendar.firstDay)) && (!this.linkedCalendars || side == 'left')) {
html += '
';
} else {
html += '
';
}
var dateHtml = this.locale.monthNames[calendar[1][1].month()] + calendar[1][1].format(" YYYY");
if (this.showDropdowns) {
var currentMonth = calendar[1][1].month();
var currentYear = calendar[1][1].year();
var maxYear = (maxDate && maxDate.year()) || (this.maxYear);
var minYear = (minDate && minDate.year()) || (this.minYear);
var inMinYear = currentYear == minYear;
var inMaxYear = currentYear == maxYear;
var monthHtml = '";
var yearHtml = '';
dateHtml = monthHtml + yearHtml;
}
html += '
' + dateHtml + '
';
if ((!maxDate || maxDate.isAfter(calendar.lastDay)) && (!this.linkedCalendars || side == 'right' || this.singleDatePicker)) {
html += '
';
} else {
html += '
';
}
html += '
';
html += '
';
// add week number label
if (this.showWeekNumbers || this.showISOWeekNumbers)
html += '
' + this.locale.weekLabel + '
';
$.each(this.locale.daysOfWeek, function(index, dayOfWeek) {
html += '
' + dayOfWeek + '
';
});
html += '
';
html += '';
html += '';
//adjust maxDate to reflect the maxSpan setting in order to
//grey out end dates beyond the maxSpan
if (this.endDate == null && this.maxSpan) {
var maxLimit = this.startDate.clone().add(this.maxSpan).endOf('day');
if (!maxDate || maxLimit.isBefore(maxDate)) {
maxDate = maxLimit;
}
}
for (var row = 0; row < 6; row++) {
html += '
';
// add week number
if (this.showWeekNumbers)
html += '
' + calendar[row][0].week() + '
';
else if (this.showISOWeekNumbers)
html += '
' + calendar[row][0].isoWeek() + '
';
for (var col = 0; col < 7; col++) {
var classes = [];
//highlight today's date
if (calendar[row][col].isSame(new Date(), "day"))
classes.push('today');
//highlight weekends
if (calendar[row][col].isoWeekday() > 5)
classes.push('weekend');
//grey out the dates in other months displayed at beginning and end of this calendar
if (calendar[row][col].month() != calendar[1][1].month())
classes.push('off', 'ends');
//don't allow selection of dates before the minimum date
if (this.minDate && calendar[row][col].isBefore(this.minDate, 'day'))
classes.push('off', 'disabled');
//don't allow selection of dates after the maximum date
if (maxDate && calendar[row][col].isAfter(maxDate, 'day'))
classes.push('off', 'disabled');
//don't allow selection of date if a custom function decides it's invalid
if (this.isInvalidDate(calendar[row][col]))
classes.push('off', 'disabled');
//highlight the currently selected start date
if (calendar[row][col].format('YYYY-MM-DD') == this.startDate.format('YYYY-MM-DD'))
classes.push('active', 'start-date');
//highlight the currently selected end date
if (this.endDate != null && calendar[row][col].format('YYYY-MM-DD') == this.endDate.format('YYYY-MM-DD'))
classes.push('active', 'end-date');
//highlight dates in-between the selected dates
if (this.endDate != null && calendar[row][col] > this.startDate && calendar[row][col] < this.endDate)
classes.push('in-range');
//apply custom classes for this date
var isCustom = this.isCustomDate(calendar[row][col]);
if (isCustom !== false) {
if (typeof isCustom === 'string')
classes.push(isCustom);
else
Array.prototype.push.apply(classes, isCustom);
}
var cname = '', disabled = false;
for (var i = 0; i < classes.length; i++) {
cname += classes[i] + ' ';
if (classes[i] == 'disabled')
disabled = true;
}
if (!disabled)
cname += 'available';
html += '
' + calendar[row][col].date() + '
';
}
html += '
';
}
html += '';
html += '
';
this.container.find('.drp-calendar.' + side + ' .calendar-table').html(html);
},
renderTimePicker: function(side) {
// Don't bother updating the time picker if it's currently disabled
// because an end date hasn't been clicked yet
if (side == 'right' && !this.endDate) return;
var html, selected, minDate, maxDate = this.maxDate;
if (this.maxSpan && (!this.maxDate || this.startDate.clone().add(this.maxSpan).isBefore(this.maxDate)))
maxDate = this.startDate.clone().add(this.maxSpan);
if (side == 'left') {
selected = this.startDate.clone();
minDate = this.minDate;
} else if (side == 'right') {
selected = this.endDate.clone();
minDate = this.startDate;
//Preserve the time already selected
var timeSelector = this.container.find('.drp-calendar.right .calendar-time');
if (timeSelector.html() != '') {
selected.hour(!isNaN(selected.hour()) ? selected.hour() : timeSelector.find('.hourselect option:selected').val());
selected.minute(!isNaN(selected.minute()) ? selected.minute() : timeSelector.find('.minuteselect option:selected').val());
selected.second(!isNaN(selected.second()) ? selected.second() : timeSelector.find('.secondselect option:selected').val());
if (!this.timePicker24Hour) {
var ampm = timeSelector.find('.ampmselect option:selected').val();
if (ampm === 'PM' && selected.hour() < 12)
selected.hour(selected.hour() + 12);
if (ampm === 'AM' && selected.hour() === 12)
selected.hour(0);
}
}
if (selected.isBefore(this.startDate))
selected = this.startDate.clone();
if (maxDate && selected.isAfter(maxDate))
selected = maxDate.clone();
}
//
// hours
//
html = ' ';
//
// minutes
//
html += ': ';
//
// seconds
//
if (this.timePickerSeconds) {
html += ': ';
}
//
// AM/PM
//
if (!this.timePicker24Hour) {
html += '';
}
this.container.find('.drp-calendar.' + side + ' .calendar-time').html(html);
},
updateFormInputs: function() {
if (this.singleDatePicker || (this.endDate && (this.startDate.isBefore(this.endDate) || this.startDate.isSame(this.endDate)))) {
this.container.find('button.applyBtn').removeAttr('disabled');
} else {
this.container.find('button.applyBtn').attr('disabled', 'disabled');
}
},
move: function() {
var parentOffset = { top: 0, left: 0 },
containerTop;
var parentRightEdge = $(window).width();
if (!this.parentEl.is('body')) {
parentOffset = {
top: this.parentEl.offset().top - this.parentEl.scrollTop(),
left: this.parentEl.offset().left - this.parentEl.scrollLeft()
};
parentRightEdge = this.parentEl[0].clientWidth + this.parentEl.offset().left;
}
if (this.drops == 'up')
containerTop = this.element.offset().top - this.container.outerHeight() - parentOffset.top;
else
containerTop = this.element.offset().top + this.element.outerHeight() - parentOffset.top;
// Force the container to it's actual width
this.container.css({
top: 0,
left: 0,
right: 'auto'
});
var containerWidth = this.container.outerWidth();
this.container[this.drops == 'up' ? 'addClass' : 'removeClass']('drop-up');
if (this.opens == 'left') {
var containerRight = parentRightEdge - this.element.offset().left - this.element.outerWidth();
if (containerWidth + containerRight > $(window).width()) {
this.container.css({
top: containerTop,
right: 'auto',
left: 9
});
} else {
this.container.css({
top: containerTop,
right: containerRight,
left: 'auto'
});
}
} else if (this.opens == 'center') {
var containerLeft = this.element.offset().left - parentOffset.left + this.element.outerWidth() / 2
- containerWidth / 2;
if (containerLeft < 0) {
this.container.css({
top: containerTop,
right: 'auto',
left: 9
});
} else if (containerLeft + containerWidth > $(window).width()) {
this.container.css({
top: containerTop,
left: 'auto',
right: 0
});
} else {
this.container.css({
top: containerTop,
left: containerLeft,
right: 'auto'
});
}
} else {
var containerLeft = this.element.offset().left - parentOffset.left;
if (containerLeft + containerWidth > $(window).width()) {
this.container.css({
top: containerTop,
left: 'auto',
right: 0
});
} else {
this.container.css({
top: containerTop,
left: containerLeft,
right: 'auto'
});
}
}
},
show: function(e) {
if (this.isShowing) return;
// Create a click proxy that is private to this instance of datepicker, for unbinding
this._outsideClickProxy = $.proxy(function(e) { this.outsideClick(e); }, this);
// Bind global datepicker mousedown for hiding and
$(document)
.on('mousedown.daterangepicker', this._outsideClickProxy)
// also support mobile devices
.on('touchend.daterangepicker', this._outsideClickProxy)
// also explicitly play nice with Bootstrap dropdowns, which stopPropagation when clicking them
.on('click.daterangepicker', '[data-toggle=dropdown]', this._outsideClickProxy)
// and also close when focus changes to outside the picker (eg. tabbing between controls)
.on('focusin.daterangepicker', this._outsideClickProxy);
// Reposition the picker if the window is resized while it's open
$(window).on('resize.daterangepicker', $.proxy(function(e) { this.move(e); }, this));
this.oldStartDate = this.startDate.clone();
this.oldEndDate = this.endDate.clone();
this.previousRightTime = this.endDate.clone();
this.updateView();
this.container.show();
this.move();
this.element.trigger('show.daterangepicker', this);
this.isShowing = true;
},
hide: function(e) {
if (!this.isShowing) return;
//incomplete date selection, revert to last values
if (!this.endDate) {
this.startDate = this.oldStartDate.clone();
this.endDate = this.oldEndDate.clone();
}
//if a new date range was selected, invoke the user callback function
if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate))
this.callback(this.startDate.clone(), this.endDate.clone(), this.chosenLabel);
//if picker is attached to a text input, update it
this.updateElement();
$(document).off('.daterangepicker');
$(window).off('.daterangepicker');
this.container.hide();
this.element.trigger('hide.daterangepicker', this);
this.isShowing = false;
},
toggle: function(e) {
if (this.isShowing) {
this.hide();
} else {
this.show();
}
},
outsideClick: function(e) {
var target = $(e.target);
// if the page is clicked anywhere except within the daterangerpicker/button
// itself then call this.hide()
if (
// ie modal dialog fix
e.type == "focusin" ||
target.closest(this.element).length ||
target.closest(this.container).length ||
target.closest('.calendar-table').length
) return;
this.hide();
this.element.trigger('outsideClick.daterangepicker', this);
},
showCalendars: function() {
this.container.addClass('show-calendar');
this.move();
this.element.trigger('showCalendar.daterangepicker', this);
},
hideCalendars: function() {
this.container.removeClass('show-calendar');
this.element.trigger('hideCalendar.daterangepicker', this);
},
clickRange: function(e) {
var label = e.target.getAttribute('data-range-key');
this.chosenLabel = label;
if (label == this.locale.customRangeLabel) {
this.showCalendars();
} else {
var dates = this.ranges[label];
this.startDate = dates[0];
this.endDate = dates[1];
if (!this.timePicker) {
this.startDate.startOf('day');
this.endDate.endOf('day');
}
if (!this.alwaysShowCalendars)
this.hideCalendars();
this.clickApply();
}
},
clickPrev: function(e) {
var cal = $(e.target).parents('.drp-calendar');
if (cal.hasClass('left')) {
this.leftCalendar.month.subtract(1, 'month');
if (this.linkedCalendars)
this.rightCalendar.month.subtract(1, 'month');
} else {
this.rightCalendar.month.subtract(1, 'month');
}
this.updateCalendars();
},
clickNext: function(e) {
var cal = $(e.target).parents('.drp-calendar');
if (cal.hasClass('left')) {
this.leftCalendar.month.add(1, 'month');
} else {
this.rightCalendar.month.add(1, 'month');
if (this.linkedCalendars)
this.leftCalendar.month.add(1, 'month');
}
this.updateCalendars();
},
hoverDate: function(e) {
//ignore dates that can't be selected
if (!$(e.target).hasClass('available')) return;
var title = $(e.target).attr('data-title');
var row = title.substr(1, 1);
var col = title.substr(3, 1);
var cal = $(e.target).parents('.drp-calendar');
var date = cal.hasClass('left') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col];
//highlight the dates between the start date and the date being hovered as a potential end date
var leftCalendar = this.leftCalendar;
var rightCalendar = this.rightCalendar;
var startDate = this.startDate;
if (!this.endDate) {
this.container.find('.drp-calendar tbody td').each(function(index, el) {
//skip week numbers, only look at dates
if ($(el).hasClass('week')) return;
var title = $(el).attr('data-title');
var row = title.substr(1, 1);
var col = title.substr(3, 1);
var cal = $(el).parents('.drp-calendar');
var dt = cal.hasClass('left') ? leftCalendar.calendar[row][col] : rightCalendar.calendar[row][col];
if ((dt.isAfter(startDate) && dt.isBefore(date)) || dt.isSame(date, 'day')) {
$(el).addClass('in-range');
} else {
$(el).removeClass('in-range');
}
});
}
},
clickDate: function(e) {
if (!$(e.target).hasClass('available')) return;
var title = $(e.target).attr('data-title');
var row = title.substr(1, 1);
var col = title.substr(3, 1);
var cal = $(e.target).parents('.drp-calendar');
var date = cal.hasClass('left') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col];
//
// this function needs to do a few things:
// * alternate between selecting a start and end date for the range,
// * if the time picker is enabled, apply the hour/minute/second from the select boxes to the clicked date
// * if autoapply is enabled, and an end date was chosen, apply the selection
// * if single date picker mode, and time picker isn't enabled, apply the selection immediately
// * if one of the inputs above the calendars was focused, cancel that manual input
//
if (this.endDate || date.isBefore(this.startDate, 'day')) { //picking start
if (this.timePicker) {
var hour = parseInt(this.container.find('.left .hourselect').val(), 10);
if (!this.timePicker24Hour) {
var ampm = this.container.find('.left .ampmselect').val();
if (ampm === 'PM' && hour < 12)
hour += 12;
if (ampm === 'AM' && hour === 12)
hour = 0;
}
var minute = parseInt(this.container.find('.left .minuteselect').val(), 10);
if (isNaN(minute)) {
minute = parseInt(this.container.find('.left .minuteselect option:last').val(), 10);
}
var second = this.timePickerSeconds ? parseInt(this.container.find('.left .secondselect').val(), 10) : 0;
date = date.clone().hour(hour).minute(minute).second(second);
}
this.endDate = null;
this.setStartDate(date.clone());
} else if (!this.endDate && date.isBefore(this.startDate)) {
//special case: clicking the same date for start/end,
//but the time of the end date is before the start date
this.setEndDate(this.startDate.clone());
} else { // picking end
if (this.timePicker) {
var hour = parseInt(this.container.find('.right .hourselect').val(), 10);
if (!this.timePicker24Hour) {
var ampm = this.container.find('.right .ampmselect').val();
if (ampm === 'PM' && hour < 12)
hour += 12;
if (ampm === 'AM' && hour === 12)
hour = 0;
}
var minute = parseInt(this.container.find('.right .minuteselect').val(), 10);
if (isNaN(minute)) {
minute = parseInt(this.container.find('.right .minuteselect option:last').val(), 10);
}
var second = this.timePickerSeconds ? parseInt(this.container.find('.right .secondselect').val(), 10) : 0;
date = date.clone().hour(hour).minute(minute).second(second);
}
this.setEndDate(date.clone());
if (this.autoApply) {
this.calculateChosenLabel();
this.clickApply();
}
}
if (this.singleDatePicker) {
this.setEndDate(this.startDate);
if (!this.timePicker)
this.clickApply();
}
this.updateView();
//This is to cancel the blur event handler if the mouse was in one of the inputs
e.stopPropagation();
},
calculateChosenLabel: function () {
var customRange = true;
var i = 0;
for (var range in this.ranges) {
if (this.timePicker) {
var format = this.timePickerSeconds ? "YYYY-MM-DD HH:mm:ss" : "YYYY-MM-DD HH:mm";
//ignore times when comparing dates if time picker seconds is not enabled
if (this.startDate.format(format) == this.ranges[range][0].format(format) && this.endDate.format(format) == this.ranges[range][1].format(format)) {
customRange = false;
this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')').addClass('active').attr('data-range-key');
break;
}
} else {
//ignore times when comparing dates if time picker is not enabled
if (this.startDate.format('YYYY-MM-DD') == this.ranges[range][0].format('YYYY-MM-DD') && this.endDate.format('YYYY-MM-DD') == this.ranges[range][1].format('YYYY-MM-DD')) {
customRange = false;
this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')').addClass('active').attr('data-range-key');
break;
}
}
i++;
}
if (customRange) {
if (this.showCustomRangeLabel) {
this.chosenLabel = this.container.find('.ranges li:last').addClass('active').attr('data-range-key');
} else {
this.chosenLabel = null;
}
this.showCalendars();
}
},
clickApply: function(e) {
this.hide();
this.element.trigger('apply.daterangepicker', this);
},
clickCancel: function(e) {
this.startDate = this.oldStartDate;
this.endDate = this.oldEndDate;
this.hide();
this.element.trigger('cancel.daterangepicker', this);
},
monthOrYearChanged: function(e) {
var isLeft = $(e.target).closest('.drp-calendar').hasClass('left'),
leftOrRight = isLeft ? 'left' : 'right',
cal = this.container.find('.drp-calendar.'+leftOrRight);
// Month must be Number for new moment versions
var month = parseInt(cal.find('.monthselect').val(), 10);
var year = cal.find('.yearselect').val();
if (!isLeft) {
if (year < this.startDate.year() || (year == this.startDate.year() && month < this.startDate.month())) {
month = this.startDate.month();
year = this.startDate.year();
}
}
if (this.minDate) {
if (year < this.minDate.year() || (year == this.minDate.year() && month < this.minDate.month())) {
month = this.minDate.month();
year = this.minDate.year();
}
}
if (this.maxDate) {
if (year > this.maxDate.year() || (year == this.maxDate.year() && month > this.maxDate.month())) {
month = this.maxDate.month();
year = this.maxDate.year();
}
}
if (isLeft) {
this.leftCalendar.month.month(month).year(year);
if (this.linkedCalendars)
this.rightCalendar.month = this.leftCalendar.month.clone().add(1, 'month');
} else {
this.rightCalendar.month.month(month).year(year);
if (this.linkedCalendars)
this.leftCalendar.month = this.rightCalendar.month.clone().subtract(1, 'month');
}
this.updateCalendars();
},
timeChanged: function(e) {
var cal = $(e.target).closest('.drp-calendar'),
isLeft = cal.hasClass('left');
var hour = parseInt(cal.find('.hourselect').val(), 10);
var minute = parseInt(cal.find('.minuteselect').val(), 10);
if (isNaN(minute)) {
minute = parseInt(cal.find('.minuteselect option:last').val(), 10);
}
var second = this.timePickerSeconds ? parseInt(cal.find('.secondselect').val(), 10) : 0;
if (!this.timePicker24Hour) {
var ampm = cal.find('.ampmselect').val();
if (ampm === 'PM' && hour < 12)
hour += 12;
if (ampm === 'AM' && hour === 12)
hour = 0;
}
if (isLeft) {
var start = this.startDate.clone();
start.hour(hour);
start.minute(minute);
start.second(second);
this.setStartDate(start);
if (this.singleDatePicker) {
this.endDate = this.startDate.clone();
} else if (this.endDate && this.endDate.format('YYYY-MM-DD') == start.format('YYYY-MM-DD') && this.endDate.isBefore(start)) {
this.setEndDate(start.clone());
}
} else if (this.endDate) {
var end = this.endDate.clone();
end.hour(hour);
end.minute(minute);
end.second(second);
this.setEndDate(end);
}
//update the calendars so all clickable dates reflect the new time component
this.updateCalendars();
//update the form inputs above the calendars with the new time
this.updateFormInputs();
//re-render the time pickers because changing one selection can affect what's enabled in another
this.renderTimePicker('left');
this.renderTimePicker('right');
},
elementChanged: function() {
if (!this.element.is('input')) return;
if (!this.element.val().length) return;
var dateString = this.element.val().split(this.locale.separator),
start = null,
end = null;
if (dateString.length === 2) {
start = moment(dateString[0], this.locale.format);
end = moment(dateString[1], this.locale.format);
}
if (this.singleDatePicker || start === null || end === null) {
start = moment(this.element.val(), this.locale.format);
end = start;
}
if (!start.isValid() || !end.isValid()) return;
this.setStartDate(start);
this.setEndDate(end);
this.updateView();
},
keydown: function(e) {
//hide on tab or enter
if ((e.keyCode === 9) || (e.keyCode === 13)) {
this.hide();
}
//hide on esc and prevent propagation
if (e.keyCode === 27) {
e.preventDefault();
e.stopPropagation();
this.hide();
}
},
updateElement: function() {
if (this.element.is('input') && this.autoUpdateInput) {
var newValue = this.startDate.format(this.locale.format);
if (!this.singleDatePicker) {
newValue += this.locale.separator + this.endDate.format(this.locale.format);
}
if (newValue !== this.element.val()) {
this.element.val(newValue).trigger('change');
}
}
},
remove: function() {
this.container.remove();
this.element.off('.daterangepicker');
this.element.removeData();
}
};
$.fn.daterangepicker = function(options, callback) {
var implementOptions = $.extend(true, {}, $.fn.daterangepicker.defaultOptions, options);
this.each(function() {
var el = $(this);
if (el.data('daterangepicker'))
el.data('daterangepicker').remove();
el.data('daterangepicker', new DateRangePicker(el, implementOptions, callback));
});
return this;
};
return DateRangePicker;
}));
================================================
FILE: modules/backend/behaviors/FormController.php
================================================
"Form record with an ID of :id could not be found.",
'flashCreate' => ":name Created",
'flashUpdate' => ":name Updated",
'flashDelete' => ":name Deleted",
];
/**
* __construct the behavior
* @param Backend\Classes\Controller $controller
*/
public function __construct($controller)
{
parent::__construct($controller);
// Build configuration
$this->setConfig($controller->formConfig, $this->requiredConfig);
if (!$this->isPopupDesign()) {
$this->hidePopupDesign();
}
}
/**
* beforeDisplay fires before the page is displayed and AJAX is executed.
*/
public function beforeDisplay()
{
if ($this->isPopupDesign()) {
$this->beforeDisplayPopup();
}
}
/**
* initForm initializes the form configuration against a model and context value.
* This will process the configuration found in the `$formConfig` property
* and prepare the Form widget, which is the underlying tool used for
* actually rendering the form. The model used by this form is passed
* to this behavior via this method as the first argument.
*
* @see Backend\Widgets\Form
* @param October\Rain\Database\Model $model
* @param string $context Form context
* @return void
*/
public function initForm($model, $context = null)
{
if ($context !== null) {
$this->context = $context;
}
$context = $this->formGetContext();
$formConfig = $this->config = $this->controller->formGetConfig();
// Each page can supply a unique form definition, if desired
$formFields = $this->getConfig("{$context}[form]", $formConfig->form);
$config = $this->makeConfig($formFields);
$config->model = $model;
$config->arrayName = class_basename($model);
$config->context = $this->getConfig("{$context}[context]", $context);
$config->surveyMode = $this->isSurveyDesign();
$config->sessionKey = post('_form_session_key');
$config->horizontalMode = $this->isHorizontalForm();
// Make Form Widget and apply extensions
$this->formWidget = $this->makeWidget(\Backend\Widgets\Form::class, $config);
// Setup the default preview mode on form initialization if the context is preview
if ($config->context === 'preview') {
$this->formWidget->previewMode = true;
}
$this->formWidget->bindEvent('form.extendFieldsBefore', function () {
$this->controller->formExtendFieldsBefore($this->formWidget);
});
$this->formWidget->bindEvent('form.extendFields', function ($fields) {
$this->controller->formExtendFields($this->formWidget, $fields);
});
$this->formWidget->bindEvent('form.beforeRefresh', function ($holder) {
$result = $this->controller->formExtendRefreshData($this->formWidget, $holder->data);
if (is_array($result)) {
$holder->data = $result;
}
});
$this->formWidget->bindEvent('form.refreshFields', function ($fields) {
return $this->controller->formExtendRefreshFields($this->formWidget, $fields);
});
$this->formWidget->bindEvent('form.refresh', function ($result) {
return $this->controller->formExtendRefreshResults($this->formWidget, $result);
});
$this->formWidget->bindToController();
// Detected Relation controller behavior
if ($this->controller->isClassExtendedWith(\Backend\Behaviors\RelationController::class)) {
$this->controller->initRelation($model);
}
$this->prepareVars($model);
$this->model = $model;
}
/**
* Prepares commonly used view data.
* @param October\Rain\Database\Model $model
*/
protected function prepareVars($model)
{
$this->controller->vars['formModel'] = $model;
$this->controller->vars['formContext'] = $this->formGetContext();
$this->controller->vars['formRecordName'] = Lang::get($this->getConfig('name', 'backend::lang.model.name'));
$this->controller->vars['formSidebarWidth'] = $this->getDesignFormSize('sidebarSize');
}
//
// Create
//
/**
* create controller action used for creating new model records.
*
* @param string $context Form context
* @return void
*/
public function create($context = null)
{
if (!$this->controller->formCheckPermission('modelCreate')) {
throw new ForbiddenException;
}
try {
$this->context = $context ?: $this->getConfig('create[context]', FormField::CONTEXT_CREATE);
$this->controller->bodyClass ??= $this->getDesignBodyClass();
$this->controller->pageSize ??= $this->getDesignFormSize();
$this->controller->pageTitle ??= $this->getLang('create[title]', 'backend::lang.form.create_title');
$model = $this->controller->formCreateModelObject();
$model = $this->controller->formExtendModel($model) ?: $model;
$this->initForm($model);
}
catch (Exception $ex) {
$this->controller->handleError($ex);
}
}
/**
* create_onSave AJAX handler called from the create action and
* primarily used for creating new records.
*
* This handler will invoke the unique controller overrides
* `formBeforeCreate` and `formAfterCreate`.
*
* @param string $context Form context
* @return mixed
*/
public function create_onSave($context = null)
{
if (!$this->controller->formCheckPermission('modelCreate')) {
throw new ForbiddenException;
}
$this->context = $context ?: $this->getConfig('create[context]', FormField::CONTEXT_CREATE);
$model = $this->controller->formCreateModelObject();
$model = $this->controller->formExtendModel($model) ?: $model;
$this->initForm($model);
$this->controller->formBeforeSave($model);
$this->controller->formBeforeCreate($model);
$this->controller->fireSystemEvent('backend.form.beforeSave', [$model]);
$this->controller->fireSystemEvent('backend.form.beforeCreate', [$model]);
$this->performSaveOnModel(
$model,
$this->formWidget->getSaveData(),
['sessionKey' => $this->formWidget->getSessionKey(), 'propagate' => true]
);
$this->controller->formAfterSave($model);
$this->controller->formAfterCreate($model);
$this->controller->fireSystemEvent('backend.form.afterSave', [$model]);
$this->controller->fireSystemEvent('backend.form.afterCreate', [$model]);
Flash::success($this->getCustomLang('flashCreate'));
if ($redirect = $this->makeRedirect('create', $model)) {
return $redirect;
}
}
/**
* create_onCancel AJAX handler called from the create action and
* used for aborting record creation
*
* This handler will invoke the unique controller override
* `formAfterCancel`.
*
* @return mixed
*/
public function create_onCancel($context = null)
{
$this->context = $context ?: $this->getConfig('create[context]', FormField::CONTEXT_CREATE);
$model = $this->controller->formCreateModelObject();
$model = $this->controller->formExtendModel($model) ?: $model;
$this->initForm($model);
$model->cancelDeferred($this->formWidget->getSessionKey());
$this->controller->formAfterCancel($model);
$this->controller->fireSystemEvent('backend.form.afterCancel', [$model]);
if ($redirect = $this->makeRedirect('cancel')) {
return $redirect;
}
}
//
// Update
//
/**
* update controller action used for updating existing model records.
* This action takes a record identifier (primary key of the model)
* to locate the record used for sourcing the existing form values.
*
* @param int $recordId Record identifier
* @param string $context Form context
* @return void
*/
public function update($recordId = null, $context = null)
{
if (!$this->controller->formCheckPermission('modelUpdate')) {
throw new ForbiddenException;
}
try {
$this->context = $context ?: $this->getConfig('update[context]', FormField::CONTEXT_UPDATE);
$this->controller->bodyClass ??= $this->getDesignBodyClass();
$this->controller->pageSize ??= $this->getDesignFormSize();
$this->controller->pageTitle ??= $this->getLang('update[title]', 'backend::lang.form.update_title');
$model = $this->controller->formFindModelObject($recordId);
// Multisite
if ($this->controller->formHasMultisite($model)) {
if ($redirect = $this->makeMultisiteRedirect('create', $model)) {
return $redirect;
}
$this->addHandlerToSiteSwitcher();
}
$this->initForm($model);
}
catch (Exception $ex) {
$this->controller->handleError($ex);
}
}
/**
* update_onSave AJAX handler called from the update action and
* primarily used for updating existing records.
*
* This handler will invoke the unique controller overrides
* `formBeforeUpdate` and `formAfterUpdate`.
*
* @param int $recordId Record identifier
* @param string $context Form context
* @return mixed
*/
public function update_onSave($recordId = null, $context = null)
{
if (!$this->controller->formCheckPermission('modelUpdate')) {
throw new ForbiddenException;
}
$this->context = $context ?: $this->getConfig('update[context]', FormField::CONTEXT_UPDATE);
$model = $this->controller->formFindModelObject($recordId);
$this->initForm($model);
$this->controller->formBeforeSave($model);
$this->controller->formBeforeUpdate($model);
$this->controller->fireSystemEvent('backend.form.beforeSave', [$model]);
$this->controller->fireSystemEvent('backend.form.beforeUpdate', [$model]);
$this->performSaveOnModel(
$model,
$this->formWidget->getSaveData(),
['sessionKey' => $this->formWidget->getSessionKey(), 'propagate' => true]
);
$this->controller->formAfterSave($model);
$this->controller->formAfterUpdate($model);
$this->controller->fireSystemEvent('backend.form.afterSave', [$model]);
$this->controller->fireSystemEvent('backend.form.afterUpdate', [$model]);
Flash::success($this->getCustomLang('flashUpdate'));
if ($redirect = $this->makeRedirect('update', $model)) {
return $redirect;
}
}
/**
* update_onDelete AJAX handler called from the update action and
* used for deleting existing records.
*
* This handler will invoke the unique controller override
* `formAfterDelete`.
*
* @param int $recordId Record identifier
* @return mixed
*/
public function update_onDelete($recordId = null)
{
if (!$this->controller->formCheckPermission('modelDelete')) {
throw new ForbiddenException;
}
$this->context = $this->getConfig('update[context]', FormField::CONTEXT_UPDATE);
$model = $this->controller->formFindModelObject($recordId);
$this->initForm($model);
$model->delete();
$this->controller->formAfterDelete($model);
$this->controller->fireSystemEvent('backend.form.afterDelete', [$model]);
Flash::success($this->getCustomLang('flashDelete'));
if ($redirect = $this->makeRedirect('delete', $model)) {
return $redirect;
}
}
/**
* update_onCancel AJAX handler called from the update action and
* used for aborting existing record updates.
*
* This handler will invoke the unique controller override
* `formAfterCancel`.
*
* @param int $recordId Record identifier
* @return mixed
*/
public function update_onCancel($recordId = null)
{
$this->context = $this->getConfig('update[context]', FormField::CONTEXT_UPDATE);
$model = $this->controller->formFindModelObject($recordId);
$this->initForm($model);
$model->cancelDeferred($this->formWidget->getSessionKey());
$this->controller->formAfterCancel($model);
$this->controller->fireSystemEvent('backend.form.afterCancel', [$model]);
if ($redirect = $this->makeRedirect('cancel')) {
return $redirect;
}
}
//
// Preview
//
/**
* preview controller action used for viewing existing model records.
* This action takes a record identifier (primary key of the model)
* to locate the record used for sourcing the existing preview data.
*
* @param int $recordId Record identifier
* @param string $context Form context
* @return void
*/
public function preview($recordId = null, $context = null)
{
if (!$this->controller->formCheckPermission('modelPreview')) {
throw new ForbiddenException;
}
try {
$this->context = $context ?: $this->getConfig('preview[context]', FormField::CONTEXT_PREVIEW);
$this->controller->bodyClass ??= $this->getDesignBodyClass();
$this->controller->pageSize ??= $this->getDesignFormSize();
$this->controller->pageTitle ??= $this->getLang('preview[title]', 'backend::lang.form.preview_title');
$model = $this->controller->formFindModelObject($recordId);
// Multisite
if ($this->controller->formHasMultisite($model)) {
if ($redirect = $this->makeMultisiteRedirect('preview', $model)) {
return $redirect;
}
$this->addHandlerToSiteSwitcher();
}
$this->initForm($model);
}
catch (Exception $ex) {
$this->controller->handleError($ex);
}
}
//
// Utils
//
/**
* formRender the prepared form markup. This method is usually called from a view file.
*
* = $this->formRender() ?>
*
* The first argument supports an array of render options. The supported
* options can be found via the `render` method of the Form widget class.
*
* = $this->formRender(['preview' => true, 'section' => 'primary']) ?>
*
* @see Backend\Widgets\Form
* @param array $options Render options
* @return string Rendered HTML for the form.
*/
public function formRender($options = [])
{
if (!$this->formWidget) {
throw new ApplicationException(Lang::get('backend::lang.form.behavior_not_ready'));
}
// Sections provided by the behavior, then use the widget as fallback
$section = strtolower($options['section'] ?? '');
switch ($section) {
case 'buttons':
return $this->formMakePartial($this->isPopupDesign() ? 'popup_buttons' : 'buttons');
}
return $this->formWidget->render($options);
}
/**
* formRenderDesign renders a preset form design as either:
* basic, custom, sidebar, document, popup
*/
public function formRenderDesign($options = [])
{
if ($this->controller->hasFatalError()) {
return $this->formMakePartial($this->isPopupDesign() ? 'popup_error' : 'error', [
'fatalError' => $this->controller->getFatalError()
]);
}
if (!isset($options['displayMode'])) {
$options['displayMode'] = $this->getDesignDisplayMode();
}
$this->vars['options'] = $options;
$displayMode = strtolower($options['displayMode'] ?? 'basic');
switch ($displayMode) {
case 'popup':
case 'sidebar':
case 'document':
return $this->formMakePartial("mode_{$displayMode}");
case 'custom':
return $this->formRender();
default:
return $this->formMakePartial('mode_basic');
}
}
/**
* formMakePartial is a controller accessor for making partials within this behavior.
* @param string $partial
* @param array $params
* @return string
*/
public function formMakePartial($partial, $params = [])
{
$contents = $this->controller->makePartial('form_'.$partial, $params + $this->vars, false);
if (!$contents) {
$contents = $this->makePartial($partial, $params);
}
return $contents;
}
/**
* formGetModel returns the model initialized by this form behavior.
* The model will be provided by one of the page actions or AJAX
* handlers via the `initForm` method.
*
* @return \October\Rain\Database\Model
*/
public function formGetModel()
{
return $this->model;
}
/**
* formGetContext returns the active form context, either obtained from the postback
* variable called `form_context` or detected from the configuration,
* or routing parameters.
*
* @return string
*/
public function formGetContext()
{
return post('form_context') ?: $this->context;
}
/**
* createModel internal method used to prepare the form model object.
*
* @return \October\Rain\Database\Model
*/
protected function createModel()
{
return App::make($this->config->modelClass);
}
/**
* makeRedirect returns a Redirect object based on supplied context and parses
* the model primary key.
*
* @param string $context Redirect context, eg: create, update, delete
* @param Model $model The active model to parse in it's ID and attributes.
* @return Redirect
*/
public function makeRedirect($context = null, $model = null, $queryParams = [])
{
$redirectUrl = null;
if (post('close') && !ends_with($context, '-close')) {
$context .= '-close';
}
if (post('refresh', false)) {
return Redirect::refresh();
}
if (post('redirect', true)) {
$redirectUrl = $this->getRedirectUrl($context);
}
if ($model && $redirectUrl) {
$redirectUrl = RouterHelper::replaceParameters($model, $redirectUrl);
}
$url = $this->controller->formGetRedirectUrl($context, $model);
if ($url) {
$redirectUrl = $url;
}
if (!$redirectUrl) {
return null;
}
if ($queryParams) {
$redirectUrl .= '?' . http_build_query($queryParams);
}
if (starts_with($redirectUrl, ['//', 'http://', 'https://'])) {
$redirect = Redirect::to($redirectUrl);
}
else {
$redirect = Backend::redirect($redirectUrl);
}
return $redirect;
}
/**
* getRedirectUrl is an internal method that returns a redirect URL from the config
* based on supplied context. Otherwise the default redirect is used.
*
* @param string $context Redirect context, eg: create, update, delete.
* @return string
*/
protected function getRedirectUrl($context = null)
{
$redirectContext = explode('-', $context, 2)[0];
$redirectSource = ends_with($context, '-close') ? 'redirectClose' : 'redirect';
// Get the redirect for the provided context
$redirects = [$context => $this->getConfig("{$redirectContext}[{$redirectSource}]", '')];
// Assign the default redirect afterwards to prevent the
// source for the default redirect being default[redirect]
$redirects['default'] = $this->getConfig('defaultRedirect', '');
if (empty($redirects[$context])) {
return $redirects['default'];
}
return $redirects[$context];
}
/**
* getLang parses in some default variables to a language string defined in config.
*
* @param string $name Configuration property containing the language string
* @param string $default A default language string to use if the config is not found
* @param array $extras Any extra params to include in the language string variables
* @return string The translated string.
*/
protected function getLang($name, $default = null, $extras = [])
{
$name = $this->getConfig($name, $default);
$vars = $extras + [
'name' => Lang::get($this->getConfig('name', 'backend::lang.model.name'))
];
return Lang::get($name, $vars);
}
/**
* getCustomLang parses custom messages provided by the config
*/
protected function getCustomLang(string $name, ?string $default = null, array $extras = []): string
{
$foundKey = $this->getConfig("{$this->context}[customMessages][{$name}]");
// @deprecated messages can be local to the config
if ($foundKey === null) {
$foundKey = $this->getConfig("{$this->context}[{$name}]");
}
if ($foundKey === null) {
$foundKey = $this->getConfig("customMessages[{$name}]");
}
// @deprecated flashSave overrides flashCreate and flashUpdate
if ($foundKey === null && in_array($name, ['flashCreate', 'flashUpdate'])) {
return $this->getCustomLang('flashSave', $this->customMessages[$name], $extras);
}
if ($foundKey === null) {
$foundKey = $default;
}
if ($foundKey === null) {
$foundKey = $this->customMessages[$name] ?? '???';
}
$vars = $extras + [
'name' => Lang::get($this->getConfig('name', 'backend::lang.model.name'))
];
return Lang::get($foundKey, $vars);
}
//
// Pass-through Helpers
//
/**
* formGetWidget returns the form widget used by this behavior.
*
* @return Backend\Widgets\Form
*/
public function formGetWidget()
{
return $this->formWidget;
}
/**
* formGetId returns a unique ID for the form widget used by this behavior.
* This is useful for dealing with identifiers in the markup.
*
*
...
*
* A suffix may be used passed as the first argument to reuse
* the identifier in other areas.
*
*
*
* @param string $suffix
* @return string
*/
public function formGetId($suffix = null)
{
return $this->formWidget->getId($suffix);
}
/**
* formGetSessionKey is a helper to get the form session key.
* @return string
*/
public function formGetSessionKey()
{
return $this->formWidget->getSessionKey();
}
/**
* formGetConfig returns the configuration used by this behavior. You may override this
* method in your controller as an alternative to defining a formConfig property.
* @return object
*/
public function formGetConfig()
{
$config = $this->config;
$config->modelClass = Str::normalizeClassName($config->modelClass);
return $config;
}
/**
* formSetSaveValue will override the save values passed to the form. Set the value
* to null to omit the field from the dataset.
*/
public function formSetSaveValue($key, $value)
{
$this->formWidget->setSaveDataOverride($key, $value);
}
/**
* formCheckPermission checks if a custom permission has been specified
*/
public function formCheckPermission(string $name)
{
$foundKey = $this->getConfig("permissions[{$name}]");
return $foundKey ? BackendAuth::userHasAccess($foundKey) : true;
}
//
// Overrides
//
/**
* formFindModelObject finds a Model record by its primary identifier, used by update
* actions. This logic can be changed by overriding it in the controller.
* @param string $recordId
* @return Model
*/
public function formFindModelObject($recordId)
{
if (!strlen($recordId)) {
throw new ApplicationException($this->getCustomLang('notFound', 'backend::lang.form.missing_id'));
}
$model = $this->controller->formCreateModelObject();
// Prepare query and find model record
$query = $model->newQuery();
// Remove multisite restriction
if ($this->controller->formHasMultisite($model)) {
$query->withSites();
}
elseif ($this->controller->formHasMultisiteGroup($model)) {
$query->withSiteGroups();
}
$this->controller->formExtendQuery($query);
$result = $query->find($recordId);
if (!$result) {
throw new ApplicationException($this->getCustomLang('notFound', null, [
'class' => get_class($model), 'id' => $recordId
]));
}
$result = $this->controller->formExtendModel($result) ?: $result;
return $result;
}
/**
* extendFormFields is a static helper for extending form fields
* @deprecated for best performance, use Event class directly, see docs
* @link https://docs.octobercms.com/4.x/extend/forms/form-controller.html#extending-form-fields
*/
public static function extendFormFields($callback)
{
$calledClass = self::getCalledExtensionClass();
Event::listen('backend.form.extendFields', function ($widget) use ($calledClass, $callback) {
if (!is_a($widget->getController(), $calledClass)) {
return;
}
call_user_func_array($callback, [$widget, $widget->model, $widget->getContext()]);
});
}
}
================================================
FILE: modules/backend/behaviors/ImportExportController.php
================================================
setConfig($controller->importExportConfig, $this->requiredConfig);
}
/**
* beforeDisplay fires before the page is displayed and AJAX is executed.
*/
public function beforeDisplay()
{
if ($this->controller->getAction() === 'import') {
$this->beforeDisplayImport();
}
elseif ($this->controller->getAction() === 'export') {
$this->beforeDisplayExport();
}
}
/**
* beforeDisplayImport loads the import form widgets
*/
public function beforeDisplayImport()
{
if ($this->importUploadFormWidget = $this->makeImportUploadFormWidget()) {
$this->importUploadFormWidget->bindToController();
}
if ($this->importOptionsFormWidget = $this->makeImportOptionsFormWidget()) {
$this->importOptionsFormWidget->bindToController();
}
}
/**
* beforeDisplayExport loads the export form widgets
*/
public function beforeDisplayExport()
{
if ($this->exportFormatFormWidget = $this->makeExportFormatFormWidget()) {
$this->exportFormatFormWidget->bindToController();
}
if ($this->exportOptionsFormWidget = $this->makeExportOptionsFormWidget()) {
$this->exportOptionsFormWidget->bindToController();
}
}
//
// Import
//
/**
* import action
*/
public function import()
{
if ($response = $this->checkPermissionsForType('import')) {
return $response;
}
$this->addJs('js/october.import.js');
$this->addCss('css/import.css');
$this->controller->pageTitle = $this->controller->pageTitle
?: Lang::get($this->getConfig('import[title]', 'Import Records'));
$this->prepareImportVars();
}
/**
* onImport
*/
public function onImport()
{
try {
$this->actionImport();
}
catch (MassAssignmentException $ex) {
$this->controller->handleError(new ApplicationException(Lang::get(
'backend::lang.model.mass_assignment_failed',
['attribute' => $ex->getMessage()]
)));
}
catch (Exception $ex) {
$this->controller->handleError($ex);
}
return $this->importExportMakePartial('import_result_form');
}
/**
* onImportLoadColumnSampleForm
*/
public function onImportLoadColumnSampleForm()
{
$this->actionImportLoadColumnSampleForm();
return $this->importExportMakePartial('column_sample_form');
}
/**
* onImportLoadForm
*/
public function onImportLoadForm()
{
try {
if (!$this->isCustomFileFormat()) {
$this->checkRequiredImportColumns();
}
}
catch (Exception $ex) {
$this->controller->handleError($ex);
}
return $this->importExportMakePartial('import_form');
}
//
// Export
//
/**
* export action
*/
public function export()
{
if ($response = $this->checkPermissionsForType('export')) {
return $response;
}
if ($response = $this->checkUseListExportMode()) {
return $response;
}
$this->addJs('js/october.export.js');
$this->addCss('css/export.css');
$this->controller->pageTitle = $this->controller->pageTitle
?: Lang::get($this->getConfig('export[title]', 'Export Records'));
$this->prepareExportVars();
}
/**
* download action
*/
public function download($name, $outputName = null)
{
$this->controller->pageTitle = $this->controller->pageTitle
?: Lang::get($this->getConfig('export[title]', 'Export Records'));
return $this->exportGetModel()->download($name, $outputName);
}
/**
* onExport
*/
public function onExport()
{
try {
$this->actionExport();
}
catch (MassAssignmentException $ex) {
$this->controller->handleError(new ApplicationException(Lang::get(
'backend::lang.model.mass_assignment_failed',
['attribute' => $ex->getMessage()]
)));
}
catch (Exception $ex) {
$this->controller->handleError($ex);
}
return $this->importExportMakePartial('export_result_form');
}
/**
* onExportLoadForm
*/
public function onExportLoadForm()
{
return $this->importExportMakePartial('export_form');
}
//
// Internals
//
/**
* importExportMakePartial controller accessor for making partials within this behavior.
* @param string $partial
* @param array $params
* @return string Partial contents
*/
public function importExportMakePartial($partial, $params = [])
{
$contents = $this->controller->makePartial('import_export_'.$partial, $params + $this->vars, false);
if (!$contents) {
$contents = $this->makePartial($partial, $params);
}
return $contents;
}
/**
* checkPermissionsForType checks to see if the import/export is controlled by permissions
* and if the logged in user has permissions.
*/
protected function checkPermissionsForType($type)
{
if (
($permissions = $this->getConfig($type.'[permissions]')) &&
(!BackendAuth::getUser()->hasAnyAccess((array) $permissions))
) {
throw new ForbiddenException;
}
}
/**
* makeOptionsFormWidgetForType
*/
protected function makeOptionsFormWidgetForType($type)
{
if (!$this->getConfig($type)) {
return null;
}
$fieldConfig = $this->getConfig($type.'[form]');
if ($fieldConfig !== null) {
$widgetConfig = $this->makeConfig($fieldConfig);
$widgetConfig->model = $this->getModelForType($type);
$widgetConfig->alias = $type.'OptionsForm';
$widgetConfig->arrayName = ucfirst($type).'Options';
return $this->makeWidget(\Backend\Widgets\Form::class, $widgetConfig);
}
return null;
}
/**
* getModelForType
*/
protected function getModelForType($type)
{
$cacheProperty = $type.'Model';
if ($this->{$cacheProperty} !== null) {
return $this->{$cacheProperty};
}
$modelClass = $this->getConfig($type.'[modelClass]');
if (!$modelClass) {
throw new ApplicationException(__("Please specify the modelClass property for :type", [
'type' => $type
]));
}
$model = App::make($modelClass);
$this->controller->importExportExtendModel($model);
return $this->{$cacheProperty} = $model;
}
/**
* makeListColumns
*/
protected function makeListColumns($config, $model)
{
$config = $this->makeConfig($config);
$config->model = $model;
$widget = $this->makeWidget(\Backend\Widgets\Lists::class, $config);
$columns = $widget->getColumns();
if (!isset($columns) || !is_array($columns)) {
return null;
}
$result = [];
foreach ($columns as $attribute => $column) {
$result[$attribute] = $column->label;
}
return $result;
}
/**
* getRedirectUrlForType
*/
protected function getRedirectUrlForType($type = null)
{
$redirect = $this->getConfig($type.'[redirect]');
if ($redirect !== null) {
return $redirect ? Backend::url($redirect) : 'javascript:;';
}
return $this->controller->actionUrl($type);
}
/**
* getFormatOptionsForPost returns the file format options from postback. This method
* can be used to define presets.
*/
protected function getFormatOptionsForPost(): array
{
$defaults = [
'file_format' => 'json',
'format_delimiter' => ',',
'format_enclosure' => '"',
'format_escape' => '\\',
'format_encoding' => 'UTF-8',
'first_row_titles' => true,
];
return [
'file_format' => post('file_format', $this->getConfig('defaultFormatOptions[fileFormat]', $defaults['file_format'])),
'format_delimiter' => post('format_delimiter', $this->getConfig('defaultFormatOptions[delimiter]', $defaults['format_delimiter'])),
'format_enclosure' => post('format_enclosure', $this->getConfig('defaultFormatOptions[enclosure]', $defaults['format_enclosure'])),
'format_escape' => post('format_escape', $this->getConfig('defaultFormatOptions[escape]', $defaults['format_escape'])),
'format_encoding' => post('format_encoding', $this->getConfig('defaultFormatOptions[encoding]', $defaults['format_encoding'])),
'first_row_titles' => post('first_row_titles', $this->getConfig('defaultFormatOptions[firstRowTitles]', $defaults['first_row_titles'])),
];
}
/**
* getFormatOptionsForModel returns the file format options used by models.
*/
protected function getFormatOptionsForModel(): array
{
$options = [
'fileFormat' => post('file_format', $this->getConfig('defaultFormatOptions[fileFormat]')),
'delimiter' => post('format_delimiter', $this->getConfig('defaultFormatOptions[delimiter]')),
'enclosure' => post('format_enclosure', $this->getConfig('defaultFormatOptions[enclosure]')),
'escape' => post('format_escape', $this->getConfig('defaultFormatOptions[escape]')),
'encoding' => post('format_encoding', $this->getConfig('defaultFormatOptions[encoding]')),
'firstRowTitles' => (bool) post('first_row_titles', $this->getConfig('defaultFormatOptions[firstRowTitles]', true)),
'customJson' => $this->getConfig('defaultFormatOptions[customJson]'),
];
if ($options['fileFormat'] !== 'csv_custom') {
$options['delimiter'] = null;
$options['enclosure'] = null;
$options['escape'] = null;
$options['encoding'] = null;
}
return $options;
}
/**
* isCustomFileFormat returns true if the process is using a custom format
* via `customJson` or otherwise.
*/
protected function isCustomFileFormat()
{
if (!$fileFormat = post('file_format', 'json')) {
return false;
}
if ($fileFormat !== 'json') {
return false;
}
return (bool) $this->getFormatOptionsForModel()['customJson'];
}
//
// Overrides
//
/**
* importExportGetFileName
* @return string
*/
public function importExportGetFileName()
{
return $this->exportFileName;
}
/**
* importExportExtendModel
* @param Model $model
* @return Model
*/
public function importExportExtendModel($model)
{
return $model;
}
/**
* importExportExtendColumns
*/
public function importExportExtendColumns($columns, $context = null)
{
return $columns;
}
}
================================================
FILE: modules/backend/behaviors/ListController.php
================================================
listConfig)) {
$this->listDefinitions = $controller->listConfig;
$this->primaryDefinition = key($this->listDefinitions);
}
else {
$this->listDefinitions = ['list' => $controller->listConfig];
$this->primaryDefinition = 'list';
}
// Build configuration
$this->setConfig($this->listDefinitions[$this->primaryDefinition], $this->requiredConfig);
}
/**
* makeLists creates all the list widgets based on the definitions.
* @return array
*/
public function makeLists()
{
foreach ($this->listDefinitions as $definition => $config) {
$this->listWidgets[$definition] = $this->makeList($definition);
}
return $this->listWidgets;
}
/**
* makeList prepares the widgets used by this action
* @return \Backend\Classes\WidgetBase
*/
public function makeList($definition = null)
{
if (!$definition || !isset($this->listDefinitions[$definition])) {
$definition = $this->primaryDefinition;
}
$listConfig = $this->config = $this->controller->listGetConfig($definition);
// Create the model
$model = $this->createModel();
$model = $this->controller->listExtendModel($model, $definition);
// Prepare the list widget
$widgetConfig = $this->makeConfig($listConfig->list);
$widgetConfig->model = $model;
$widgetConfig->alias = $listConfig->widgetAlias ?? $definition;
// Prepare the columns configuration
$configFieldsToTransfer = [
'recordUrl',
'recordOnClick',
'recordsPerPage',
'perPageOptions',
'showPageNumbers',
'noRecordsMessage',
'defaultSort',
'showSorting',
'showSetup',
'expandLastColumn',
'showCheckboxes',
'customViewPath',
'customPageName',
];
foreach ($configFieldsToTransfer as $field) {
if (isset($listConfig->{$field})) {
$widgetConfig->{$field} = $listConfig->{$field};
}
}
// List Widget with extensibility
$structureConfig = $this->makeListStructureConfig($widgetConfig, $listConfig);
if ($structureConfig) {
$widget = $this->makeWidget(\Backend\Widgets\ListStructure::class, $structureConfig);
}
else {
$widget = $this->makeWidget(\Backend\Widgets\Lists::class, $widgetConfig);
}
$widget->bindEvent('list.extendColumns', function () use ($widget) {
$this->controller->listExtendColumns($widget);
});
$widget->bindEvent('list.extendQueryBefore', function ($query) use ($definition) {
$this->controller->listExtendQueryBefore($query, $definition);
});
$widget->bindEvent('list.extendQuery', function ($query) use ($definition) {
$this->controller->listExtendQuery($query, $definition);
});
$widget->bindEvent('list.extendSortColumn', function ($query, $sortColumn, $sortDirection) use ($definition) {
$this->controller->listExtendSortColumn($query, $sortColumn, $sortDirection, $definition);
});
$widget->bindEvent('list.extendRecords', function ($records) use ($definition) {
return $this->controller->listExtendRecords($records, $definition);
});
$widget->bindEvent('list.injectRowClass', function ($record) use ($definition) {
return $this->controller->listInjectRowClass($record, $definition);
});
$widget->bindEvent('list.overrideColumnValue', function ($record, $column, $value) use ($definition) {
return $this->controller->listOverrideColumnValue($record, $column->columnName, $definition);
});
$widget->bindEvent('list.overrideHeaderValue', function ($column, $value) use ($definition) {
return $this->controller->listOverrideHeaderValue($column->columnName, $definition);
});
$widget->bindEvent('list.overrideRecordAction', function ($record, $url, $onClick) use ($definition) {
return $this->controller->listOverrideRecordUrl($record, $definition);
});
$widget->bindEvent('list.reorderStructure', function ($record) use ($definition) {
return $this->controller->listAfterReorder($record, $definition);
});
$widget->bindEvent('list.refresh', function ($result) use ($widget, $definition) {
return $this->controller->listExtendRefreshResults($widget, $result, $definition);
});
$widget->bindToController();
// Prepare the toolbar widget (optional)
if (isset($listConfig->toolbar)) {
$toolbarConfig = $this->makeConfig($listConfig->toolbar);
$toolbarConfig->alias = $widget->alias . 'Toolbar';
$toolbarWidget = $this->makeWidget(\Backend\Widgets\Toolbar::class, $toolbarConfig);
$toolbarWidget->listWidgetId = $widget->getId();
$toolbarWidget->cssClasses[] = 'list-header';
// Pass the list setup AJAX handler to the toolbar
if (isset($listConfig->showSetup) && $listConfig->showSetup) {
$toolbarWidget->setupHandler = $widget->getEventHandler('onLoadSetup');
}
// Link the Search Widget to the List Widget
if ($searchWidget = $toolbarWidget->getSearchWidget()) {
$searchWidget->bindEvent('search.submit', function () use ($widget, $searchWidget) {
$widget->setSearchTerm($searchWidget->getActiveTerm(), true);
return $widget->onRefresh();
});
// Pass search options
$widget->setSearchOptions([
'mode' => $searchWidget->mode,
'scope' => $searchWidget->scope,
]);
// Find predefined search term
$widget->setSearchTerm($searchWidget->getActiveTerm());
}
// Bind to controller
$toolbarWidget->bindToController();
$this->toolbarWidgets[$definition] = $toolbarWidget;
}
// Prepare the filter widget (optional)
if (isset($listConfig->filter)) {
$widget->cssClasses[] = 'list-flush';
$filterConfig = $this->makeConfig($listConfig->filter);
$filterConfig->model = $model;
$filterConfig->alias = $widget->alias . 'Filter';
$filterConfig->customPageName = $listConfig->customPageName ?? null;
$filterWidget = $this->makeWidget(\Backend\Widgets\Filter::class, $filterConfig);
// Filter the list when the scopes are changed
$filterWidget->bindEvent('filter.update', function() use ($widget) {
return $widget->onFilter();
});
// Filter Widget with extensibility
$filterWidget->bindEvent('filter.extendScopes', function() use ($filterWidget) {
$this->controller->listFilterExtendScopes($filterWidget);
});
// Extend the query of the list of options
$filterWidget->bindEvent('filter.extendQuery', function($query, $scope) {
$this->controller->listFilterExtendQuery($query, $scope);
});
// Apply predefined filter values
$widget->addFilter([$filterWidget, 'applyAllScopesToQuery']);
// Bind to controller
$filterWidget->bindToController();
$this->filterWidgets[$definition] = $filterWidget;
}
return $widget;
}
/**
* makeListStructureConfig
*/
protected function makeListStructureConfig(object $widgetConfig, object $config): ?object
{
// @deprecated old API
if (isset($config->showTree)) {
$widgetConfig->showTree = $config->showTree;
$widgetConfig->treeExpanded = $config->treeExpanded ?? false;
$widgetConfig->showReorder = false;
if (!isset($config->structure)) {
return $widgetConfig;
}
}
// New API
if (isset($config->structure)) {
return $this->mergeConfig($widgetConfig, $config->structure);
}
return null;
}
/**
* index controller action
* @return void
*/
public function index()
{
if (!$this->controller->pageTitle) {
$this->controller->pageTitle = Lang::get($this->getConfig(
'title',
'backend::lang.list.default_title'
));
}
$this->controller->bodyClass ??= 'slim-container';
$this->makeLists();
}
/**
* index_onDelete bulk deletes records.
* @return void
*/
public function index_onDelete()
{
if (method_exists($this->controller, 'onDelete')) {
return call_user_func_array([$this->controller, 'onDelete'], func_get_args());
}
// Establish the list definition
$definition = post('definition', $this->primaryDefinition);
if (!isset($this->listDefinitions[$definition])) {
throw new ApplicationException(Lang::get('backend::lang.list.missing_parent_definition', compact('definition')));
}
$this->config = $this->controller->listGetConfig($definition);
// Check conditions for deletion
if (!$this->listCanDeleteRecords()) {
throw new ForbiddenException;
}
// Validate checked identifiers
$checkedIds = post('checked');
if (!$checkedIds || !is_array($checkedIds) || !count($checkedIds)) {
Flash::error(Lang::get('backend::lang.list.delete_selected_empty'));
return $this->controller->listRefresh();
}
// Create the model
$model = $this->createModel();
$model = $this->controller->listExtendModel($model, $definition);
// Create the query
$query = $model->newQuery();
$this->controller->listExtendQueryBefore($query, $definition);
$query->whereIn($model->getQualifiedKeyName(), $checkedIds);
$this->controller->listExtendQuery($query, $definition);
// Delete records
$records = $query->get();
if ($records->count()) {
foreach ($records as $record) {
$record->delete();
}
Flash::success(Lang::get('backend::lang.list.delete_selected_success'));
}
else {
Flash::error(Lang::get('backend::lang.list.delete_selected_empty'));
}
return $this->controller->listRefresh($definition);
}
/**
* listCanDeleteRecords determines if records can be deleted from the list
*/
protected function listCanDeleteRecords(): bool
{
if (!$this->getConfig('showCheckboxes')) {
return false;
}
if ($requiredPermission = $this->getConfig('requiredPermissions[recordDelete]')) {
return BackendAuth::userHasAccess($requiredPermission);
}
return true;
}
/**
* createModel is an internal method used to prepare the list model object.
* @return \October\Rain\Database\Model
*/
protected function createModel()
{
return App::make($this->config->modelClass);
}
/**
* listRender renders the widget collection.
* @param string $definition Optional list definition.
* @return string Rendered HTML for the list.
*/
public function listRender($definition = null)
{
if (!count($this->listWidgets)) {
throw new ApplicationException(Lang::get('backend::lang.list.behavior_not_ready'));
}
if (!$definition || !isset($this->listDefinitions[$definition])) {
$definition = $this->primaryDefinition;
}
$vars = [
'toolbar' => null,
'filter' => null,
'list' => null,
];
if (isset($this->toolbarWidgets[$definition])) {
$vars['toolbar'] = $this->toolbarWidgets[$definition];
}
if (isset($this->filterWidgets[$definition])) {
$vars['filter'] = $this->filterWidgets[$definition];
}
$vars['list'] = $this->listWidgets[$definition];
return $this->listMakePartial('container', $vars);
}
/**
* listMakePartial is a controller accessor for making partials within this behavior.
* @param string $partial
* @param array $params
* @return string Partial contents
*/
public function listMakePartial($partial, $params = [])
{
$contents = $this->controller->makePartial('list_'.$partial, $params + $this->vars, false);
if (!$contents) {
$contents = $this->makePartial($partial, $params);
}
return $contents;
}
/**
* listRefresh refreshes the list container only, useful for returning in custom AJAX requests.
* @param string $definition Optional list definition.
* @return array The list element selector as the key, and the list contents are the value.
*/
public function listRefresh($definition = null)
{
if (!count($this->listWidgets)) {
$this->makeLists();
}
if (!$definition || !isset($this->listDefinitions[$definition])) {
$definition = $this->primaryDefinition;
}
return $this->listWidgets[$definition]->onRefresh();
}
/**
* listGetWidget returns the widget used by this behavior.
* @return \Backend\Classes\WidgetBase
*/
public function listGetWidget($definition = null)
{
if (!$definition) {
$definition = $this->primaryDefinition;
}
return array_get($this->listWidgets, $definition);
}
/**
* listGetFilterWidget returns the filter widget used by this behavior.
* @return \Backend\Classes\WidgetBase
*/
public function listGetFilterWidget($definition = null)
{
if (!$definition) {
$definition = $this->primaryDefinition;
}
return array_get($this->filterWidgets, $definition);
}
/**
* listGetToolbarWidget returns the toolbar widget used by this behavior.
* @return \Backend\Classes\WidgetBase
*/
public function listGetToolbarWidget($definition = null)
{
if (!$definition) {
$definition = $this->primaryDefinition;
}
return array_get($this->toolbarWidgets, $definition);
}
/**
* listGetId returns a unique ID for the list widget used by this behavior.
* This is useful for dealing with identifiers in the markup.
*
*
...
*
* A suffix may be used passed as the first argument to reuse
* the identifier in other areas.
*
*
*
* @param string $suffix
* @return string
*/
public function listGetId($suffix = null, $definition = null)
{
return $this->listGetWidget($definition)->getId($suffix);
}
/**
* listGetConfig returns the configuration used by this behavior. You may override this
* method in your controller as an alternative to defining a listConfig property.
* @return object|null
*/
public function listGetConfig($definition = null)
{
if (!$definition) {
$definition = $this->primaryDefinition;
}
$config = array_get($this->listConfig, $definition);
if (!$config) {
$config = $this->listConfig[$definition] = $this->makeConfig($this->listDefinitions[$definition], $this->requiredConfig);
}
return $config;
}
//
// Overrides
//
/**
* extendListColumns is a static helper for extending list columns.
* @deprecated for best performance, use Event class directly, see docs
* @link https://docs.octobercms.com/4.x/extend/lists/list-controller.html#extending-column-definitions
*/
public static function extendListColumns($callback)
{
$calledClass = self::getCalledExtensionClass();
Event::listen('backend.list.extendColumns', function ($widget) use ($calledClass, $callback) {
if (!is_a($widget->getController(), $calledClass)) {
return;
}
call_user_func_array($callback, [$widget, $widget->model]);
});
}
/**
* extendListFilterScopes is a static helper for extending filter scopes.
* @deprecated for best performance, use Event class directly, see docs
* @link https://docs.octobercms.com/4.x/extend/lists/list-controller.html#extending-filter-scopes
*/
public static function extendListFilterScopes($callback)
{
$calledClass = self::getCalledExtensionClass();
Event::listen('backend.filter.extendScopes', function ($widget) use ($calledClass, $callback) {
if (!is_a($widget->getController(), $calledClass)) {
return;
}
call_user_func_array($callback, [$widget]);
});
}
}
================================================
FILE: modules/backend/behaviors/RelationController.php
================================================
"Create :name",
'buttonCreateForm' => "Create",
'buttonCancelForm' => "Cancel",
'buttonCloseForm' => "Close",
'buttonUpdate' => "Update :name",
'buttonUpdateForm' => "Update",
'buttonAdd' => "Add :name",
'buttonAddMany' => "Add Selected",
'buttonAddForm' => "Add",
'buttonLink' => "Link :name",
'buttonDelete' => "Delete",
'buttonDeleteMany' => "Delete Selected",
'buttonRemove' => "Remove",
'buttonRemoveMany' => "Remove Selected",
'buttonUnlink' => "Unlink",
'buttonUnlinkMany' => "Unlink Selected",
'confirmDelete' => "Are you sure?",
'confirmUnlink' => "Are you sure?",
'titlePreviewForm' => "Preview :name",
'titleCreateForm' => "Create :name",
'titleUpdateForm' => "Update :name",
'titleLinkForm' => "Link a New :name",
'titleAddForm' => "Add a New :name",
'titlePivotForm' => "Related :name Data",
'flashCreate' => ":name Created",
'flashUpdate' => ":name Updated",
'flashDelete' => ":name Deleted",
'flashAdd' => ":name Added",
'flashLink' => ":name Linked",
'flashRemove' => ":name Removed",
'flashUnlink' => ":name Unlinked",
];
/**
* __construct the behavior
* @param Backend\Classes\Controller $controller
*/
public function __construct($controller)
{
parent::__construct($controller);
// Build configuration
$this->setConfig($controller->relationConfig ?? [], $this->requiredConfig);
}
/**
* beforeDisplay fires before the page is displayed and AJAX is executed.
*/
public function beforeDisplay()
{
$this->addJs('js/october.relation.js');
$this->addCss('css/relation.css');
}
/**
* validateField validates the supplied field and initializes the relation manager.
* @param string $field The relationship field.
* @return string The active field name.
*/
protected function validateField($field = null)
{
$field = $field ?: post(self::PARAM_FIELD);
if ($field && $field !== $this->field) {
$this->initRelation($this->model, $field);
}
if (!$field && !$this->field) {
throw new ApplicationException(Lang::get('backend::lang.relation.missing_definition', compact('field')));
}
return $field ?: $this->field;
}
/**
* prepareVars for display
*/
public function prepareVars()
{
$this->vars['relationLabel'] = $this->config->label ?: $this->field;
$this->vars['relationField'] = $this->field;
$this->vars['relationPopupSize'] = $this->popupSize;
$this->vars['relationReadOnly'] = $this->readOnly;
$this->vars['relationType'] = $this->relationType;
$this->vars['relationSearchWidget'] = $this->searchWidget;
$this->vars['relationToolbarWidget'] = $this->toolbarWidget;
$this->vars['relationToolbarButtons'] = $this->toolbarButtons;
$this->vars['relationSessionKey'] = $this->relationSessionKey;
$this->vars['relationExtraConfig'] = $this->extraConfig;
// Manage
$this->vars['relationManageId'] = $this->manageId;
$this->vars['relationManageTitle'] = $this->manageTitle;
$this->vars['relationManageFilterWidget'] = $this->manageFilterWidget;
$this->vars['relationManageFormWidget'] = $this->manageFormWidget;
$this->vars['relationManageListWidget'] = $this->manageListWidget;
$this->vars['relationManageModel'] = $this->manageModel;
$this->vars['relationManageMode'] = $this->manageMode;
// View
$this->vars['relationViewFilterWidget'] = $this->viewFilterWidget;
$this->vars['relationViewFormWidget'] = $this->viewFormWidget;
$this->vars['relationViewListWidget'] = $this->viewListWidget;
$this->vars['relationViewModel'] = $this->viewModel;
$this->vars['relationViewMode'] = $this->viewMode;
// Pivot
$this->vars['relationPivotId'] = $this->pivotId;
$this->vars['relationPivotTitle'] = $this->pivotTitle;
$this->vars['relationPivotWidget'] = $this->pivotWidget;
// Misc
$this->vars['externalToolbarAppState'] = $this->externalToolbarAppState;
$this->vars['formSessionKey'] = post('_form_session_key', post('_session_key', FormHelper::getSessionKey()));
// @deprecated
$this->vars['relationManageWidget'] = $this->relationGetManageWidget();
$this->vars['relationViewWidget'] = $this->relationGetViewWidget();
}
/**
* beforeAjax is needed because each AJAX request must initialize the
* relation's field name (_relation_field).
*/
protected function beforeAjax()
{
if ($this->initialized) {
return;
}
if ($fatalError = $this->controller->getFatalError()) {
throw new ApplicationException($fatalError);
}
$this->validateField();
$this->setExtraConfigForChain();
$this->prepareVars();
$this->initialized = true;
}
//
// Interface
//
/**
* initRelation prepares the widgets used by this behavior
* @param Model $model
* @param string $field
* @return void
*/
public function initRelation($model, $field = null)
{
if ($extraConfig = post(self::PARAM_EXTRA_CONFIG)) {
$this->setExtraConfig($extraConfig);
$this->initNestedRelation($model, $field);
}
$this->initRelationInternal($model, $field);
}
/**
* initRelationInternal is an internal method for initRelation
* @param Model $model
* @param string $field
* @return void
*/
protected function initRelationInternal($model, $field = null)
{
if ($this->originalConfig === null) {
$this->originalConfig = $this->controller->relationGetConfig();
}
if ($field === null) {
$field = post(self::PARAM_FIELD);
}
$this->config = $this->originalConfig;
$this->model = $model;
$this->field = $field;
if (!$field) {
return;
}
if (!$this->model) {
throw new ApplicationException(Lang::get('backend::lang.relation.missing_model', [
'class' => get_class($this->controller),
]));
}
if (!$this->model instanceof Model) {
throw new ApplicationException(Lang::get('backend::lang.model.invalid_class', [
'model' => get_class($this->model),
'class' => get_class($this->controller),
]));
}
// Configuration details
if (!$this->relationHasField($field)) {
throw new ApplicationException(Lang::get('backend::lang.relation.missing_definition', compact('field')));
}
$this->applyExtraConfig($field);
$this->alias = camel_case('relation ' . HtmlHelper::nameToId($field));
$this->config = $this->makeConfig($this->originalConfig->{$field}, $this->requiredRelationProperties);
$this->controller->relationExtendConfig($this->config, $this->field, $this->model);
$this->manageId = $this->getManageIdForField($field);
$this->pivotId = post('pivot_id');
$this->foreignId = post('foreign_id');
[$this->sessionKey, $this->relationSessionKey] = $this->getSessionKeysForField($field);
// Relationship details
[$nestedModel, $nestedField] = $this->makeNestedRelationModel($this->model, $this->config->valueFrom ?? $field);
if (!$nestedModel->hasRelation($nestedField)) {
throw new ApplicationException(Lang::get('backend::lang.model.missing_relation', ['class' => get_class($nestedModel), 'relation' => $nestedField]));
}
$this->relationParent = $nestedModel;
$this->relationName = $nestedField;
$this->relationType = $nestedModel->getRelationType($nestedField);
$this->relationObject = $nestedModel->{$nestedField}();
$this->relationModel = $this->relationObject instanceof HasOneOrMany
? $this->relationObject->make()
: $this->relationObject->getRelated();
$this->readOnly = $this->getConfig('readOnly');
$this->popupSize = $this->getConfig('popupSize', 950);
$this->externalToolbarAppState = $this->getConfig('externalToolbarAppState');
$this->eventTarget = $this->evalEventTarget();
$this->deferredBinding = $this->evalDeferredBinding();
$this->viewMode = $this->evalViewMode();
$this->manageMode = $this->evalManageMode();
$this->manageTitle = $this->evalManageTitle();
$this->pivotTitle = $this->evalPivotTitle();
$this->toolbarButtons = $this->evalToolbarButtons();
// Toolbar widget
if ($this->toolbarWidget = $this->makeToolbarWidget()) {
$this->toolbarWidget->bindToController();
}
// Search widget
if ($this->searchWidget = $this->makeSearchWidget()) {
$this->searchWidget->bindToController();
}
// View widgets
if ($this->viewFilterWidget = $this->makeFilterWidgetFor('view')) {
$this->controller->relationExtendViewFilterWidget($this->viewFilterWidget, $this->field, $this->model);
$this->viewFilterWidget->bindToController();
}
if ($this->viewListWidget = $this->makeViewListWidget()) {
$this->controller->relationExtendViewListWidget($this->viewListWidget, $this->field, $this->model);
$this->controller->relationExtendViewWidget($this->viewListWidget, $this->field, $this->model);
$this->viewListWidget->bindToController();
}
if ($this->viewFormWidget = $this->makeViewFormWidget()) {
$this->controller->relationExtendViewFormWidget($this->viewFormWidget, $this->field, $this->model);
$this->controller->relationExtendViewWidget($this->viewFormWidget, $this->field, $this->model);
$this->viewFormWidget->bindToController();
}
// Manage widgets
if ($this->manageFilterWidget = $this->makeFilterWidgetFor('manage')) {
$this->controller->relationExtendManageFilterWidget($this->manageFilterWidget, $this->field, $this->model);
$this->manageFilterWidget->bindToController();
}
if ($this->manageListWidget = $this->makeManageListWidget()) {
$this->controller->relationExtendManageListWidget($this->manageListWidget, $this->field, $this->model);
$this->controller->relationExtendManageWidget($this->manageListWidget, $this->field, $this->model);
$this->manageListWidget->bindToController();
}
if ($this->manageFormWidget = $this->makeManageFormWidget()) {
$this->controller->relationExtendManageFormWidget($this->manageFormWidget, $this->field, $this->model);
$this->controller->relationExtendManageWidget($this->manageFormWidget, $this->field, $this->model);
$this->manageFormWidget->bindToController();
}
// Pivot widget
if ($this->pivotWidget = $this->makePivotFormWidget()) {
$this->controller->relationExtendPivotFormWidget($this->pivotWidget, $this->field, $this->model);
$this->controller->relationExtendPivotWidget($this->pivotWidget, $this->field, $this->model);
$this->pivotWidget->bindToController();
}
}
/**
* relationHasField
*/
public function relationHasField(string $field): bool
{
if ($this->originalConfig === null) {
$this->config = $this->originalConfig = $this->controller->relationGetConfig();
}
return (bool) ($this->originalConfig->{$field} ?? false);
}
/**
* relationRegisterField registers a new relation dynamically
*/
public function relationRegisterField(string $relationName, array $config)
{
if ($this->originalConfig === null) {
$this->config = $this->originalConfig = $this->controller->relationGetConfig();
}
$this->originalConfig->{$relationName} = $config;
}
/**
* relationRender renders the relationship manager.
* @param string $field The relationship field.
* @param array $options
* @return string Rendered HTML for the relationship manager.
*/
public function relationRender($field = null, $options = [])
{
if ($field === null) {
$field = $this->field;
}
// Session key
if (is_string($options)) {
$options = ['sessionKey' => $options];
}
if (isset($options['sessionKey'])) {
$this->sessionKey = $options['sessionKey'];
}
// Apply options and extra config
$allowConfig = ['readOnly', 'readOnlyDefault', 'recordUrl', 'recordOnClick'];
$extraConfig = array_only($options, $allowConfig);
$this->setExtraConfigForRender($extraConfig);
$this->applyExtraConfig($field);
// Initialize
$this->validateField($field);
$this->prepareVars();
// Determine the partial to use based on the supplied section option
$section = strtolower($options['section'] ?? '');
return match ($section) {
'toolbar' => $this->toolbarWidget?->render(),
'view' => $this->relationMakePartial('view'),
default => $this->relationMakePartial('container'),
};
}
/**
* relationRefresh refreshes the relation container only, useful for returning in custom AJAX requests.
* @param string $field Relation definition.
* @return array The relation element selector as the key, and the relation view contents are the value.
*/
public function relationRefresh($field = null)
{
$field = $this->validateField($field);
$result = ['#'.$this->relationGetId('view') => $this->relationRenderView($field)];
if ($toolbar = $this->relationRenderToolbar($field)) {
$result['#'.$this->relationGetId('toolbar')] = $toolbar;
}
if ($eventResult = $this->controller->relationExtendRefreshResults($field)) {
$result = $eventResult + $result;
}
return $result;
}
/**
* relationRenderToolbar renders the toolbar only.
* @param string $field The relationship field.
* @return string Rendered HTML for the toolbar.
*/
public function relationRenderToolbar($field = null)
{
return $this->relationRender($field, ['section' => 'toolbar']);
}
/**
* relationRenderView renders the view only.
* @param string $field The relationship field.
* @return string Rendered HTML for the view.
*/
public function relationRenderView($field = null)
{
return $this->relationRender($field, ['section' => 'view']);
}
/**
* relationMakePartial is a controller accessor for making partials within this behavior.
* @param string $partial
* @param array $params
* @return string Partial contents
*/
public function relationMakePartial($partial, $params = [])
{
$contents = $this->controller->makePartial('relation_'.$partial, $params + $this->vars, false);
if (!$contents) {
$contents = $this->makePartial($partial, $params);
}
return $contents;
}
/**
* relationGetId returns a unique ID for this relation and field combination.
* @param string $suffix A suffix to use with the identifier.
* @return string
*/
public function relationGetId($suffix = null)
{
$id = class_basename($this);
if ($this->field) {
$id .= '-' . HtmlHelper::nameToId($this->field);
}
if ($suffix !== null) {
$id .= '-' . $suffix;
}
return $this->controller->getId($id);
}
/**
* relationGetSessionKey returns the active session key for relation binding.
*/
public function relationGetSessionKey()
{
return $this->relationSessionKey;
}
/**
* relationGetConfig returns the configuration used by this behavior. You may override this
* method in your controller as an alternative to defining a relationConfig property.
* @return object
*/
public function relationGetConfig()
{
return $this->config;
}
/**
* relationGetMessage is a public API for accessing custom messages
*/
public function relationGetMessage(string $code): string
{
return $this->getCustomLang($code);
}
//
// Widgets
//
/**
* makeFilterWidgetFor
* @param $type string Either 'manage' or 'view'
* @return \Backend\Classes\WidgetBase|null
*/
protected function makeFilterWidgetFor($type)
{
if (!$this->getConfig($type . '[filter]')) {
return null;
}
$filterConfig = $this->makeConfig($this->getConfig("{$type}[filter]"));
$filterConfig->model = $this->relationModel;
$filterConfig->alias = $this->alias . ucfirst($type) . 'Filter';
$filterConfig->customPageName = $this->getConfig("{$type}[customPageName]", false);
$filterWidget = $this->makeWidget(\Backend\Widgets\Filter::class, $filterConfig);
return $filterWidget;
}
/**
* makeToolbarWidget
*/
protected function makeToolbarWidget()
{
$defaultConfig = [];
// Add buttons to toolbar
$defaultButtons = null;
if (!$this->readOnly && $this->toolbarButtons) {
$defaultButtons = '~/modules/backend/behaviors/relationcontroller/partials/_toolbar.php';
}
$defaultConfig['buttons'] = $this->getConfig('view[toolbarPartial]', $defaultButtons);
// Make config
$toolbarConfig = $this->makeConfig($this->getConfig('toolbar', $defaultConfig));
$toolbarConfig->alias = $this->alias . 'Toolbar';
// Add search to toolbar
$useSearch = $this->viewMode === 'multi' && $this->getConfig('view[showSearch]');
if ($useSearch) {
$toolbarConfig->search = [
'prompt' => 'backend::lang.list.search_prompt'
];
}
// No buttons, no search should mean no toolbar
if (empty($toolbarConfig->search) && empty($toolbarConfig->buttons)) {
return;
}
$toolbarWidget = $this->makeWidget(\Backend\Widgets\Toolbar::class, $toolbarConfig);
$toolbarWidget->cssClasses[] = 'list-header';
return $toolbarWidget;
}
/**
* makeSearchWidget
*/
protected function makeSearchWidget()
{
if (!$this->getConfig('manage[showSearch]')) {
return null;
}
$config = $this->makeConfig();
$config->alias = $this->alias . 'ManageSearch';
$config->growable = false;
$config->prompt = 'backend::lang.list.search_prompt';
$widget = $this->makeWidget(\Backend\Widgets\Search::class, $config);
$widget->cssClasses[] = 'recordfinder-search';
// Persist the search term across AJAX requests only
if (!Request::ajax()) {
$widget->setActiveTerm(null);
}
return $widget;
}
//
// Helpers
//
/**
* findExistingRelationIds returns the existing record IDs for the relation.
*/
protected function findExistingRelationIds($checkIds = null)
{
$foreignKeyName = $this->relationModel->getQualifiedKeyName();
$results = $this->relationObject
->getBaseQuery()
->select($foreignKeyName);
if ($checkIds !== null && is_array($checkIds) && count($checkIds)) {
$results = $results->whereIn($foreignKeyName, $checkIds);
}
return $results->pluck($foreignKeyName)->all();
}
/**
* evalDeferredBinding
*/
protected function evalDeferredBinding(): bool
{
if ($this->relationType === 'hasManyThrough') {
return false;
}
return $this->getConfig('deferredBinding') || !$this->relationParent->exists;
}
/**
* evalToolbarButtons determines the default buttons based on the model relationship type.
*/
protected function evalToolbarButtons(): array
{
$buttons = $this->getConfig('view[toolbarButtons]');
if ($buttons === false) {
return [];
}
elseif (is_string($buttons)) {
return array_map('trim', explode('|', $buttons));
}
elseif (is_array($buttons)) {
return $buttons;
}
if ($this->manageMode === 'pivot') {
return ['add', 'remove'];
}
switch ($this->relationType) {
case 'hasMany':
case 'morphMany':
return ['create', 'delete'];
case 'belongsToMany':
case 'morphedByMany':
case 'morphToMany':
return ['create', 'add', 'delete', 'remove'];
case 'hasOne':
case 'morphOne':
case 'belongsTo':
return ['create', 'update', 'link', 'delete', 'unlink'];
case 'hasManyThrough':
return [];
}
return [];
}
/**
* evalFormContext determines supplied form context
*/
protected function evalFormContext($mode = 'manage', $exists = false)
{
$config = $this->config->{$mode} ?? [];
$context = FormField::CONTEXT_CREATE;
if ($exists) {
$context = FormField::CONTEXT_UPDATE;
}
if ($this->readOnly) {
$context = FormField::CONTEXT_PREVIEW;
}
if ($configContext = array_get($config, 'context')) {
$context = is_array($configContext)
? array_get($configContext, $context, $context)
: $configContext;
}
return $context;
}
/**
* makeConfigForMode returns the configuration for a mode (view, manage, pivot) for an
* expected type (list, form) and uses fallback configuration
*/
protected function makeConfigForMode($mode = 'view', $type = 'list')
{
$config = null;
// Look for $this->config->view['list']
if (
isset($this->config->{$mode}) &&
array_key_exists($type, $this->config->{$mode})
) {
$config = $this->config->{$mode}[$type];
}
// Look for $this->config->list
elseif (isset($this->config->{$type})) {
$config = $this->config->{$type};
}
// Apply substitutes:
// - view.list => manage.list
if ($config === null) {
if ($mode === 'manage' && $type === 'list') {
return $this->makeConfigForMode('view', $type);
}
return false;
}
return $this->makeConfig($config);
}
/**
* getCustomLang parses custom messages provided by the config
*/
protected function getCustomLang(string $name, ?string $default = null, array $extras = []): string
{
$foundKey = $this->getConfig("customMessages[{$name}]");
if ($foundKey === null) {
$foundKey = $this->originalConfig->customMessages[$name] ?? null;
}
if ($foundKey === null) {
$foundKey = $default;
}
if ($foundKey === null) {
$foundKey = $this->customMessages[$name] ?? '???';
}
$vars = $extras + [
'name' => Lang::get($this->getConfig('label', $this->field))
];
return Lang::get($foundKey, $vars);
}
/**
* showFlashMessage displays a flash message if its found
*/
protected function showFlashMessage(string $message): void
{
if (!$this->useFlashMessages()) {
return;
}
if ($message = $this->getCustomLang($message)) {
Flash::success($message);
}
}
/**
* useFlashMessages determines if flash messages should be used
*/
protected function useFlashMessages(): bool
{
$useFlash = $this->getConfig('showFlash');
if ($useFlash === null) {
$useFlash = $this->originalConfig->showFlash ?? null;
}
if ($useFlash === null) {
$useFlash = true;
}
return $useFlash;
}
}
================================================
FILE: modules/backend/behaviors/ReorderController.php
================================================
config = $this->makeConfig($this->controller->reorderConfig, $this->requiredConfig);
/*
* Populate from config
*/
$this->nameFrom = $this->getConfig('nameFrom', $this->nameFrom);
}
/**
* beforeDisplay fires before the page is displayed and AJAX is executed.
*/
public function beforeDisplay()
{
/*
* Widgets
*/
if ($this->toolbarWidget = $this->makeToolbarWidget()) {
$this->toolbarWidget->bindToController();
}
}
//
// Controller actions
//
public function reorder()
{
$this->addJs('js/october.reorder.js');
$this->controller->pageTitle = $this->controller->pageTitle
?: Lang::get($this->getConfig('title', 'backend::lang.reorder.default_title'));
$this->validateModel();
$this->prepareVars();
}
//
// AJAX
//
public function onReorder()
{
$model = $this->validateModel();
/*
* Simple
*/
if ($this->sortMode == 'simple') {
if (
(!$ids = post('record_ids')) ||
(!$orders = post('sort_orders'))
) {
return;
}
$model->setSortableOrder($ids, $orders);
}
/*
* Nested set
*/
elseif ($this->sortMode == 'nested') {
$sourceNode = $model->find(post('sourceNode'));
$targetNode = post('targetNode') ? $model->find(post('targetNode')) : null;
if ($sourceNode == $targetNode) {
return;
}
switch (post('position')) {
case 'before':
$sourceNode->moveBefore($targetNode);
break;
case 'after':
$sourceNode->moveAfter($targetNode);
break;
case 'child':
$sourceNode->makeChildOf($targetNode);
break;
default:
$sourceNode->makeRoot();
break;
}
}
}
//
// Reordering
//
/**
* Prepares common form data
*/
protected function prepareVars()
{
$this->vars['reorderRecords'] = $this->getRecords();
$this->vars['reorderModel'] = $this->model;
$this->vars['reorderSortMode'] = $this->sortMode;
$this->vars['reorderShowTree'] = $this->showTree;
$this->vars['reorderToolbarWidget'] = $this->toolbarWidget;
}
public function reorderRender()
{
return $this->reorderMakePartial('container');
}
public function reorderGetModel()
{
if ($this->model !== null) {
return $this->model;
}
$modelClass = $this->getConfig('modelClass');
if (!$modelClass) {
throw new ApplicationException('Please specify the modelClass property for reordering');
}
return $this->model = new $modelClass;
}
/**
* Returns the display name for a record.
* @return string
*/
public function reorderGetRecordName($record)
{
return $record->{$this->nameFrom};
}
/**
* validateModel validates the supplied form model.
*/
protected function validateModel()
{
$model = $this->controller->reorderGetModel();
$modelTraits = class_uses($model);
if (isset($modelTraits[\October\Rain\Database\Traits\Sortable::class])) {
$this->sortMode = 'simple';
}
elseif (isset($modelTraits[\October\Rain\Database\Traits\NestedTree::class])) {
$this->sortMode = 'nested';
$this->showTree = true;
}
else {
throw new ApplicationException('The model must implement the NestedTree or Sortable traits.');
}
return $model;
}
/**
* Returns all the records from the supplied model.
* @return Collection
*/
protected function getRecords()
{
$records = null;
$model = $this->controller->reorderGetModel();
$query = $model->newQuery();
$this->controller->reorderExtendQuery($query);
if ($this->sortMode == 'simple') {
$records = $query
->orderBy($model->getSortOrderColumn())
->get()
;
}
elseif ($this->sortMode == 'nested') {
$records = $query->getNested();
}
return $records;
}
/**
* Extend the query used for finding reorder records. Extra conditions
* can be applied to the query, for example, $query->withTrashed();
* @param October\Rain\Database\Builder $query
* @return void
*/
public function reorderExtendQuery($query)
{
}
//
// Widgets
//
protected function makeToolbarWidget()
{
if ($toolbarConfig = $this->getConfig('toolbar')) {
$toolbarConfig = $this->makeConfig($toolbarConfig);
$toolbarWidget = $this->makeWidget(\Backend\Widgets\Toolbar::class, $toolbarConfig);
}
else {
$toolbarWidget = null;
}
return $toolbarWidget;
}
//
// Helpers
//
/**
* Controller accessor for making partials within this behavior.
* @param string $partial
* @param array $params
* @return string Partial contents
*/
public function reorderMakePartial($partial, $params = [])
{
$contents = $this->controller->makePartial(
'reorder_' . $partial,
$params + $this->vars,
false
);
if (!$contents) {
$contents = $this->makePartial($partial, $params);
}
return $contents;
}
}
================================================
FILE: modules/backend/behaviors/UserPreferencesModel.php
================================================
model->setTable('backend_user_preferences');
}
/**
* Create an instance of the settings model, intended as a static method
*/
public function instance()
{
if (isset(self::$instances[$this->recordCode])) {
return self::$instances[$this->recordCode];
}
$item = $this->getSettingsRecord();
if (!$item) {
$this->model->initSettingsData();
$item = $this->model;
}
return self::$instances[$this->recordCode] = $item;
}
/**
* Checks if the model has been set up previously, intended as a static method
*/
public function isConfigured()
{
return $this->getSettingsRecord() !== null;
}
/**
* Returns the raw Model record that stores the settings.
* @return Model
*/
public function getSettingsRecord()
{
$item = UserPreference::forUser();
$record = $item
->scopeApplyKeyAndUser($this->model, $this->recordCode, $item->userContext)
->remember(1440, $this->getCacheKey())
->first();
return $record ?: null;
}
/**
* Before the model is saved, ensure the record code is set
* and the jsonable field values
*/
public function beforeModelSave()
{
$preferences = UserPreference::forUser();
[$namespace, $group, $item] = $preferences->parseKey($this->recordCode);
$this->model->item = $item;
$this->model->group = $group;
$this->model->namespace = $namespace;
$this->model->user_id = $preferences->userContext->id;
if ($this->fieldValues) {
$this->model->value = $this->fieldValues;
}
}
/**
* Checks if a key is legitimate or should be added to
* the field value collection
*/
protected function isKeyAllowed($key)
{
/*
* Let the core columns through
*/
if ($key == 'namespace' || $key == 'group') {
return true;
}
return parent::isKeyAllowed($key);
}
/**
* Returns a cache key for this record.
*/
protected function getCacheKey()
{
$item = UserPreference::forUser();
$userId = $item->userContext ? $item->userContext->id : 0;
return $this->recordCode.'-userpreference-'.$userId;
}
}
================================================
FILE: modules/backend/behaviors/formcontroller/HasFormDesigns.php
================================================
getConfig(
"{$this->context}[design][displayMode]",
$this->getConfig('design[displayMode]')
) ?: 'basic';
}
/**
* getDesignFormSize returns the page size taken from the form configuration,
* can also specify a custom configuration name, e.g. `sidebarSize`.
*/
protected function getDesignFormSize($name = 'size')
{
$value = $this->getConfig(
"{$this->context}[design][{$name}]",
$this->getConfig("design[{$name}]")
) ?: 'auto';
return Backend::sizeToPixels($value) ?: null;
}
/**
* getDesignBodyClass
*/
protected function getDesignBodyClass()
{
if ($this->getDesignDisplayMode() === 'sidebar') {
return 'compact-container';
}
return null;
}
/**
* isHorizontalForm
*/
protected function isHorizontalForm(): bool
{
if ($this->getConfig("{$this->context}[design][horizontalMode]", $this->getConfig('design[horizontalMode]'))) {
return true;
}
return $this->getDesignDisplayMode() === 'survey';
}
/**
* isSurveyDesign
*/
protected function isSurveyDesign(): bool
{
if ($this->getConfig("{$this->context}[design][surveyMode]", $this->getConfig('design[surveyMode]'))) {
return true;
}
return $this->getDesignDisplayMode() === 'survey';
}
/**
* isPopupDesign
*/
protected function isPopupDesign(): bool
{
return $this->getDesignDisplayMode() === 'popup';
}
/**
* beforeDisplayPopup
*/
protected function beforeDisplayPopup()
{
$updateId = $this->getPopupFormRecordId();
// Emulate the form action
if (post('form_popup_flag')) {
if ($updateId) {
$this->update($updateId);
}
else {
$this->create();
}
return;
}
// Initialize the model for relation AJAX requests inside popup forms
// this is needed since bindToPopups doesn't propagate far enough, so
// this could be removed if that ability was improved to go further.
if ($this->controller->isClassExtendedWith(\Backend\Behaviors\RelationController::class)) {
$this->controller->initRelation($this->controller->formCreateModelObject());
}
}
/**
* hidePopupDesign
*/
protected function hidePopupDesign()
{
$this->extensionHideMethod('index_onPopupLoadForm');
$this->extensionHideMethod('index_onPopupSave');
$this->extensionHideMethod('index_onPopupDelete');
$this->extensionHideMethod('index_onPopupCancel');
}
/**
* index_onPopupLoadForm
*/
public function index_onLoadPopupForm()
{
if (!$this->isPopupDesign()) {
throw new ApplicationException(__("This form is not using a popup design."));
}
if ($id = $this->getPopupFormRecordId()) {
$this->update($id);
$this->vars['popupTitle'] = $this->getLang('update[title]', 'backend::lang.form.update_title');
$this->vars['recordId'] = $id;
}
else {
$this->create();
$this->vars['popupTitle'] = $this->getLang('create[title]', 'backend::lang.form.create_title');
}
$this->vars['popupSize'] = $this->controller->pageSize;
return $this->formRenderDesign();
}
/**
* index_onSave
*/
public function index_onPopupSave()
{
if ($id = $this->getPopupFormRecordId()) {
$this->update_onSave($id);
}
else {
$this->create_onSave();
}
return $this->controller->listRefresh();
}
/**
* index_onPopupCancel
*/
public function index_onPopupCancel()
{
if ($id = $this->getPopupFormRecordId()) {
$this->update_onCancel($id);
}
else {
$this->create_onCancel();
}
}
/**
* index_onPopupDelete
*/
public function index_onPopupDelete()
{
if ($id = $this->getPopupFormRecordId()) {
$this->update_onDelete($id);
}
return $this->controller->listRefresh();
}
/**
* getPopupFormRecordId returns the target identifier for the record,
* contained within the `form_record_id` postback value. The value is
* decoded since HTML attributes are escaped and it may be a string.
*/
protected function getPopupFormRecordId(): string
{
return urldecode((string) post('form_record_id'));
}
}
================================================
FILE: modules/backend/behaviors/formcontroller/HasMultisite.php
================================================
isClassInstanceOf(\October\Contracts\Database\MultisiteInterface::class) &&
$model->isMultisiteEnabled();
}
/**
* makeMultisiteRedirect
*/
public function makeMultisiteRedirect($context = null, $model = null)
{
if (!$model || !$this->controller->formHasMultisite($model)) {
return;
}
$activeSiteId = Site::getSiteIdFromContext();
if ((int) $model->site_id === (int) $activeSiteId) {
return;
}
$otherModel = $model->findOrCreateForSite($activeSiteId);
return $this->makeRedirect($context, $otherModel, ['_site_id' => $activeSiteId]);
}
/**
* onSwitchSite
*/
public function onSwitchSite($recordId = null)
{
$result = [];
$siteId = post('site_id');
$model = $this->controller->formFindModelObject($recordId);
if (!$siteId || !$model) {
return $result;
}
$otherModel = $model->findForSite($siteId);
// Model missing or trashed
$showConfirm = !$otherModel || (
$otherModel->isClassInstanceOf(\October\Contracts\Database\SoftDeleteInterface::class) &&
$otherModel->trashed()
);
if ($showConfirm) {
$result['confirm'] = __('A record does not exist for the selected site. Create one?');
}
return $result;
}
/**
* addHandlerToSiteSwitcher
*/
protected function addHandlerToSiteSwitcher()
{
$siteSwitcher = $this->getWidget('siteSwitcher');
if (!$siteSwitcher) {
return;
}
$siteSwitcher->setSwitchHandler('onSwitchSite');
}
}
================================================
FILE: modules/backend/behaviors/formcontroller/HasMultisiteGroup.php
================================================
isClassInstanceOf(\October\Contracts\Database\MultisiteGroupInterface::class) &&
$model->isMultisiteGroupEnabled();
}
}
================================================
FILE: modules/backend/behaviors/formcontroller/HasOverrides.php
================================================
slug = Str::slug($model->name);
* });
*
*/
}
/**
* formAfterSave is called after the creation or updating form is saved
* @param \Model
*/
public function formAfterSave($model)
{
/**
* @event backend.form.afterSave
* Called after the form model is saved
*
* Example usage:
*
* Event::listen('backend.form.afterSave', function ((\Backend\Classes\Controller) $controller, (\Model) $model) {
* $model->slug = Str::slug($model->name);
* });
*
*/
}
/**
* formBeforeCreate is called before the creation form is saved
* @param \Model
*/
public function formBeforeCreate($model)
{
/**
* @event backend.form.beforeCreate
* Called before the form model is created
*
* Example usage:
*
* Event::listen('backend.form.beforeCreate', function ((\Backend\Classes\Controller) $controller, (\Model) $model) {
* $model->slug = Str::slug($model->name);
* });
*
*/
}
/**
* formAfterCreate is called after the creation form is saved
* @param \Model
*/
public function formAfterCreate($model)
{
/**
* @event backend.form.afterCreate
* Called after the form model is created
*
* Example usage:
*
* Event::listen('backend.form.afterCreate', function ((\Backend\Classes\Controller) $controller, (\Model) $model) {
* $model->slug = Str::slug($model->name);
* });
*
*/
}
/**
* formBeforeUpdate is called before the updating form is saved
* @param \Model
*/
public function formBeforeUpdate($model)
{
/**
* @event backend.form.beforeUpdate
* Called before the form model is updated
*
* Example usage:
*
* Event::listen('backend.form.beforeUpdate', function ((\Backend\Classes\Controller) $controller, (\Model) $model) {
* $model->slug = Str::slug($model->name);
* });
*
*/
}
/**
* formAfterUpdate is called after the updating form is saved
* @param \Model
*/
public function formAfterUpdate($model)
{
/**
* @event backend.form.afterUpdate
* Called after the form model is updated
*
* Example usage:
*
* Event::listen('backend.form.afterUpdate', function ((\Backend\Classes\Controller) $controller, (\Model) $model) {
* $model->slug = Str::slug($model->name);
* });
*
*/
}
/**
* formAfterDelete called after the form model is deleted
* @param \Model
*/
public function formAfterDelete($model)
{
/**
* @event backend.form.afterDelete
* Called after the form model is deleted
*
* Example usage:
*
* Event::listen('backend.form.afterDelete', function ((\Backend\Classes\Controller) $controller, (\Model) $model) {
* // Delete other records
* });
*
*/
}
/**
* formAfterCancel called after the user has cancelled the form
* @param \Model
*/
public function formAfterCancel($model)
{
/**
* @event backend.form.afterCancel
* Called after the form model has deferred binding cancelled
*
* Example usage:
*
* Event::listen('backend.form.afterCancel', function ((\Backend\Classes\Controller) $controller, (\Model) $model) {
* // Delete other records
* });
*
*/
}
/**
* formCreateModelObject creates a new instance of a form model. This logic can
* be changed by overriding it in the controller.
* @return Model
*/
public function formCreateModelObject()
{
return $this->createModel();
}
/**
* formExtendFieldsBefore is called before the form fields are defined
* @param \Backend\Widgets\Form $host The hosting form widget
* @return void
*/
public function formExtendFieldsBefore($host)
{
}
/**
* formExtendFields is called after the form fields are defined
* @param \Backend\Widgets\Form $host The hosting form widget
* @param \October\Rain\Element\ElementHolder|array $fields Array of all defined form field objects (\Backend\Classes\FormField)
* @return void
*/
public function formExtendFields($host, $fields)
{
}
/**
* formExtendRefreshData is called before the form is refreshed, should return an array
* of additional save data.
* @param \Backend\Widgets\Form $host The hosting form widget
* @param array $saveData Current save data
* @return array
*/
public function formExtendRefreshData($host, $saveData)
{
}
/**
* formExtendRefreshFields is called when the form is refreshed, giving the opportunity
* to modify the form fields.
* @param \Backend\Widgets\Form $host The hosting form widget
* @param array $fields Current form fields
* @return array
*/
public function formExtendRefreshFields($host, $fields)
{
}
/**
* formExtendRefreshResults is called after the form is refreshed, should return an
* array of additional result parameters.
* @param \Backend\Widgets\Form $host The hosting form widget
* @param array $result Current result parameters.
* @return array
*/
public function formExtendRefreshResults($host, $result)
{
}
/**
* formExtendModel extends the supplied model used by create and update actions, the model can
* be altered by overriding it in the controller.
* @param \Model $model
* @return Model
*/
public function formExtendModel($model)
{
}
/**
* formExtendQuery extends the query used for finding the form model. Extra conditions
* can be applied to the query, for example, $query->withTrashed();
* @param October\Rain\Database\Builder $query
* @return void
*/
public function formExtendQuery($query)
{
}
}
================================================
FILE: modules/backend/behaviors/formcontroller/HasRenderers.php
================================================
formRenderField('field_name') ?>
*
* @param string $name Field name
* @param array $options (e.g. ['useContainer'=>false])
* @return string HTML markup
*/
public function formRenderField($name, $options = [])
{
return $this->formWidget->renderField($name, $options);
}
/**
* formRefreshField is a view helper to render a field from AJAX based on their field names.
* @param array|string $names
*/
public function formRefreshFields($names): array
{
$result = [];
foreach ((array) $names as $name) {
if (!$fieldObject = $this->formWidget->getField($name)) {
throw new SystemException("Field {$name} was not found in the form definitions.");
}
$result['#' . $fieldObject->getId('group')] = $this->formRenderField($name, ['useContainer' => false]);
}
return $result;
}
/**
* formRenderPreview is a view helper to render the form in preview mode.
*
* = $this->formRenderPreview() ?>
*
* @return string The form HTML markup.
*/
public function formRenderPreview()
{
return $this->formWidget->render(['preview' => true]);
}
/**
* formHasOutsideFields is a view helper to check if a form tab has fields in the
* non-tabbed section (outside fields).
*
* formHasOutsideFields()): ?>
*
*
*
* @return bool
*/
public function formHasOutsideFields()
{
return $this->formWidget->getTab('outside')->hasFields();
}
/**
* formRenderOutsideFields is a view helper to render the form fields belonging to the
* non-tabbed section (outside form fields).
*
* = $this->formRenderOutsideFields() ?>
*
* @return string HTML markup
*/
public function formRenderOutsideFields($options = [])
{
return $this->formWidget->render(['section' => 'outside'] + $options);
}
/**
* formHasPrimaryTabs is a view helper to check if a form tab has fields in the
* primary tab section.
*
* formHasPrimaryTabs()): ?>
*
*
*
* @return bool
*/
public function formHasPrimaryTabs()
{
return $this->formWidget->getTab('primary')->hasFields();
}
/**
* formRenderPrimaryTabs is a view helper to render the form fields belonging to the
* primary tabs section.
*
* = $this->formRenderPrimaryTabs() ?>
*
* @return string HTML markup
*/
public function formRenderPrimaryTabs($options = [])
{
return $this->formWidget->render(['section' => 'primary'] + $options);
}
/**
* formRenderPrimaryTab renders the contents of a primary tab
*/
public function formRenderPrimaryTab($tabName, $options = [])
{
return $this->formWidget->renderTab($tabName, $options);
}
/**
* formHasSecondaryTabs is a view helper to check if a form tab has fields in the
* secondary tab section.
*
* formHasSecondaryTabs()): ?>
*
*
*
* @return bool
*/
public function formHasSecondaryTabs()
{
return $this->formWidget->getTab('secondary')->hasFields();
}
/**
* formRenderSecondaryTabs is a view helper to render the form fields belonging to the
* secondary tabs section.
*
* = $this->formRenderPrimaryTabs() ?>
*
* @return string HTML markup
*/
public function formRenderSecondaryTabs($options = [])
{
return $this->formWidget->render(['section' => 'secondary'] + $options);
}
/**
* formRenderSecondaryTab renders the contents of a secondary tab
*/
public function formRenderSecondaryTab($tabName, $options = [])
{
return $this->formWidget->renderTab($tabName, ['secondaryTab' => true] + $options);
}
}
================================================
FILE: modules/backend/behaviors/formcontroller/partials/_buttons.php
================================================
================================================
FILE: modules/backend/behaviors/importexportcontroller/partials/fields_export.yaml
================================================
# ===================================
# Field Definitions
# ===================================
fields:
step1_section:
label: "1. Export output format"
type: section
file_format:
label: File Format
type: dropdown
options:
json: JSON
csv: CSV
csv_custom: CSV Custom
span: left
format_delimiter:
label: Delimiter Character
span: left
trigger:
action: show
condition: value[csv_custom]
field: file_format
format_enclosure:
label: Enclosure Character
span: auto
trigger:
action: show
condition: value[csv_custom]
field: file_format
format_escape:
label: Escape Character
span: auto
trigger:
action: show
condition: value[csv_custom]
field: file_format
format_encoding:
label: File Encoding
span: auto
type: dropdown
trigger:
action: show
condition: value[csv_custom]
field: file_format
step2_section:
label: 2. Select columns to export
type: section
export_columns:
type: partial
path: ~/modules/backend/behaviors/importexportcontroller/partials/_export_columns.php
span: left
dependsOn: file_format
step3_section:
label: 3. Set export options
type: section
================================================
FILE: modules/backend/behaviors/importexportcontroller/partials/fields_import.yaml
================================================
# ===================================
# Field Definitions
# ===================================
fields:
step1_section:
label: "1. Upload an Import File"
type: section
file_format:
label: File Format
type: dropdown
options:
json: JSON
csv: CSV
csv_custom: CSV Custom
span: right
import_file:
label: Import File
type: fileupload
mode: file
span: left
fileTypes: [csv, json]
useCaption: false
format_delimiter:
label: Delimiter Character
span: left
trigger:
action: show
condition: value[csv_custom]
field: file_format
format_enclosure:
label: Enclosure Character
span: auto
trigger:
action: show
condition: value[csv_custom]
field: file_format
format_escape:
label: Escape Character
span: auto
trigger:
action: show
condition: value[csv_custom]
field: file_format
format_encoding:
label: File Encoding
span: auto
type: dropdown
trigger:
action: show
condition: value[csv_custom]
field: file_format
first_row_titles:
label: First row contains column titles
comment: Leave this checked if the first row in the CSV is used as the column titles.
type: checkbox
span: left
trigger:
action: show
condition: value[csv][csv_custom]
field: file_format
step2_section:
label: "2. Match the File Columns to Database Fields"
type: section
column_matcher:
type: partial
path: ~/modules/backend/behaviors/importexportcontroller/partials/_import_column_matcher.php
dependsOn: [import_file, file_format, first_row_titles, format_delimiter, format_enclosure, format_escape, format_encoding]
step3_section:
label: "3. Set Import Options"
type: section
================================================
FILE: modules/backend/behaviors/listcontroller/HasOverrides.php
================================================
).
* @param Model $record The populated model used for the column
* @param string $definition List definition (optional)
* @return string CSS class name
*/
public function listInjectRowClass($record, $definition = null)
{
}
/**
* listOverrideColumnValue replaces a table column value (
...
)
* @param Model $record The populated model used for the column
* @param string $columnName The column name to override
* @param string $definition List definition (optional)
* @return string HTML view
*/
public function listOverrideColumnValue($record, $columnName, $definition = null)
{
}
/**
* listOverrideHeaderValue replaces the entire table header contents (
...
) with custom HTML
* @param string $columnName The column name to override
* @param string $definition List definition (optional)
* @return string HTML view
*/
public function listOverrideHeaderValue($columnName, $definition = null)
{
}
/**
* listOverrideRecordUrl overrides the record url for the given record
* @param \October\Rain\Database\Model $record
* @param string|null $definition List definition (optional)
* @return string|array|void New url or complex directive
*/
public function listOverrideRecordUrl($record, $definition = null)
{
}
/**
* listAfterReorder is called after the list record structure is reordered
* @param \October\Rain\Database\Model $record
* @param string|null $definition List definition (optional)
*/
public function listAfterReorder($record, $definition = null)
{
}
/**
* listExtendRefreshResults is called when the list is refreshed using AJAX,
* and should return an array of additional partial updates.
* @param Backend\Widgets\List $host
* @param array $result
* @param string $definition List definition (optional)
* @return array
*/
public function listExtendRefreshResults($host, $result, $definition = null)
{
}
}
================================================
FILE: modules/backend/behaviors/listcontroller/partials/_container.php
================================================
= $toolbar->render() ?>
= $filter->render() ?>
= $list->render() ?>
================================================
FILE: modules/backend/behaviors/relationcontroller/HasExtraConfig.php
================================================
bumpSessionKeys) {
$this->relationSessionKey = $this->sessionKey;
$this->sessionKey = str_random(40);
}
$extraConfig = $this->extraConfig;
$extraConfig['chain'][] = $this->field;
$extraConfig['manageIds'][$this->field] = $this->manageId;
$extraConfig['sessionKeys'][$this->field] = [$this->sessionKey, $this->relationSessionKey];
$this->extraConfigChain = $this->extraConfig = $extraConfig;
}
/**
* setExtraConfigForRender comes from the render call method
*/
protected function setExtraConfigForRender($config)
{
$this->extraConfigRender = $config;
$this->setExtraConfig($config);
}
/**
* setExtraConfig usually comes from the postback variable
*/
protected function setExtraConfig($config)
{
if (is_string($config)) {
$config = json_decode($config, true);
}
if (!is_array($config)) {
$config = [];
}
$this->extraConfig = array_merge(
$config,
$this->extraConfigChain,
$this->extraConfigRender
);
}
/**
* applyExtraConfig
*/
protected function applyExtraConfig($field = null)
{
if (!$field) {
$field = $this->field;
}
$config = $this->extraConfig;
$originalConfig = $this->originalConfig->{$field} ?? null;
if (!$config || !$originalConfig) {
return;
}
// readOnlyDefault is used by the relation widget to apply a soft
// default value, i.e. where a value is otherwise unspecified.
// In order of application: 1. default 2. config 3. render
if (
array_key_exists('readOnlyDefault', $config) &&
!array_key_exists('readOnly', $config) &&
!array_key_exists('readOnly', $originalConfig)
) {
$config['readOnly'] = $config['readOnlyDefault'];
}
$parsedConfig = array_only($config, ['readOnly']);
$parsedConfig['view'] = array_only($config, ['recordUrl', 'recordOnClick']);
$this->originalConfig->{$field} = array_replace_recursive(
$originalConfig,
$parsedConfig
);
}
}
================================================
FILE: modules/backend/behaviors/relationcontroller/HasManageMode.php
================================================
manageFormWidget;
}
/**
* relationGetManageListWidget returns the manage list widget used by this behavior
*/
public function relationGetManageListWidget(): ?ListWidget
{
return $this->manageListWidget;
}
/**
* relationGetManageWidget returns the manage widget used by this behavior
* @deprecated use relationGetManageListWidget or relationGetManageFormWidget
* @return \Backend\Classes\WidgetBase
*/
public function relationGetManageWidget()
{
// Multiple (has many, belongs to many)
if ($this->manageMode === 'list' || $this->manageMode === 'pivot') {
return $this->manageListWidget;
}
// Single (belongs to, has one)
if ($this->manageMode === 'form') {
return $this->manageFormWidget;
}
return null;
}
/**
* makeManageListWidget prepares the list widget for management
*/
protected function makeManageListWidget(): ?ListWidget
{
if (!$config = $this->makeConfigForMode('manage', 'list')) {
return null;
}
$this->manageModel = $this->relationModel;
$isPivot = $this->manageMode === 'pivot';
$config->model = $this->manageModel;
$config->alias = $this->alias . 'ManageList';
$config->showSetup = $this->getConfig('manage[showSetup]', false);
$config->showCheckboxes = $this->getConfig('manage[showCheckboxes]', !$isPivot);
$config->showSorting = $this->getConfig('manage[showSorting]', !$isPivot);
$config->defaultSort = $this->getConfig('manage[defaultSort]');
$config->recordsPerPage = $this->getConfig('manage[recordsPerPage]');
$config->customPageName = $this->getConfig('manage[customPageName]', false);
$config->recordOnClick = $this->getConfig('manage[recordOnClick]');
if ($this->viewMode === 'single') {
$config->showCheckboxes = false;
$config->recordOnClick ??= sprintf(
"oc.relationBehavior.clickManageListRecord(this, ':%s', '%s', '%s')",
$this->manageModel->getKeyName(),
$this->relationGetId(),
$this->relationSessionKey
);
}
elseif ($config->showCheckboxes) {
$config->recordOnClick ??= "oc.relationBehavior.toggleListCheckbox(this)";
}
elseif ($isPivot) {
$config->recordOnClick ??= sprintf(
"oc.relationBehavior.clickManagePivotListRecord(this, ':%s', '%s', '%s')",
$this->manageModel->getKeyName(),
$this->relationGetId(),
$this->relationSessionKey
);
}
$widget = $this->makeWidget(ListWidget::class, $config);
// Apply defined constraints
if ($sqlConditions = $this->getConfig('manage[conditions]')) {
$widget->bindEvent('list.extendQueryBefore', function ($query) use ($sqlConditions) {
$query->whereRaw($sqlConditions);
});
}
elseif ($scopeMethod = $this->getConfig('manage[scope]')) {
$widget->bindEvent('list.extendQueryBefore', function ($query) use ($scopeMethod) {
$query->$scopeMethod($this->relationParent);
});
}
else {
$widget->bindEvent('list.extendQueryBefore', function ($query) {
$this->relationObject->addDefinedConstraintsToQuery($query);
// Reset any orders that come from the definition since they may
// reference the pivot table that isn't included in this query
if (in_array($this->relationType, ['belongsToMany', 'morphedByMany', 'morphToMany'])) {
$query->getQuery()->reorder();
}
});
}
// Link the Search Widget to the List Widget
if ($this->searchWidget) {
$this->searchWidget->bindEvent('search.submit', function () use ($widget) {
$widget->setSearchTerm($this->searchWidget->getActiveTerm());
return $widget->onRefresh();
});
// Pass search options
$widget->setSearchOptions([
'mode' => $this->getConfig('manage[searchMode]'),
'scope' => $this->getConfig('manage[searchScope]'),
]);
// Persist the search term across AJAX requests only
if (Request::ajax()) {
$widget->setSearchTerm($this->searchWidget->getActiveTerm());
}
}
// Link the Filter Widget to the List Widget
if ($this->manageFilterWidget) {
$this->manageFilterWidget->bindEvent('filter.update', function () use ($widget) {
return $widget->onFilter();
});
// Apply predefined filter values
$widget->addFilter([$this->manageFilterWidget, 'applyAllScopesToQuery']);
}
// Exclude existing relationships for non-incrementing pivots
if (!$this->isPivotIncrementing()) {
$widget->bindEvent('list.extendQuery', function ($query) {
if (count($existingIds = $this->findExistingRelationIds())) {
$query->whereNotIn($this->manageModel->getQualifiedKeyName(), $existingIds);
}
});
}
return $widget;
}
/**
* makeManageFormWidget prepares the form widget for management
*/
protected function makeManageFormWidget(): ?FormWidget
{
if (!$config = $this->makeConfigForMode('manage', 'form')) {
return null;
}
$this->manageModel = $this->relationModel;
// Existing record
if ($this->manageId) {
$this->manageModel = $this->findManageModelObject($this->manageId);
if (!$this->manageModel) {
throw new ApplicationException(Lang::get('backend::lang.model.not_found', [
'class' => get_class($this->relationModel),
'id' => $this->manageId,
]));
}
}
$config->model = $this->manageModel;
$config->arrayName = class_basename($this->relationModel);
$config->context = $this->evalFormContext('manage', !!$this->manageId);
$config->alias = $this->alias . 'ManageForm';
$config->parentFieldName = $this->field;
$widget = $this->makeWidget(FormWidget::class, $config);
return $widget;
}
/**
* onRelationManageForm
*/
public function onRelationManageForm()
{
// The form should not share its session key with the parent
$this->bumpSessionKeys = true;
$this->beforeAjax();
if ($this->manageMode === 'form' && !$this->manageFormWidget) {
throw new ApplicationException("Missing configuration for [manage.{$this->manageMode}] in RelationController definition [{$this->field}].");
}
if ($this->manageMode !== 'form' && !$this->manageListWidget) {
throw new ApplicationException("Missing configuration for [manage.{$this->manageMode}] in RelationController definition [{$this->field}].");
}
// Updating an existing record
if ($this->manageMode === 'pivot' && ($this->manageId || $this->pivotId)) {
return $this->onRelationManagePivotForm();
}
$this->vars['newSessionKey'] = $this->sessionKey;
return $this->relationMakePartial('manage_' . $this->manageMode);
}
/**
* onRelationManageCreate a new related model
*/
public function onRelationManageCreate()
{
$this->beforeAjax();
$saveData = $this->manageFormWidget->getSaveData();
$sessionKey = $this->deferredBinding ? $this->relationSessionKey : null;
$parentModel = $this->relationObject->getParent();
$newModel = $this->relationModel;
$this->controller->relationBeforeSave($this->field, $newModel);
$this->controller->relationBeforeCreate($this->field, $newModel);
$modelsToSave = $this->prepareModelsToSave($newModel, $saveData);
foreach ($modelsToSave as $modelToSave) {
$modelToSave->save(['sessionKey' => $this->manageFormWidget->getSessionKey(), 'propagate' => true]);
}
// No need to add relationships that have a valid association via HasOneOrMany::make
if (!$this->relationObject instanceof HasOneOrMany || !$parentModel->exists) {
$this->relationObject->add($newModel, $sessionKey);
}
// Belongs To won't save when using add() so it should occur if the conditions are right.
if ($this->relationType === 'belongsTo' && $parentModel->exists && !$this->deferredBinding) {
$parentModel->save();
}
// Display updated form
if ($this->viewMode === 'single') {
$this->viewFormWidget->model = $newModel;
$this->viewFormWidget->setFormValues($saveData);
}
$this->controller->relationAfterSave($this->field, $newModel);
$this->controller->relationAfterCreate($this->field, $newModel);
$this->showFlashMessage('flashCreate');
return $this->relationRefresh();
}
/**
* onRelationManageUpdate an existing related model's fields
*/
public function onRelationManageUpdate()
{
$this->beforeAjax();
$saveData = $this->manageFormWidget->getSaveData();
$this->controller->relationBeforeSave($this->field, $this->manageModel);
$this->controller->relationBeforeUpdate($this->field, $this->manageModel);
$modelsToSave = $this->prepareModelsToSave($this->manageModel, $saveData);
foreach ($modelsToSave as $modelToSave) {
$modelToSave->save(['sessionKey' => $this->manageFormWidget->getSessionKey(), 'propagate' => true]);
}
// Display updated form
if ($this->viewMode === 'single') {
$this->viewFormWidget->setFormValues($saveData);
}
$this->controller->relationAfterSave($this->field, $this->manageModel);
$this->controller->relationAfterUpdate($this->field, $this->manageModel);
$this->showFlashMessage('flashUpdate');
return $this->relationRefresh();
}
/**
* onRelationManageDelete an existing related model completely
*/
public function onRelationManageDelete()
{
$this->beforeAjax();
$deletedModels = [];
// Multiple (has many, belongs to many)
if ($this->viewMode === 'multi') {
if (($checkedIds = post('checked')) && is_array($checkedIds)) {
foreach ($checkedIds as $relationId) {
if ($pivotKey = $this->isPivotIncrementing()) {
$obj = $this->relationObject->wherePivot($pivotKey, $relationId)->first();
}
else {
$obj = $this->findManageModelObject($relationId);
}
if (!$obj) {
continue;
}
$obj->delete();
$deletedModels[] = $obj;
}
}
}
// Single (belongs to, has one)
elseif ($this->viewMode === 'single') {
$relatedModel = $this->viewModel;
if ($relatedModel->exists) {
$relatedModel->delete();
$deletedModels[] = $relatedModel;
}
$this->resetViewWidgetModel();
$this->viewModel = $this->relationModel;
}
$this->controller->relationAfterDelete($this->field, $deletedModels);
$this->showFlashMessage('flashDelete');
return $this->relationRefresh();
}
/**
* onRelationManageAdd an existing related model to the primary model
*/
public function onRelationManageAdd()
{
$this->beforeAjax();
$recordId = post('record_id');
$sessionKey = $this->deferredBinding ? $this->relationSessionKey : null;
// Add
if ($this->viewMode === 'multi') {
$checkedIds = $recordId ? [$recordId] : $this->manageListWidget->getAllCheckedIds();
if (is_array($checkedIds)) {
// Remove existing relations from the array
$existingIds = $this->findExistingRelationIds($checkedIds);
$checkedIds = array_diff($checkedIds, $existingIds);
$foreignKeyName = $this->relationModel->getKeyName();
$models = $this->relationModel->whereIn($foreignKeyName, $checkedIds)->get();
$this->controller->relationBeforeAdd($this->field, $models);
foreach ($models as $model) {
$this->relationObject->add($model, $sessionKey);
}
$this->controller->relationAfterAdd($this->field, $models);
}
$this->showFlashMessage('flashAdd');
}
// Link
elseif ($this->viewMode === 'single') {
if ($recordId && ($model = $this->findManageModelObject($recordId))) {
$this->relationObject->add($model, $sessionKey);
$this->viewFormWidget->setFormValues($model->attributes);
// Belongs To won't save when using add() so it should occur if the conditions are right.
if ($this->relationType === 'belongsTo' && !$this->deferredBinding) {
$parentModel = $this->relationObject->getParent();
if ($parentModel->exists) {
$parentModel->save();
}
}
}
$this->showFlashMessage('flashLink');
}
return $this->relationRefresh();
}
/**
* onRelationManageRemove an existing related model from the primary model
*/
public function onRelationManageRemove()
{
$this->beforeAjax();
$recordId = post('record_id');
$sessionKey = $this->deferredBinding ? $this->relationSessionKey : null;
$relatedModel = $this->relationModel;
// Remove
if ($this->viewMode === 'multi') {
$checkedIds = $recordId ? [$recordId] : post('checked');
if (is_array($checkedIds)) {
if ($pivotKey = $this->isPivotIncrementing()) {
$models = $this->relationObject->wherePivotIn($pivotKey, $checkedIds)->get();
}
else {
$foreignKeyName = $relatedModel->getKeyName();
$models = $relatedModel->whereIn($foreignKeyName, $checkedIds)->get();
}
$this->controller->relationBeforeRemove($this->field, $models);
foreach ($models as $model) {
$this->relationObject->remove($model, $sessionKey);
}
$this->controller->relationAfterRemove($this->field, $models);
}
$this->showFlashMessage('flashRemove');
}
// Unlink
elseif ($this->viewMode === 'single') {
if ($this->relationType === 'belongsTo') {
$this->relationObject->dissociate();
$this->relationObject->getParent()->save();
}
elseif ($this->relationType === 'hasOne' || $this->relationType === 'morphOne') {
if ($obj = $this->findManageModelObject($recordId)) {
$this->relationObject->remove($obj, $sessionKey);
}
elseif ($this->viewModel->exists) {
$this->relationObject->remove($this->viewModel, $sessionKey);
}
}
$this->resetViewWidgetModel();
$this->showFlashMessage('flashUnlink');
}
return $this->relationRefresh();
}
/**
* evalManageTitle determines the management mode popup title
*/
protected function evalManageTitle(): string
{
if ($customTitle = $this->getConfig('manage[title]')) {
return Lang::get($customTitle);
}
switch ($this->manageMode) {
case 'pivot':
case 'list':
if ($this->eventTarget === 'button-link') {
return $this->getCustomLang('titleLinkForm');
}
else {
return $this->getCustomLang('titleAddForm');
}
case 'form':
if ($this->readOnly) {
return $this->getCustomLang('titlePreviewForm');
}
elseif ($this->manageId) {
return $this->getCustomLang('titleUpdateForm');
}
else {
return $this->getCustomLang('titleCreateForm');
}
}
return '';
}
/**
* evalManageMode determines the management mode based on the relation type and settings
* @return string
*/
protected function evalManageMode()
{
switch ($this->eventTarget) {
case 'button-create':
case 'button-update':
return 'form';
case 'button-link':
return 'list';
}
switch ($this->relationType) {
case 'belongsTo':
return 'list';
case 'morphToMany':
case 'morphedByMany':
case 'belongsToMany':
if (isset($this->config->pivot)) {
return 'pivot';
}
elseif ($this->eventTarget === 'list') {
return 'form';
}
else {
return 'list';
}
case 'hasOne':
case 'morphOne':
case 'hasMany':
case 'morphMany':
case 'hasManyThrough':
if ($this->eventTarget === 'button-add') {
return 'list';
}
return 'form';
}
}
/**
* findManageModelObject for the current field
*/
protected function findManageModelObject($recordId)
{
if (!strlen($recordId)) {
return null;
}
$query = $this->relationModel->newQuery();
$this->controller->relationExtendManageFormQuery($this->field, $query);
return $query->find($recordId);
}
}
================================================
FILE: modules/backend/behaviors/relationcontroller/HasNestedRelations.php
================================================
sessionKeys[$field])) {
return $this->sessionKeys[$field];
}
if ($configSessionKey = $this->getConfig('sessionKey')) {
return $this->sessionKeys[$field] = [$configSessionKey, $configSessionKey];
}
$sessionKey = post('_session_key', FormHelper::getSessionKey());
$relationSessionKey = post('_relation_session_key', $sessionKey);
return $this->sessionKeys[$field] = [$sessionKey, $relationSessionKey];
}
/**
* getManageIdForField
*/
protected function getManageIdForField($field)
{
// Field checksum for manage id
if ($field === post('_relation_field') && ($manageId = post('manage_id', -1)) !== -1) {
return $this->manageIds[$field] = $manageId;
}
if (isset($this->manageIds[$field])) {
return $this->manageIds[$field];
}
return null;
}
/**
* initNestedRelation checks the extra configuration for a relationship chain
* and binds the manage forms to the controller, which may contain additional
* relation definitions via the relation form widget.
*/
protected function initNestedRelation($model, $parentField)
{
// Process session keys
$sessionKeys = $this->extraConfig['sessionKeys'] ?? [];
foreach ($sessionKeys as $field => $keys) {
if (is_array($keys)) {
$this->sessionKeys[$field] = $keys;
}
}
// Process manage IDs
$manageIds = $this->extraConfig['manageIds'] ?? [];
foreach ($manageIds as $field => $id) {
$this->manageIds[$field] = $id;
}
// Process nesting chain
$checked = [];
$checked[$parentField] = true;
$chain = $this->extraConfig['chain'] ?? [];
foreach ($chain as $field) {
if (!$field || isset($checked[$field])) {
continue;
}
$this->initRelationInternal($model, $field);
$checked[$field] = true;
}
}
/**
* makeNestedRelationModel resolves a relation based on a nested field name
* E.g: model[relation1][relation2] → $model->relation1()->relation2()
*/
protected function makeNestedRelationModel($model, $field)
{
if (!str_contains($field, '[') || !str_contains($field, ']')) {
return [$model, $field];
}
if ($result = $this->resolveNestedRelationModelFromManageId($model, $field)) {
return $result;
}
if ($result = $this->resolveNestedRelationModelFromModelRelationship($model, $field)) {
return $result;
}
// Fallback with an empty related model
return $this->resolveNestedRelationModelFromDefault($model, $field);
}
/**
* resolveNestedRelationModelFromModelRelationship returns a nested relation model
* locating it using `array_get` to resolve the value
*/
protected function resolveNestedRelationModelFromModelRelationship($model, $field): ?array
{
$parts = HtmlHelper::nameToArray($field);
$lastField = array_pop($parts);
// Custom array_get() function to look for [id:x] segments
$arrayGet = function($model, $parts, $default = null) {
foreach ($parts as $segment) {
$isPrimaryKey = str_starts_with($segment, 'id:');
if ($isPrimaryKey) {
$segment = ltrim($segment, 'id:');
}
if ($isPrimaryKey && $model instanceof \Illuminate\Support\Collection) {
$model = $model->find($segment);
}
else {
$model = array_get($model, $segment);
}
// Prevents an empty collection resolving as true here
if ($model instanceof \Illuminate\Support\Collection && $model->isEmpty()) {
return $default;
}
if (!$model) {
return $default;
}
}
return $model;
};
if ($lookupModel = $arrayGet($model, $parts)) {
return [$lookupModel, $lastField];
}
return null;
}
/**
* resolveNestedRelationModelFromManageId returns a resolved relation with a single hop
*/
protected function resolveNestedRelationModelFromManageId($model, $field): ?array
{
// Looking for a direct hop up, so pop off the end
$parts = HtmlHelper::nameToArray($field);
array_pop($parts);
// Rebuild the field name with the end popped off
$parentField = array_shift($parts);
if ($parts) {
$parentField .= '['.implode('][', $parts).']';
}
// Locate the parent in the manage IDs, populate the model if possible
if (array_key_exists($parentField, $this->manageIds)) {
[$lastModel, $lastField] = $this->resolveNestedRelationModelFromDefault($model, $field);
if (
($parentId = $this->manageIds[$parentField]) &&
($manageModel = $lastModel->find($parentId))
) {
$lastModel = $manageModel;
}
return [$lastModel, $lastField];
}
return null;
}
/**
* resolveNestedRelationModelFromDefault
*/
protected function resolveNestedRelationModelFromDefault($model, $field): array
{
$parts = array_filter(HtmlHelper::nameToArray($field), function($val) {
return !is_numeric(ltrim($val, 'id:'));
});
$lastModel = $model;
$lastField = array_pop($parts);
while ($rootField = array_shift($parts)) {
$lastModel = $lastModel->$rootField()->getRelated();
}
return [$lastModel, $lastField];
}
}
================================================
FILE: modules/backend/behaviors/relationcontroller/HasOverrides.php
================================================
withTrashed();
* @param \October\Rain\Database\Builder $query
* @return void
*/
public function relationExtendManageFormQuery($field, $query)
{
}
/**
* relationExtendViewListWidget provides an opportunity to manipulate the view widget.
* @param \Backend\Widgets\List $widget
* @param string $field
* @param \October\Rain\Database\Model $model
*/
public function relationExtendViewListWidget($widget, $field, $model)
{
}
/**
* relationExtendViewFormWidget provides an opportunity to manipulate the manage widget.
* @param \Backend\Widgets\Form $widget
* @param string $field
* @param \October\Rain\Database\Model $model
*/
public function relationExtendViewFormWidget($widget, $field, $model)
{
}
/**
* relationExtendManageListWidget provides an opportunity to manipulate the view widget.
* @param \Backend\Widgets\List $widget
* @param string $field
* @param \October\Rain\Database\Model $model
*/
public function relationExtendManageListWidget($widget, $field, $model)
{
}
/**
* relationExtendManageFormWidget provides an opportunity to manipulate the manage widget.
* @param \Backend\Widgets\Form $widget
* @param string $field
* @param \October\Rain\Database\Model $model
*/
public function relationExtendManageFormWidget($widget, $field, $model)
{
}
/**
* relationExtendPivotWidget provides an opportunity to manipulate the pivot widget.
* @param \Backend\Widgets\Form $widget
* @param string $field
* @param \October\Rain\Database\Model $model
*/
public function relationExtendPivotFormWidget($widget, $field, $model)
{
}
/**
* relationExtendManageFilterWidget provides an opportunity to manipulate the manage filter widget.
* @param \Backend\Widgets\Filter $widget
* @param string $field
* @param \October\Rain\Database\Model $model
*/
public function relationExtendManageFilterWidget($widget, $field, $model)
{
}
/**
* relationExtendViewFilterWidget provides an opportunity to manipulate the view filter widget.
* @param \Backend\Widgets\Filter $widget
* @param string $field
* @param \October\Rain\Database\Model $model
*/
public function relationExtendViewFilterWidget($widget, $field, $model)
{
}
/**
* relationExtendRefreshResults is needed because the view widget is often
* refreshed when the manage widget makes a change, you can use this method
* to inject additional containers when this process occurs. Return an array
* with the extra values to send to the browser, eg:
*
* return ['#myCounter' => 'Total records: 6'];
*
* @param string $field
* @return array
*/
public function relationExtendRefreshResults($field)
{
}
/**
* @deprecated use relationExtendViewListWidget or relationExtendViewFormWidget
*/
public function relationExtendViewWidget($widget, $field, $model)
{
}
/**
* @deprecated use relationExtendManageListWidget or relationExtendManageFormWidget
*/
public function relationExtendManageWidget($widget, $field, $model)
{
}
/**
* @deprecated use relationExtendPivotFormWidget
*/
public function relationExtendPivotWidget($widget, $field, $model)
{
}
}
================================================
FILE: modules/backend/behaviors/relationcontroller/HasPivotMode.php
================================================
manageMode !== 'pivot') {
return null;
}
if (!$config = $this->makeConfigForMode('pivot', 'form')) {
return null;
}
$config->model = $this->relationModel;
$config->arrayName = class_basename($this->relationModel);
$config->context = $this->evalFormContext('pivot', !!$this->manageId);
$config->alias = $this->alias . 'ManagePivotForm';
$foreignKeyName = $this->relationModel->getQualifiedKeyName();
// Incrementing pivot record
if ($this->pivotId && ($pivotKey = $this->isPivotIncrementing())) {
$this->pivotModel = $this->relationObject->wherePivot($pivotKey, $this->pivotId)->first();
if ($this->pivotModel) {
$config->model = $this->pivotModel;
}
else {
throw new ApplicationException(Lang::get('backend::lang.model.not_found', [
'class' => get_class($this->relationObject->newPivot()),
'id' => $this->pivotId,
]));
}
}
// Existing record
elseif ($this->manageId) {
$this->pivotModel = $this->relationObject->where($foreignKeyName, $this->manageId)->first();
if ($this->pivotModel) {
$config->model = $this->pivotModel;
}
else {
throw new ApplicationException(Lang::get('backend::lang.model.not_found', [
'class' => get_class($config->model),
'id' => $this->manageId,
]));
}
}
// New record
else {
if ($this->foreignId) {
$foreignModel = $this->relationModel
->whereIn($foreignKeyName, (array) $this->foreignId)
->first();
if ($foreignModel) {
$foreignModel->exists = false;
$config->model = $foreignModel;
}
}
$config->model->setRelation('pivot', $this->relationObject->newPivot());
}
return $this->makeWidget(FormWidget::class, $config);
}
/**
* onRelationManageAddPivot adds multiple items using a single pivot form.
*/
public function onRelationManageAddPivot()
{
return $this->onRelationManagePivotForm();
}
/**
* onRelationManagePivotForm
*/
public function onRelationManagePivotForm()
{
$this->beforeAjax();
if (!$this->pivotWidget) {
throw new ApplicationException("Missing configuration for [pivot.form] in RelationController definition [{$this->field}].");
}
$this->vars['foreignId'] = $this->foreignId ?: post('checked');
return $this->relationMakePartial('pivot_form');
}
/**
* onRelationManagePivotCreate
*/
public function onRelationManagePivotCreate()
{
$this->beforeAjax();
// If the pivot model fails for some reason, abort the sync
Db::transaction(function () {
// Add the checked IDs to the pivot table
$foreignIds = (array) $this->foreignId;
$saveData = (array) $this->pivotWidget->getSaveData();
$pivotData = $this->getPivotDataForAttach($saveData);
// Two methods are used to synchronize the records, the first inserts records in
// bulk but may encounter collisions. The fallback adds records one at a time
// and checks for collisions with existing records.
try {
$this->relationObject->attach($foreignIds, $pivotData);
}
catch (Exception $ex) {
$this->relationObject->sync(array_fill_keys($foreignIds, $pivotData), false);
}
// Find newly attached models to save with deferred binding and nesting
$foreignKeyName = $this->relationModel->getQualifiedKeyName();
if ($pivotKey = $this->isPivotIncrementing()) {
// Must guess the models here since there is no way to fetch the last incrementing IDs
$hydratedModels = $this->relationObject
->whereIn($foreignKeyName, $foreignIds)
->limit(count($foreignIds))
->orderBy($this->relationObject->qualifyPivotColumn($pivotKey), 'desc')
->get();
}
else {
$hydratedModels = $this->relationObject->whereIn($foreignKeyName, $foreignIds)->get();
}
// Save data to models
foreach ($hydratedModels as $hydratedModel) {
$modelsToSave = $this->prepareModelsToSave($hydratedModel, $saveData);
foreach ($modelsToSave as $modelToSave) {
$modelToSave->save(['sessionKey' => $this->pivotWidget->getSessionKey()]);
}
}
});
$this->showFlashMessage('flashAdd');
return ['#'.$this->relationGetId('view') => $this->relationRenderView()];
}
/**
* onRelationManagePivotUpdate
*/
public function onRelationManagePivotUpdate()
{
$this->beforeAjax();
// Save data to model
$saveData = $this->pivotWidget->getSaveData();
$modelsToSave = $this->prepareModelsToSave($this->pivotModel, $saveData);
foreach ($modelsToSave as $modelToSave) {
$modelToSave->save(['sessionKey' => $this->pivotWidget->getSessionKey()]);
}
$this->showFlashMessage('flashUpdate');
return ['#'.$this->relationGetId('view') => $this->relationRenderView()];
}
/**
* evalPivotTitle determines the pivot mode popup title
*/
protected function evalPivotTitle(): string
{
if ($customTitle = $this->getConfig('pivot[title]')) {
return $customTitle;
}
return $this->getCustomLang('titlePivotForm');
}
/**
* getPivotDataForAttach returns either a list of IDs to sync, or an associative
* array with sync keys and pivot attributes as values.
*
* This method only exists to send the pivot attributes to the `model.relation.attach`
* event. The attributes are set and saved a second time via the regular life cycle.
* Eloquent should not send it to SQL twice if the attributes are an exact match.
*/
protected function getPivotDataForAttach(array $saveData): array
{
if (!isset($saveData['pivot']) || !is_array($saveData['pivot'])) {
return [];
}
$pivotModel = $this->relationObject->newPivot();
$this->setModelAttributes($pivotModel, $saveData['pivot']);
// Emulate save events for attribute manipulation
$pivotModel->fireEvent('model.beforeSave');
$pivotModel->fireEvent('model.beforeCreate');
$pivotModel->fireEvent('model.beforeSaveDone');
$pivotData = $pivotModel->getAttributes();
if (!$pivotData) {
return [];
}
return $pivotData;
}
/**
* isPivotIncrementing
*/
protected function isPivotIncrementing()
{
if ($this->manageMode !== 'pivot') {
return false;
}
$definition = $this->relationParent->getRelationDefinition($this->relationName);
if (!isset($definition['pivotModel'])) {
return false;
}
if (is_string($pivotKey = $definition['pivotKey'] ?? null)) {
return $pivotKey;
}
return false;
}
}
================================================
FILE: modules/backend/behaviors/relationcontroller/HasViewMode.php
================================================
viewFormWidget;
}
/**
* relationGetViewListWidget returns the manage list widget used by this behavior
*/
public function relationGetViewListWidget(): ?ListWidget
{
return $this->viewListWidget;
}
/**
* relationGetViewWidget returns the view widget used by this behavior
* @deprecated use relationGetViewListWidget or relationGetViewFormWidget
* @return \Backend\Classes\WidgetBase
*/
public function relationGetViewWidget()
{
// Multiple (has many, belongs to many)
if ($this->viewMode === 'multi') {
return $this->viewListWidget;
}
// Single (belongs to, has one)
if ($this->viewMode === 'single') {
return $this->viewFormWidget;
}
return null;
}
/**
* makeViewListWidget prepares the list widget for viewing
*/
protected function makeViewListWidget(): ?ListWidget
{
if ($this->viewMode !== 'multi') {
return null;
}
if (!$config = $this->makeConfigForMode('view', 'list')) {
return null;
}
$this->viewModel = $this->relationModel;
$isPivot = in_array($this->relationType, ['belongsToMany', 'morphedByMany', 'morphToMany']);
$isPivotIncrementing = $this->isPivotIncrementing();
$config->model = $this->viewModel;
$config->alias = $this->alias . 'ViewList';
$config->showSetup = $this->getConfig('view[showSetup]', false);
$config->showSorting = $this->getConfig('view[showSorting]', true);
$config->defaultSort = $this->getConfig('view[defaultSort]');
$config->recordsPerPage = $this->getConfig('view[recordsPerPage]');
$config->showCheckboxes = $this->getConfig('view[showCheckboxes]', !$this->readOnly);
$config->recordUrl = $this->getConfig('view[recordUrl]', null);
$config->customViewPath = $this->getConfig('view[customViewPath]', null);
$config->customPageName = $this->getConfig('view[customPageName]', camel_case(class_basename($this->relationModel).'Page'));
$config->pivotMode = $isPivotIncrementing;
if ($isPivotIncrementing) {
$defaultOnClick = sprintf(
"oc.relationBehavior.clickViewPivotListRecord(this, ':%s', '%s', '%s')",
$isPivotIncrementing,
$this->relationGetId(),
$this->relationSessionKey
);
}
else {
$defaultOnClick = sprintf(
"oc.relationBehavior.clickViewListRecord(this, ':%s', '%s', '%s')",
$this->viewModel->getKeyName(),
$this->relationGetId(),
$this->relationSessionKey
);
}
if ($config->recordUrl) {
$defaultOnClick = null;
}
elseif (
!$this->makeConfigForMode('manage', 'form') &&
!$this->makeConfigForMode('pivot', 'form')
) {
$defaultOnClick = null;
}
$config->recordOnClick = $this->getConfig('view[recordOnClick]', $defaultOnClick);
if ($emptyMessage = $this->getConfig('emptyMessage')) {
$config->noRecordsMessage = $emptyMessage;
}
if ($isPivot) {
$this->viewModel->setRelation('pivot', $this->relationObject->newPivot());
}
// Make structure enabled widget
$structureConfig = $this->makeListStructureConfig($config);
if ($structureConfig) {
$widget = $this->makeWidget(ListStructureWidget::class, $structureConfig);
}
else {
$widget = $this->makeWidget(ListWidget::class, $config);
}
// Linkage for JS plugins
if ($this->toolbarWidget) {
$this->toolbarWidget->listWidgetId = $widget->getId();
// Pass the list setup AJAX handler to the toolbar
if ($config->showSetup) {
$this->toolbarWidget->setupHandler = $widget->getEventHandler('onLoadSetup');
}
}
// Custom structure reordering logic
if (
$this->relationParent->isClassInstanceOf(\October\Contracts\Database\SortableRelationInterface::class) &&
$this->relationParent->isSortableRelation($this->relationName)
) {
$widget->bindEvent('list.beforeReorderStructure', function () {
// Set sort orders in deferred bindings as well
if ($this->deferredBinding) {
$this->relationParent->sessionKey = $this->relationSessionKey;
}
$this->relationParent->setSortableRelationOrder($this->relationName, post('sort_orders'), true);
return false;
}, -1);
}
// Apply defined constraints
if ($sqlConditions = $this->getConfig('view[conditions]')) {
$widget->bindEvent('list.extendQueryBefore', function ($query) use ($sqlConditions) {
$query->whereRaw($sqlConditions);
});
}
elseif ($scopeMethod = $this->getConfig('view[scope]')) {
$widget->bindEvent('list.extendQueryBefore', function ($query) use ($scopeMethod) {
$query->$scopeMethod($this->relationParent);
});
}
else {
$widget->bindEvent('list.extendQueryBefore', function ($query) {
$this->relationObject->addDefinedConstraintsToQuery($query);
});
}
// Constrain the query by the relationship and deferred items
$widget->bindEvent('list.extendQuery', function (&$query) use ($isPivot) {
$this->relationObject->setQuery($query);
$sessionKey = $this->deferredBinding ? $this->relationSessionKey : null;
if ($sessionKey) {
$this->relationObject->withDeferredQuery(null, $sessionKey);
}
elseif ($this->relationParent->exists) {
$this->relationObject->addConstraints();
}
// Allows pivot data to enter the fray by replacing the query
if ($isPivot) {
$this->relationObject->setQuery($query->getQuery());
$query = $this->relationObject;
}
});
// Constrain the list by the search widget, if available
if (
$this->toolbarWidget &&
$this->getConfig('view[showSearch]') &&
$searchWidget = $this->toolbarWidget->getSearchWidget()
) {
$searchWidget->bindEvent('search.submit', function () use ($widget, $searchWidget) {
$widget->setSearchTerm($searchWidget->getActiveTerm());
return $widget->onRefresh();
});
// Pass search options
$widget->setSearchOptions([
'mode' => $this->getConfig('view[searchMode]'),
'scope' => $this->getConfig('view[searchScope]'),
]);
// Persist the search term across AJAX requests only
if (Request::ajax()) {
$widget->setSearchTerm($searchWidget->getActiveTerm());
}
else {
$searchWidget->setActiveTerm(null);
}
}
// Link the Filter Widget to the List Widget
if ($this->viewFilterWidget) {
$this->viewFilterWidget->bindEvent('filter.update', function () use ($widget) {
return $widget->onFilter();
});
// Apply predefined filter values
$widget->addFilter([$this->viewFilterWidget, 'applyAllScopesToQuery']);
}
return $widget;
}
/**
* makeViewFormWidget prepares the form widget for viewing
*/
protected function makeViewFormWidget(): ?FormWidget
{
if ($this->viewMode !== 'single') {
return null;
}
if (!$config = $this->makeConfigForMode('view', 'form')) {
return null;
}
$this->viewModel = $this->relationObject->getResults()
?: $this->relationModel;
$config->model = $this->viewModel;
$config->arrayName = class_basename($this->relationModel);
$config->context = 'relation';
$config->alias = $this->alias . 'ViewForm';
$widget = $this->makeWidget(FormWidget::class, $config);
$widget->previewMode = true;
return $widget;
}
/**
* makeListStructureConfig
*/
protected function makeListStructureConfig(object $config): ?object
{
$structureConfig = $this->makeConfigForMode('view', 'structure');
if (!$structureConfig) {
return null;
}
if (
$this->relationParent->isClassInstanceOf(\October\Contracts\Database\SortableRelationInterface::class) &&
$this->relationParent->isSortableRelation($this->relationName)
) {
$structureConfig->includeSortOrders = true;
}
return $this->mergeConfig($config, $structureConfig);
}
//
// AJAX (Buttons)
//
/**
* onRelationButtonAdd
*/
public function onRelationButtonAdd()
{
return $this->onRelationManageForm();
}
/**
* onRelationButtonCreate
*/
public function onRelationButtonCreate()
{
return $this->onRelationManageForm();
}
/**
* onRelationButtonDelete
*/
public function onRelationButtonDelete()
{
return $this->onRelationManageDelete();
}
/**
* onRelationButtonLink
*/
public function onRelationButtonLink()
{
return $this->onRelationManageForm();
}
/**
* onRelationButtonUnlink
*/
public function onRelationButtonUnlink()
{
return $this->onRelationManageRemove();
}
/**
* onRelationButtonRemove
*/
public function onRelationButtonRemove()
{
return $this->onRelationManageRemove();
}
/**
* onRelationButtonUpdate
*/
public function onRelationButtonUpdate()
{
return $this->onRelationManageForm();
}
//
// AJAX (List events)
//
/**
* onRelationClickManageList
*/
public function onRelationClickManageList()
{
return $this->onRelationManageAdd();
}
/**
* onRelationClickManageListPivot
*/
public function onRelationClickManageListPivot()
{
return $this->onRelationManagePivotForm();
}
/**
* onRelationClickViewList
*/
public function onRelationClickViewList()
{
return $this->onRelationManageForm();
}
/**
* onRelationClickViewListPivot
*/
public function onRelationClickViewListPivot()
{
return $this->onRelationManageForm();
}
/**
* evalEventTarget determines the source of an AJAX event used for determining
* the manage mode state. See the `evalManageMode` method.
* @return string
*/
protected function evalEventTarget()
{
switch ($this->controller->getAjaxHandler()) {
case 'onRelationButtonAdd':
return 'button-add';
case 'onRelationButtonCreate':
return 'button-create';
case 'onRelationButtonLink':
return 'button-link';
case 'onRelationButtonUpdate':
return 'button-update';
case 'onRelationClickViewList':
return 'list';
default:
return '';
}
}
/**
* evalViewMode determines the view mode based on the model relationship type
* @return string
*/
protected function evalViewMode()
{
if ($viewMode = $this->getConfig('manage[viewMode]')) {
return $viewMode;
}
switch ($this->relationType) {
case 'hasMany':
case 'morphMany':
case 'morphToMany':
case 'morphedByMany':
case 'belongsToMany':
case 'hasManyThrough':
return 'multi';
case 'hasOne':
case 'morphOne':
case 'belongsTo':
return 'single';
default:
return '';
}
}
/**
* resetViewWidgetModel is an internal method used when deleting singular relationships
*/
protected function resetViewWidgetModel()
{
$this->viewFormWidget->model = $this->relationModel;
$this->viewFormWidget->setFormValues([]);
}
}
================================================
FILE: modules/backend/behaviors/relationcontroller/assets/css/relation.css
================================================
.relation-behavior.relation-view-multi,.relation-behavior.relation-view-single{background:var(--oc-form-control-bg);border:1px solid var(--bs-border-color);border-radius:4px;margin-bottom:20px;overflow:hidden}.relation-behavior.relation-view-multi .control-filter,.relation-behavior.relation-view-single .control-filter{border-top:none}.relation-behavior.relation-view-multi .relation-toolbar .control-toolbar,.relation-behavior.relation-view-single .relation-toolbar .control-toolbar{border-bottom:1px solid var(--oc-border-color);padding:0}.relation-behavior.relation-view-multi .relation-toolbar .control-toolbar .loading-indicator-container.size-input-text .loading-indicator>span,.relation-behavior.relation-view-single .relation-toolbar .control-toolbar .loading-indicator-container.size-input-text .loading-indicator>span{top:7px}.relation-behavior.relation-view-multi .relation-toolbar .control-toolbar .toolbar-item.toolbar-primary,.relation-behavior.relation-view-single .relation-toolbar .control-toolbar .toolbar-item.toolbar-primary{padding:3px}.relation-behavior.relation-view-multi .relation-toolbar .control-toolbar .search-input-container:before,.relation-behavior.relation-view-single .relation-toolbar .control-toolbar .search-input-container:before{right:10px;top:11px}.relation-behavior.relation-view-multi .relation-toolbar .control-toolbar .search-input-container .form-control,.relation-behavior.relation-view-single .relation-toolbar .control-toolbar .search-input-container .form-control{border-bottom:none;border-radius:0;border-right:none;border-top:none;padding-bottom:8px;padding-top:8px}.relation-behavior.relation-view-multi .relation-toolbar .control-toolbar .toolbar-item.toolbar-setup,.relation-behavior.relation-view-single .relation-toolbar .control-toolbar .toolbar-item.toolbar-setup{border-left:1px solid var(--oc-border-color);width:35px}.relation-behavior.relation-view-multi .relation-toolbar .control-toolbar .toolbar-item.toolbar-setup a>span,.relation-behavior.relation-view-single .relation-toolbar .control-toolbar .toolbar-item.toolbar-setup a>span{margin-left:5px}.relation-behavior.relation-view-multi .control-list,.relation-behavior.relation-view-single .control-list{border:none;margin-bottom:0}.relation-behavior.relation-view-single{background:transparent}.relation-behavior.relation-view-single .relation-toolbar .control-toolbar{background:var(--oc-form-control-bg);border-bottom:1px solid var(--oc-toolbar-border)}.relation-behavior.relation-view-single>.relation-manager{padding:20px 20px 0}.widget-field.span-adaptive>.relation-widget .relation-behavior{border-left:none;border-radius:0;border-right:none;border-top:none}.widget-field.span-adaptive>.relation-widget .relation-behavior .relation-toolbar{display:none}.widget-field>.relation-widget .relation-behavior{margin-bottom:0}.relation-behavior{margin-bottom:20px}.relation-behavior .control-list{border:1px solid #eee}.relation-behavior .control-list thead>tr>th{border-top:none!important;border-color:#eee}.relation-behavior .control-list table.table.data{border-bottom:none}.relation-behavior .control-list .list-content+.list-footer{border-top:1px solid var(--bs-border-color)}.relation-behavior .control-list .list-footer .pagination{--bs-pagination-bg:var(--oc-form-control-bg)}.relation-behavior .control-toolbar{padding:0 20px 20px}.relation-behavior .control-toolbar .toolbar-item.toolbar-primary:before{left:0}.relation-behavior .control-toolbar .toolbar-item.toolbar-primary:after{right:0}.relation-behavior .control-toolbar .toolbar-item .form-control.search{padding-bottom:5px;padding-top:5px}.relation-behavior .control-toolbar .loading-indicator-container.size-input-text{min-height:0}.relation-behavior .control-toolbar .loading-indicator-container.size-input-text .loading-indicator>span{top:4px}.relation-behavior .list-header{padding:0!important}.relation-behavior .control-list:last-child>table{margin-bottom:0}.relation-flush .control-list{border-top:none}.relation-inset{margin-left:-20px;margin-right:-20px}.form-group>.relation-behavior .control-toolbar{padding:0 0 10px}
================================================
FILE: modules/backend/behaviors/relationcontroller/assets/js/october.relation.js
================================================
/*
* Scripts for the Relation controller behavior.
*/
+function ($) { "use strict";
class RelationBehavior extends oc.ControlBase
{
static instanceCounter = 0;
init() {
this.instanceNumber = ++this.constructor.instanceCounter;
}
connect() {
this.listen('trigger:after-update', this.extendExternalToolbar);
// External toolbar
oc.pageReady().then(() => {
this.initToolbarExtensionPoint();
this.mountExternalToolbarEventBusEvents();
this.extendExternalToolbar();
});
}
disconnect() {
}
initToolbarExtensionPoint() {
if (!this.config.externalToolbarAppState) {
return;
}
const point = $.oc.vueUtils.getToolbarExtensionPoint(
this.config.externalToolbarAppState,
this.element
);
if (point) {
this.toolbarExtensionPoint = point.state;
this.externalToolbarEventBusObj = point.bus;
}
}
mountExternalToolbarEventBusEvents() {
if (!this.externalToolbarEventBusObj) {
return;
}
this.externalToolbarEventBusObj.on('toolbarcmd', this.proxy(this.onToolbarExternalCommand));
this.externalToolbarEventBusObj.on('extendapptoolbar', this.proxy(this.extendExternalToolbar));
}
unmountExternalToolbarEventBusEvents() {
if (!this.externalToolbarEventBusObj) {
return;
}
this.externalToolbarEventBusObj.off('toolbarcmd', this.proxy(this.onToolbarExternalCommand));
this.externalToolbarEventBusObj.off('extendapptoolbar', this.proxy(this.extendExternalToolbar));
}
onToolbarExternalCommand(ev) {
var $el = $(this.element);
var cmdPrefix = 'relationcontroller-toolbar-' + this.instanceNumber + '-';
if (ev.command.substring(0, cmdPrefix.length) != cmdPrefix) {
return;
}
var buttonClassName = ev.command.substring(cmdPrefix.length),
$toolbar = $el.find('.relation-toolbar .control-toolbar'),
$button = $toolbar.find('[class="'+buttonClassName+'"]');
$button.get(0).click(ev.ev);
}
extendExternalToolbar() {
var $el = $(this.element);
if (!$el.is(":visible") || !this.toolbarExtensionPoint) {
return;
}
this.toolbarExtensionPoint.splice(0, this.toolbarExtensionPoint.length);
this.toolbarExtensionPoint.push({
type: 'separator'
});
var $buttons = $el.find('.relation-toolbar .control-toolbar .btn');
$buttons.each((index, button) => {
var $button = $(button),
$icon = $button.find('i[class^=icon]');
this.toolbarExtensionPoint.push(
{
type: 'button',
icon: $icon.attr('class'),
label: $button.text(),
command: 'relationcontroller-toolbar-' + this.instanceNumber + '-' + $button.attr('class'),
disabled: $button.attr('disabled') !== undefined
}
);
});
}
static toggleListCheckbox(el) {
$(el).closest('.control-list').listWidget('toggleChecked', [el]);
}
static clickViewListRecord(target, recordId, relationId, sessionKey) {
var newPopup = $(target),
$container = $('#'+relationId),
requestData = paramToObj('data-request-data', $container.data('request-data'));
newPopup.popup({
handler: 'onRelationClickViewList',
extraData: $.extend({}, requestData, {
'manage_id': recordId,
'_relation_session_key': sessionKey
})
});
}
static clickViewPivotListRecord(target, recordId, relationId, sessionKey) {
var newPopup = $(target),
$container = $('#'+relationId),
requestData = paramToObj('data-request-data', $container.data('request-data'));
newPopup.popup({
handler: 'onRelationClickViewListPivot',
extraData: $.extend({}, requestData, {
'pivot_id': recordId,
'_relation_session_key': sessionKey
})
});
}
static clickManageListRecord(target, recordId, relationId, sessionKey) {
var oldPopup = $('#relationManagePopup'),
$container = $('#'+relationId),
requestData = paramToObj('data-request-data', $container.data('request-data'));
$(target).request('onRelationClickManageList', {
data: $.extend({}, requestData, {
'record_id': recordId,
'_relation_session_key': sessionKey
})
})
.done(() => {
if (requestData['_relation_field']) {
this.changed(requestData['_relation_field'], 'added');
}
});
oldPopup.popup('hide');
}
static clickManagePivotListRecord(target, foreignId, relationId, sessionKey) {
var oldPopup = $('#relationManagePivotPopup'),
newPopup = $(target),
$container = $('#'+relationId),
requestData = paramToObj('data-request-data', $container.data('request-data'));
if (oldPopup.length) {
oldPopup.popup('hide');
}
newPopup.popup({
handler: 'onRelationClickManageListPivot',
extraData: $.extend({}, requestData, {
'foreign_id': foreignId,
'_relation_session_key': sessionKey
})
});
}
// This function is called every time a record is created, added, removed
// or deleted using the relation widget. It triggers the change.oc.formwidget
// event to notify other elements on the page about the changed form state.
static changed(relationId, event) {
$('[data-field-name="' + relationId + '"]').trigger('change.oc.formwidget', { event: event });
}
// @deprecated use oc.popup.bindToPopups
static bindToPopups(container, vars) {
return oc.popup.bindToPopups(container, vars);
}
}
function paramToObj(name, value) {
if (value === undefined) value = ''
if (typeof value == 'object') return value
try {
return oc.parseJSON("{" + value + "}")
}
catch (e) {
throw new Error('Error parsing the '+name+' attribute value. '+e)
}
}
oc.registerControl('relation-controller', RelationBehavior);
oc.relationBehavior = RelationBehavior;
// @deprecated
$.oc.relationBehavior = RelationBehavior;
}(window.jQuery);
================================================
FILE: modules/backend/behaviors/relationcontroller/assets/less/relation.field.less
================================================
.widget-field.span-adaptive > .relation-widget {
.relation-behavior {
border-top: none;
border-left: none;
border-right: none;
border-radius: 0;
.relation-toolbar {
display: none;
}
}
}
.widget-field > .relation-widget {
.relation-behavior {
margin-bottom: 0;
}
}
================================================
FILE: modules/backend/behaviors/relationcontroller/assets/less/relation.less
================================================
@import "../../../../assets/less/core/boot.less";
@color-relation-border: #eeeeee;
@import "relation.ui.less";
@import "relation.field.less";
.relation-behavior {
margin-bottom: 20px;
.control-list {
border: 1px solid @color-relation-border;
thead > tr > th {
border-top: none !important;
border-color: @color-relation-border;
}
table.table.data {
border-bottom: none;
}
.list-content + .list-footer {
border-top: 1px solid @input-border;
}
.list-footer .pagination {
--bs-pagination-bg: @input-bg;
}
}
.control-toolbar {
padding: 0 20px 20px 20px;
.toolbar-item.toolbar-primary {
&:before { left: 0; }
&:after { right: 0; }
}
.toolbar-item .form-control.search {
padding-top: 5px;
padding-bottom: 5px;
}
.loading-indicator-container.size-input-text {
min-height: 0;
.loading-indicator > span {
top: 4px;
}
}
}
.list-header {
padding: 0 !important;
}
.control-list:last-child > table {
margin-bottom: 0;
}
}
// Relation manager to sit flush to the element above
.relation-flush {
.control-list {
border-top: none;
}
}
// Relation manager to sit inset the standard padding (20px)
.relation-inset {
margin-left: -20px;
margin-right: -20px;
}
// Displayed in a form field
.form-group > .relation-behavior {
.control-toolbar {
padding: 0 0 10px 0;
}
}
================================================
FILE: modules/backend/behaviors/relationcontroller/assets/less/relation.ui.less
================================================
.relation-behavior {
&.relation-view-multi, &.relation-view-single {
border: 1px solid @input-border;
border-radius: @input-border-radius;
// border-top: 1px solid @input-border;
// border-bottom: 1px solid @input-border;
margin-bottom: 20px;
background: @input-bg;
overflow: hidden;
.relation-manager {
// border-top: 1px solid var(--oc-border-color);
}
.control-filter {
border-top: none;
}
.relation-toolbar .control-toolbar {
padding: 0;
border-bottom: 1px solid var(--oc-border-color);
.loading-indicator-container.size-input-text .loading-indicator > span {
top: 7px;
}
.toolbar-item.toolbar-primary {
padding: 3px;
}
.search-input-container {
&:before {
right: 10px;
top: 11px;
}
.form-control {
border-radius: 0;
border-top: none;
border-bottom: none;
border-right: none;
padding-top: 8px;
padding-bottom: 8px;
}
}
.toolbar-item.toolbar-setup {
border-left: 1px solid var(--oc-border-color);
width: 35px;
a > span {
margin-left: 5px;
}
}
}
.control-list {
margin-bottom: 0;
border: none;
}
}
&.relation-view-single {
background: transparent;
.relation-toolbar .control-toolbar {
background: @input-bg;
border-bottom: 1px solid @toolbar-border;
}
> .relation-manager {
padding: 20px 20px 0 20px;
}
}
}
================================================
FILE: modules/backend/behaviors/relationcontroller/partials/_button_add.php
================================================
= e($this->relationGetMessage('buttonAdd')) ?>
================================================
FILE: modules/backend/behaviors/relationcontroller/partials/_button_create.php
================================================
= e($this->relationGetMessage('buttonCreate')) ?>
================================================
FILE: modules/backend/behaviors/relationcontroller/partials/_button_delete.php
================================================
================================================
FILE: modules/backend/behaviors/relationcontroller/partials/_button_link.php
================================================
= e($this->relationGetMessage('buttonLink')) ?>
================================================
FILE: modules/backend/behaviors/relationcontroller/partials/_button_remove.php
================================================
================================================
FILE: modules/backend/behaviors/relationcontroller/partials/_button_unlink.php
================================================
================================================
FILE: modules/backend/behaviors/relationcontroller/partials/_button_update.php
================================================
= e($this->relationGetMessage('buttonUpdate')) ?>
================================================
FILE: modules/backend/behaviors/relationcontroller/partials/_container.php
================================================
================================================
FILE: modules/backend/classes/AuthManager.php
================================================
getUser()) {
return $user->hasAccess($permissions, $all);
}
return false;
}
/**
* userHasAccess is identical to User::hasPermission
*/
public function userHasPermission($permissions, $all = true)
{
if ($user = $this->getUser()) {
return $user->hasPermission($permissions, $all);
}
return false;
}
/**
* {@inheritdoc}
*/
protected function createUserModelQuery()
{
return parent::createUserModelQuery()->withTrashed();
}
/**
* {@inheritdoc}
*/
protected function validateUserModel($user)
{
if (!$user instanceof $this->userModel) {
return false;
}
if ($user->deleted_at !== null) {
return false;
}
return $user;
}
}
================================================
FILE: modules/backend/classes/BackendController.php
================================================
extendableConstruct();
}
/**
* extend this object properties upon construction
*/
public static function extend(Closure $callback)
{
self::extendableExtendCallback($callback);
}
/**
* run finds and serves the requested backend controller
* If the controller cannot be found, returns the Cms page with the URL /404.
* If the /404 page doesn't exist, returns the system 404 page.
* @param string $url Specifies the requested page URL.
* If the parameter is omitted, the current URL used.
* @return string Returns the processed page content.
*/
public function run($url = null)
{
$params = RouterHelper::segmentizeUrl($url);
// Database check
if (!App::hasDatabase()) {
return System::checkDebugMode()
? Response::make(View::make('backend::no_database'), 200)
: $this->runPageNotFound();
}
// Locate edit site
$this->findEditSite();
// Look for App or Module controller
$module = $params[0] ?? 'backend';
$controller = $params[1] ?? 'index';
$isApp = strtolower($module) === 'app';
$controllerClass = "{$module}\\Controllers\\{$controller}";
$controllerBase = $isApp ? base_path() : base_path('modules');
// Store the current context
self::$action = $params[2] ?? 'index';
self::$params = array_slice($params, 3);
if ($controllerObj = $this->findController(
$controllerClass,
self::$action,
$controllerBase
)) {
if (!$isApp && !System::hasModule(ucfirst($module))) {
return Response::make(View::make('backend::404'), 404);
}
return $controllerObj->run(self::$action, self::$params);
}
// Look for Plugin controller using hint segment
$hint = $params[0] ?? null;
$namespace = PluginManager::instance()->getPluginHints()[$hint] ?? null;
if ($namespace && str_contains($namespace, '.')) {
[$author, $plugin] = explode('.', strtolower($namespace));
$controller = $params[1] ?? 'index';
$controllerClass = "{$author}\\{$plugin}\Controllers\\{$controller}";
// Store the current context
self::$action = $params[2] ?? 'index';
self::$params = array_slice($params, 3);
if ($controllerObj = $this->findController(
$controllerClass,
self::$action,
plugins_path()
)) {
return $controllerObj->run(self::$action, self::$params);
}
}
// Look for a Plugin controller
if (count($params) >= 2) {
[$author, $plugin] = $params;
$controller = $params[2] ?? 'index';
$controllerClass = "{$author}\\{$plugin}\Controllers\\{$controller}";
// Store the current context
self::$action = $params[3] ?? 'index';
self::$params = array_slice($params, 4);
if ($controllerObj = $this->findController(
$controllerClass,
self::$action,
plugins_path()
)) {
if (PluginManager::instance()->isDisabled(ucfirst($author).'.'.ucfirst($plugin))) {
return Response::make(View::make('backend::404'), 404);
}
return $controllerObj->run(self::$action, self::$params);
}
}
// Fall back to CMS controller
return $this->runPageNotFound();
}
/**
* findEditSite locates the edit site based on the current request
*/
protected function findEditSite()
{
if (!Site::hasAnyEditSite()) {
return;
}
if ($id = get('_site_id')) {
Site::applyEditSiteId($id);
}
elseif ($id = Request::header('X_SITE_ID')) {
Site::applyEditSiteId($id);
}
elseif ($site = Site::getEditSiteFromRequest()) {
Site::applyEditSite($site);
}
}
/**
* runPageNotFound display a CMS 404 page, if one is available. For security reasons,
* the backend 404 page is not used unless an admin session is found, this prevents
* random discovery of the admin panel URL by crawlers or bots.
*/
protected function runPageNotFound()
{
if (System::hasModule('Cms') && !\BackendAuth::getUser()) {
return \Cms::pageNotFound();
}
return Response::make(View::make('backend::404'), 404);
}
/**
* findController is used internally to find a backend controller with a
* callable action method
* @param string $controller Specifies a method name to execute.
* @param string $action Specifies a method name to execute.
* @param string $inPath Base path for class file location.
* @return ControllerBase|false Returns the backend controller object
*/
protected function findController($controller, $action, $inPath)
{
// Workaround: Composer does not support case insensitivity.
if (!class_exists($controller)) {
$controllerFile = $inPath.'/'.strtolower(str_replace('\\', '/', $controller)) . '.php';
if (
strpos($controllerFile, '..') !== false ||
strpos($controllerFile, './') !== false ||
strpos($controllerFile, '//') !== false
) {
return false;
}
if ($controllerFile = File::existsInsensitive($controllerFile)) {
include_once $controllerFile;
}
}
if (!class_exists($controller)) {
return false;
}
$controllerObj = App::make($controller);
// Parse the action now that the controller class is loaded
self::$action = $this->parseAction($controllerObj, $action);
if ($controllerObj->actionExists(self::$action)) {
return $controllerObj;
}
return false;
}
/**
* parseAction processes the action name, since dashes are not supported in PHP methods.
* WildcardControllers keep the original action name since it is used as a parameter.
*/
protected function parseAction($controller, string $actionName): string
{
if ($controller instanceof WildcardController) {
return $actionName;
}
if (strpos($actionName, '-') === false) {
return $actionName;
}
return snake_case(camel_case($actionName));
}
}
================================================
FILE: modules/backend/classes/Controller.php
================================================
'reload']
*/
public $turboRouter;
/**
* @deprecated use $turboRouter instead
*/
public $turboVisitControl;
/**
* @var array hiddenActions are methods that cannot be called as actions.
*/
public $hiddenActions = [
'run'
];
/**
* @var array guarded methods that cannot be called as actions.
*/
protected $guarded = [];
/**
* __construct the controller.
*/
public function __construct()
{
if (!is_array($this->implement)) {
$this->implement = [];
}
// Establish component container
$this->widget = $this->componentContainer = new \Larajax\Classes\ComponentContainer($this);
// Allow early access to route data.
$this->action = BackendController::$action;
$this->params = BackendController::$params;
// Apply $guarded methods to hidden actions
$this->hiddenActions = array_merge($this->hiddenActions, $this->guarded);
// Define layout and view paths
$this->layout = $this->layout ?: 'default';
$this->layoutPath = Skin::getActive()->getLayoutPaths();
$this->viewPath = $this->configPath = $this->guessViewPath();
// Add layout paths from the plugin / module context
$relativePath = dirname(dirname(strtolower(str_replace('\\', '/', get_called_class()))));
$this->layoutPath[] = "~/modules/{$relativePath}/layouts";
$this->layoutPath[] = "~/plugins/{$relativePath}/layouts";
// Enable turbo router
if (Config::get('backend.turbo_router', false)) {
$this->turboRouter ??= true;
}
// Create a new instance of the admin user
$this->user = BackendAuth::getUser();
// Site switcher
if ($this->user && Site::hasAnyEditSite()) {
(new \Backend\Widgets\SiteSwitcher($this))->bindToController();
}
// Impersonate backend role
if ($this->user && BackendAuth::isRoleImpersonator()) {
(new \Backend\Widgets\RoleImpersonator($this))->bindToController();
}
// Boot behavior constructors
parent::__construct();
}
/**
* beforeDisplay is a method to override in your controller as a way to execute logic before
* each action executes. It is preferred over placing logic in the constructor
*/
public function beforeDisplay()
{
}
/**
* run executes the controller action
* @param string $action The action name.
* @param array $params Routing parameters to pass to the action.
* @return mixed The action result.
*/
public function run($action = null, $params = [])
{
$this->action = $action;
$this->params = $params;
// Check security token.
// @see \System\Traits\SecurityController
if (!$this->verifyCsrfToken()) {
return Request::ajax()
? ajax()->error(Lang::get('system::lang.page.invalid_token.label'), 403)->reload()
: Backend::redirectGuest('backend/auth');
}
// Check forced HTTPS protocol.
// @see \System\Traits\SecurityController
if (!$this->verifyForceSecure()) {
return Redirect::secure(Request::path());
}
// Check that user is logged in and has permission to view this page
if (!$this->isPublicAction($action)) {
// Not logged in, redirect to login screen or show ajax error.
if (!BackendAuth::check()) {
return Request::ajax()
? ajax()->error(Lang::get('backend::lang.page.access_denied.label'), 403)
: Backend::redirectGuest('backend/auth');
}
// Check general permission to backend
if (!BackendAuth::userHasAccess('general.backend')) {
throw new ForbiddenException;
}
// Check access groups against the page definition
if ($this->requiredPermissions && !$this->user->hasAnyAccess($this->requiredPermissions)) {
throw new ForbiddenException;
}
if (System::hasModule('Cms') && \Cms\Models\MaintenanceSetting::isEnabledForBackend()) {
return View::make('backend::in_maintenance');
}
if ($this->user->hasPasswordExpired() && !$this instanceof \Backend\Controllers\AuthGates) {
return Backend::redirect('backend/authgates/expired');
}
}
// Logic hook for all actions
if ($hook = $this->beforeDisplay()) {
return $hook;
}
/**
* @event backend.page.beforeDisplay
* Provides an opportunity to override backend page content
*
* Example usage:
*
* Event::listen('backend.page.beforeDisplay', function ((\Backend\Classes\Controller) $backendController, (string) $action, (array) $params) {
* traceLog('redirect all backend pages to google');
* return Redirect::to('https://google.com');
* });
*
* Or
*
* $backendController->bindEvent('page.beforeDisplay', function ((string) $action, (array) $params) {
* traceLog('redirect all backend pages to google');
* return Redirect::to('https://google.com');
* });
*
*/
if ($event = $this->fireSystemEvent('backend.page.beforeDisplay', [$action, $params])) {
return $event;
}
// Register Vue components used on every page
$this->registerVueComponent(\Backend\VueComponents\Modal::class);
// Set the admin preference locale
BackendPreference::setAppLocale();
BackendPreference::setAppFallbackLocale();
// Execute page action or AJAX event
if ($ajaxResponse = $this->execAjaxHandlers()) {
$result = $ajaxResponse;
}
else {
$result = $this->execPageAction($action, $params);
}
// Prepare and return response
// @see \System\Traits\ResponseMaker
return $this->makeResponse($result);
}
/**
* actionExists is used internally to determines whether an action with the specified name exists.
*
* - Action must be a class public method.
* - Action name can not be prefixed with the underscore character.
* - Action name must be lowercase.
* - Action must not appear in hiddenActions.
*
* @param string $name Specifies the action name.
* @param bool $internal Allow protected actions.
* @return bool
*/
public function actionExists($name, $internal = false)
{
// Must have length, not start with underscore and actually exist
if (!strlen($name) || substr($name, 0, 1) === '_' || !$this->methodExists($name)) {
return false;
}
// Only allow lowercase actions
if (strtolower($name) !== $name) {
return false;
}
// Checks hidden actions
foreach ($this->hiddenActions as $method) {
if (strtolower($name) === strtolower($method)) {
return false;
}
}
// Internal method check
$ownMethod = method_exists($this, $name);
if ($ownMethod) {
$methodInfo = new \ReflectionMethod($this, $name);
$public = $methodInfo->isPublic();
if ($public) {
return true;
}
}
if ($internal && (($ownMethod && $methodInfo->isProtected()) || !$ownMethod)) {
return true;
}
if (!$ownMethod) {
return true;
}
return false;
}
/**
* actionUrl returns a URL for this controller and supplied action.
*/
public function actionUrl($action = null, $path = null)
{
if ($action === null) {
$action = $this->action;
}
$class = get_called_class();
$uriPath = dirname(dirname(strtolower(str_replace('\\', '/', $class))));
$controllerName = strtolower(class_basename($class));
$url = $uriPath.'/'.$controllerName.'/'.$action;
if ($path) {
$url .= '/'.$path;
}
return Backend::url($url);
}
/**
* pageAction invokes the current controller action without rendering a view.
*/
public function pageAction()
{
if (!$this->action) {
return;
}
$this->suppressView = true;
$this->execPageAction($this->action, $this->params);
}
/**
* This method is used internally.
* Invokes the controller action and loads the corresponding view.
* @param string $actionName Specifies a action name to execute.
* @param array $parameters A list of the action parameters.
*/
protected function execPageAction($actionName, $parameters)
{
$result = null;
if (!$this->actionExists($actionName)) {
if (System::checkDebugMode()) {
throw new SystemException(sprintf(
"Action %s is not found in the controller %s",
$actionName,
get_class($this)
));
}
else {
Response::make(View::make('backend::404'), 404);
}
}
// Execute the action
$result = $this->makeCallMethod($this, $actionName, $parameters);
// Expecting \Response and \RedirectResponse
if ($result instanceof \Symfony\Component\HttpFoundation\Response) {
return $result;
}
// No page title
if (!$this->pageTitle) {
$this->pageTitle = Lang::get('backend::lang.page.untitled');
}
// Load the view
if (!$this->suppressView && $result === null) {
return $this->makeView($this->actionView ?: $actionName);
}
return $this->makeViewContent($result);
}
/**
* onAjax generic handler
*/
public function onAjax()
{
}
/**
* getAjaxHandler returns the AJAX handler for the current request, if available.
* @return string
*/
public function getAjaxHandler()
{
$request = $this->getAjaxRequest();
if ($request->hasAjaxHandler()) {
return $request->qualifiedHandler;
}
return null;
}
/**
* makePartialForAjax proxies to makePartial
* @see \Larajax\Traits\AjaxController
*/
protected function makePartialForAjax($partial)
{
if (!preg_match('/^(?!.*\/\/)[a-z0-9\_][a-z0-9\_\-\/]*$/i', $partial)) {
throw new ApplicationException(Lang::get('backend::lang.partial.invalid_name', ['name'=>e($partial)]));
}
return $this->makePartial($partial);
}
/**
* makeCallForAjax proxies to makeCallMethod
* @see \Larajax\Traits\AjaxController
*/
protected function makeCallForAjax($callable, $parameters)
{
[$instance, $method] = $callable;
if ($instance instanceof \Backend\Classes\WidgetBase) {
$this->addViewPath($instance->getViewPaths());
$result = $this->makeCallMethod($instance, $method, $parameters);
$this->vars = $instance->vars + $this->vars;
return $result;
}
return $this->makeCallMethod($instance, $method, $parameters);
}
/**
* execAjaxHandlers is used internally and invokes a controller event handler and
* loads the supplied partials.
*/
protected function execAjaxHandlers()
{
$handler = $this->getAjaxHandler();
if (!$handler) {
return null;
}
try {
try {
// Execute the page action so behaviors and widgets are initialized
$this->pageAction();
if ($this->fatalError) {
throw new ApplicationException($this->fatalError);
}
/**
* @event backend.ajax.beforeRunHandler
* Provides an opportunity to modify an AJAX request
*
* The parameter provided is `$handler` (the requested AJAX handler to be run)
*
* Example usage (forwards AJAX handlers to a backend widget):
*
* Event::listen('backend.ajax.beforeRunHandler', function ((\Backend\Classes\Controller) $controller, (string) $handler) {
* if (strpos($handler, '::')) {
* [$componentAlias, $handlerName] = explode('::', $handler);
* if ($componentAlias === $this->getBackendWidgetAlias()) {
* return $this->backendControllerProxy->runAjaxHandler($handler);
* }
* }
* });
*
* Or
*
* $this->controller->bindEvent('ajax.beforeRunHandler', function ((string) $handler) {
* if (strpos($handler, '::')) {
* [$componentAlias, $handlerName] = explode('::', $handler);
* if ($componentAlias === $this->getBackendWidgetAlias()) {
* return $this->backendControllerProxy->runAjaxHandler($handler);
* }
* }
* });
*
*/
if ($event = $this->fireSystemEvent('backend.ajax.beforeRunHandler', [$handler])) {
$response = ajax()::wrap($event);
}
else {
$response = $this->callAjaxAction($this->action, $this->params);
}
}
catch (ComponentNotFound $ex) {
throw new ApplicationException(Lang::get('backend::lang.widget.not_bound', ['name'=>$this->getAjaxRequest()->component]));
}
catch (HandlerNameInvalid $ex) {
throw new ApplicationException(Lang::get('backend::lang.ajax_handler.invalid_name', ['name'=>$handler]));
}
catch (HandlerNotFound $ex) {
throw new ApplicationException(Lang::get('backend::lang.ajax_handler.not_found', ['name'=>e($handler)]));
}
catch (MassAssignmentException $ex) {
throw new ApplicationException(Lang::get('backend::lang.model.mass_assignment_failed', ['attribute' => $ex->getMessage()]));
}
}
catch (ValidationException $ex) {
Flash::error($ex->getMessage());
$response = ajax()->invalidFields($ex->errors());
}
catch (Throwable $ex) {
$response = ajax()->exception($ex);
}
if ($this->hasAssetsDefined()) {
foreach ($this->getAssetPathsWithAttributes() as $type => $paths) {
$response->asset($type, $paths);
}
}
$response = $this->outputVueComponentsForAjax($response);
if (!$response->isRedirect() && Flash::check()) {
$response->update([
'#layout-flash-messages' => $this->makeLayoutPartial('flash_messages')
]);
}
return $response;
}
//
// Getters
//
/**
* getParams returns the action parameters
*/
public function getParams()
{
return $this->params;
}
/**
* getAction returns the action name
*/
public function getAction()
{
return $this->action;
}
/**
* getPublicActions returns the controllers public actions
*/
public function getPublicActions()
{
return $this->publicActions;
}
/**
* isPublicAction returns true if the current action is public
*/
public function isPublicAction(?string $action): bool
{
if (!$action) {
return false;
}
return in_array($action, $this->publicActions);
}
/**
* getId returns a unique ID for the controller and route. Useful in creating HTML markup.
*/
public function getId($suffix = null)
{
$id = class_basename(get_called_class()) . '-' . $this->action;
if ($suffix !== null) {
$id .= '-' . $suffix;
}
return $id;
}
//
// Hints
//
/**
* makeHintPartial renders a hint partial, used for displaying informative information that
* can be hidden by the user. If you don't want to render a partial, you can
* supply content via the 'content' key of $params.
* @param string $name Unique key name
* @param string $partial Reference to content (partial name)
* @param array $params Extra parameters
* @return string
*/
public function makeHintPartial($name, $partial = null, $params = [])
{
if (is_array($partial)) {
$params = $partial;
$partial = null;
}
if (!$partial) {
$partial = array_get($params, 'partial', $name);
}
return $this->makeLayoutPartial('hint', [
'hintName' => $name,
'hintPartial' => $partial,
'hintContent' => array_get($params, 'content'),
'hintParams' => $params
] + $params);
}
/**
* onHideBackendHint ajax handler to hide a backend hint, once hidden the partial
* will no longer display for the user.
* @return void
*/
public function onHideBackendHint()
{
if (!$name = post('name')) {
throw new ApplicationException('Missing a hint name.');
}
$preferences = UserPreference::forUser();
$hiddenHints = $preferences->get('backend::hints.hidden', []);
$hiddenHints[$name] = 1;
$preferences->set('backend::hints.hidden', $hiddenHints);
}
/**
* isBackendHintHidden checks if a hint has been hidden by the user.
* @param string $name Unique key name
* @return bool
*/
public function isBackendHintHidden($name)
{
$hiddenHints = UserPreference::forUser()->get('backend::hints.hidden', []);
return array_key_exists($name, $hiddenHints);
}
//
// Turbo
//
/**
* getTurboMetaTags returns an array of turbo meta tag definitions
* based on the $turboRouter property configuration.
*/
public function getTurboMetaTags(): array
{
$turboRouter = $this->turboRouter;
// Fallback to deprecated property
if ($turboRouter === null && $this->turboVisitControl !== null) {
$turboRouter = $this->turboVisitControl === 'disable'
? false
: ['visit-control' => $this->turboVisitControl];
}
// Disabled
if ($turboRouter === null || $turboRouter === false) {
return [];
}
// Simple enable
if ($turboRouter === true) {
$turboRouter = ['visit-control' => 'enable'];
}
// String shorthand, e.g. 'reload' → ['visit-control' => 'reload']
if (is_string($turboRouter)) {
$turboRouter = ['visit-control' => $turboRouter];
}
// Default turbo-root
if (!isset($turboRouter['root'])) {
$turboRouter['root'] = \Backend::baseUrl();
}
// Build meta tags
$tags = [];
foreach ($turboRouter as $key => $value) {
$tags[] = ['name' => "turbo-{$key}", 'content' => $value];
}
return $tags;
}
}
================================================
FILE: modules/backend/classes/ControllerBehavior.php
================================================
controller = $controller;
$this->viewPath = $this->configPath = $this->guessViewPath('/partials');
$this->assetPath = $this->guessViewPath('/assets', true);
// Validate controller properties
foreach ($this->requiredProperties as $property) {
if (!isset($controller->{$property})) {
throw new ApplicationException(Lang::get('system::lang.behavior.missing_property', [
'class' => get_class($controller),
'property' => $property,
'behavior' => get_called_class()
]));
}
}
// Hide all methods that aren't explicitly listed as actions
if (is_array($this->actions)) {
$this->hideAction(array_diff(get_class_methods(get_class($this)), $this->actions));
}
// Constructor logic that is protected by authentication
$controller->bindEvent('page.beforeDisplay', function() {
$this->beforeDisplay();
});
}
/**
* beforeDisplay fires before the page is displayed and AJAX is executed.
*/
public function beforeDisplay()
{
}
/**
* setConfig sets the configuration values
* @param mixed $config Config object or array
* @param array $required Required config items
*/
public function setConfig($config, $required = [])
{
$this->config = $this->makeConfig($config, $required);
}
/**
* getConfig is a safe accessor for configuration values
* @param string $name Config name, supports array names like "field[key]"
* @param mixed $default Default value if nothing is found
* @return string
*/
public function getConfig($name = null, $default = null)
{
if (!$this->config) {
return $default;
}
return $this->getConfigValueFrom($this->config, $name, $default);
}
/**
* hideAction protects a public method from being available as an controller action.
* These methods could be defined in a controller to override a behavior default action.
* Such methods should be defined as public, to allow the behavior object to access it.
* By default public methods of a controller are considered as actions.
* To prevent this occurrence, methods should be hidden by using this method.
* @param mixed $methodName Specifies a method name.
*/
protected function hideAction($methodName)
{
if (!is_array($methodName)) {
$methodName = [$methodName];
}
$this->controller->hiddenActions = array_merge($this->controller->hiddenActions, $methodName);
}
/**
* makeFileContents makes all views in context of the controller, not the behavior.
* @param string $filePath Absolute path to the view file.
* @param array $extraParams Parameters that should be available to the view.
* @return string
*/
public function makeFileContents($filePath, $extraParams = [])
{
$this->controller->vars = array_merge($this->controller->vars, $this->vars);
return $this->controller->makeFileContents($filePath, $extraParams);
}
/**
* @deprecated
*/
protected function controllerMethodExists($methodName)
{
return method_exists($this->controller, $methodName);
}
}
================================================
FILE: modules/backend/classes/FilterScope.php
================================================
getOptionsFromModelAsString($model, $scopeOptions);
}
// Cast collections to array
if ($scopeOptions instanceof Collection) {
$scopeOptions = $scopeOptions->all();
}
// Always be an array
if ($scopeOptions === null) {
return $scopeOptions = [];
}
return $scopeOptions;
}
/**
* getOptionsFromModelAsString where options are an explicit method reference
*/
protected function getOptionsFromModelAsString($model, string $methodName)
{
// Calling via ClassName::method
if (
strpos($methodName, '::') !== false &&
($staticMethod = explode('::', $methodName)) &&
count($staticMethod) === 2 &&
is_callable($staticMethod)
) {
$scopeOptions = $staticMethod($model, $this);
if (!is_array($scopeOptions)) {
throw new SystemException(Lang::get('backend::lang.field.options_static_method_invalid_value', [
'class' => $staticMethod[0],
'method' => $staticMethod[1]
]));
}
}
// Calling via $model->method
else {
if (!$this->objectMethodExists($model, $methodName)) {
throw new SystemException(Lang::get('backend::lang.filter.options_method_not_exists', [
'model' => get_class($model),
'method' => $methodName,
'filter' => $this->fieldName
]));
}
$scopeOptions = $model->$methodName($this);
}
return $scopeOptions;
}
/**
* applyScopeMethodToQuery
*/
public function applyScopeMethodToQuery($query, $methodName = null)
{
if (!$methodName) {
$methodName = $this->modelScope;
}
// Calling via ClassName::method
if (
is_string($methodName) &&
strpos($methodName, '::') !== false &&
($staticMethod = explode('::', $methodName)) &&
count($staticMethod) === 2 &&
is_callable($staticMethod)
) {
$methodName = $staticMethod;
}
// Calling via query builder
if (is_string($methodName)) {
$query->$methodName($this);
}
// Calling via callable
else {
$methodName($query, $this);
}
}
/**
* getName returns a value suitable for the scope name property.
* @param string $arrayName
* @return string
*/
public function getName($arrayName = null)
{
if ($arrayName === null) {
$arrayName = $this->arrayName;
}
if ($arrayName) {
return $arrayName.'['.implode('][', HtmlHelper::nameToArray($this->scopeName)).']';
}
return $this->scopeName;
}
/**
* getId returns a value suitable for the scope id property.
*/
public function getId($suffix = null)
{
$id = 'scope';
$id .= '-'.$this->scopeName;
if ($suffix) {
$id .= '-'.$suffix;
}
if ($this->idPrefix) {
$id = $this->idPrefix . '-' . $id;
}
return HtmlHelper::nameToId($id);
}
/**
* getDefaultScopeValue returns a fully qualified scope default value
*/
public function getDefaultScopeValue()
{
$defaults = $this->defaults;
if ($defaults === null) {
return null;
}
// Basic value
if (is_scalar($defaults)) {
return ['value' => $defaults];
}
// Invalid value
if (!is_array($defaults)) {
return null;
}
// Numerical array
if (Arr::isList($defaults)) {
return ['value' => $defaults];
}
// Associative array
return $defaults;
}
/**
* objectMethodExists is an internal helper for method existence checks.
* @param object $object
* @param string $method
*/
protected function objectMethodExists($object, $method): bool
{
if (method_exists($object, 'methodExists')) {
return $object->methodExists($method);
}
return method_exists($object, $method);
}
}
================================================
FILE: modules/backend/classes/FilterWidgetBase.php
================================================
filterScope = $filterScope;
$this->scopeName = $filterScope->scopeName;
$this->valueFrom = $filterScope->valueFrom ?: $this->scopeName;
$this->config = $this->makeConfig($configuration);
$this->fillFromConfig([
'model',
'isJsonable',
'parentFilter',
]);
parent::__construct($controller, $configuration);
}
/**
* getParentFilter retrieves the parent form for this formwidget
* @return Backend\Widgets\Filter|null
*/
public function getParentFilter()
{
return $this->parentFilter;
}
/**
* renderForm the form to use for filtering
*/
public function renderForm()
{
}
/**
* getScopeName returns the HTML element field name for this widget, used for
* capturing user input, passed back to the getSaveValue method when saving.
* @return string
*/
public function getScopeName()
{
return $this->filterScope->getName();
}
/**
* getLoadValue returns the value for this form field,
* supports nesting via HTML array.
* @return string
*/
public function getLoadValue()
{
return $this->filterScope->scopeValue;
}
/**
* getHeaderValue looks up the scope header
*/
public function getHeaderValue()
{
return $this->getParentFilter()->getHeaderValue($this->filterScope);
}
/**
* getActiveValue
*/
public function getActiveValue()
{
if (post('clearScope')) {
return null;
}
return post($this->getScopeName(), post("Filter"));
}
/**
* getFilterScope
*/
public function getFilterScope()
{
return $this->filterScope;
}
/**
* applyScopeToQuery
*/
public function applyScopeToQuery($query)
{
}
/**
* hasPostValue
*/
protected function hasPostValue($name): bool
{
$value = post(
$this->getScopeName() . "[{$name}]",
post("Filter[{$name}]")
);
return strlen(trim((string) $value)) > 0;
}
}
================================================
FILE: modules/backend/classes/FormField.php
================================================
$config,
'label' => $label
]);
}
parent::__construct($config);
}
/**
* initDefaultValues for this field
*/
protected function initDefaultValues()
{
parent::initDefaultValues();
$this
->valueTrans(true)
->attributes([])
;
}
/**
* isSelected determines if the provided value matches this field's value.
* @param string $value
* @return bool
*/
public function isSelected($value = true)
{
if ($this->value === null) {
return false;
}
return (string) $value === (string) $this->value;
}
/**
* hasAttribute checks if the field has the supplied [unfiltered] attribute.
* @param string $name
* @param string $position
* @return bool
*/
public function hasAttribute($name, $position = 'field')
{
$posKey = $position === 'container' ? 'containerAttributes' : 'attributes';
if (!isset($this->config[$posKey])) {
return false;
}
return array_key_exists($name, $this->config[$posKey]);
}
/**
* getAttributes returns the attributes for this field at a given position.
* @param string $position
* @return array
*/
public function getAttributes($position = 'field', $htmlBuild = true)
{
$posKey = $position === 'container' ? 'containerAttributes' : 'attributes';
$result = $this->config[$posKey] ?? [];
$result = $this->filterAttributes($result, $position);
return $htmlBuild ? Html::attributes($result) : $result;
}
/**
* filterAttributes adds any circumstantial attributes to the field based on other
* settings, such as the 'disabled' option.
* @param array $attributes
* @param string $position
* @return array
*/
protected function filterAttributes($attributes, $position = 'field')
{
$position = strtolower($position);
$attributes = $this->filterTriggerAttributes($attributes, $position);
$attributes = $this->filterPresetAttributes($attributes, $position);
if ($position === 'field' && $this->disabled) {
$attributes = $attributes + ['disabled' => 'disabled'];
}
if ($position === 'field' && $this->autoFocus) {
$attributes = $attributes + ['autofocus' => 'autofocus'];
}
if ($position === 'field' && $this->readOnly) {
$attributes = $attributes + ['readonly' => 'readonly'];
if ($this->type === 'checkbox' || $this->type === 'switch') {
$attributes = $attributes + ['onclick' => 'return false;'];
}
}
return $attributes;
}
/**
* filterTriggerAttributes adds attributes used specifically by the Trigger API
* @param array $attributes
* @param string $position
* @return array
*/
protected function filterTriggerAttributes($attributes, $position = 'field')
{
if (!$this->trigger || !is_array($this->trigger)) {
return $attributes;
}
$triggerAction = array_get($this->trigger, 'action');
$triggerField = array_get($this->trigger, 'field');
$triggerCondition = array_get($this->trigger, 'condition');
$triggerForm = $this->arrayName;
$triggerMulti = '';
// Apply these to container
if (in_array($triggerAction, ['hide', 'show']) && $position !== 'container') {
return $attributes;
}
// Apply these to field/input
if (in_array($triggerAction, ['enable', 'disable', 'empty']) && $position !== 'field') {
return $attributes;
}
// Reduce the field reference for the trigger condition field
$triggerFieldParentLevel = Str::getPrecedingSymbols($triggerField, self::HIERARCHY_UP);
if ($triggerFieldParentLevel > 0) {
// Remove the preceding symbols from the trigger field name
$triggerField = substr($triggerField, $triggerFieldParentLevel);
$triggerForm = HtmlHelper::reduceNameHierarchy($triggerForm, $triggerFieldParentLevel);
}
// Preserve multi field types
if (Str::endsWith($triggerField, '[]')) {
$triggerField = substr($triggerField, 0, -2);
$triggerMulti = '[]';
}
// Final compilation
if ($this->arrayName) {
$fullTriggerField = $triggerForm.'['.implode('][', HtmlHelper::nameToArray($triggerField)).']'.$triggerMulti;
}
else {
$fullTriggerField = $triggerField.$triggerMulti;
}
$newAttributes = [
'data-trigger' => '[name="'.$fullTriggerField.'"]',
'data-trigger-action' => $triggerAction,
'data-trigger-condition' => $triggerCondition,
'data-trigger-closest-parent' => 'form, div[data-control="formwidget"]'
];
return $attributes + $newAttributes;
}
/**
* filterPresetAttributes adds attributes used specifically by the Input Preset API
* @param array $attributes
* @param string $position
* @return array
*/
protected function filterPresetAttributes($attributes, $position = 'field')
{
if (!$this->preset || $position !== 'field') {
return $attributes;
}
if (!is_array($this->preset)) {
$this->preset = ['field' => $this->preset, 'type' => 'slug'];
}
$presetField = array_get($this->preset, 'field');
$presetType = array_get($this->preset, 'type');
if ($this->arrayName) {
$fullPresetField = $this->arrayName.'['.implode('][', HtmlHelper::nameToArray($presetField)).']';
}
else {
$fullPresetField = $presetField;
}
$newAttributes = [
'data-input-preset' => '[name="'.$fullPresetField.'"]',
'data-input-preset-type' => $presetType,
'data-input-preset-closest-parent' => 'form'
];
if ($prefixInput = array_get($this->preset, 'prefixInput')) {
$newAttributes['data-input-preset-prefix-input'] = $prefixInput;
}
return $attributes + $newAttributes;
}
/**
* getName returns a value suitable for the field name property. The array name is taken
* from the field or can be specified in the arguments.
* @param string $arrayName
* @return string
*/
public function getName($arrayName = null)
{
if ($arrayName === null) {
$arrayName = $this->arrayName;
}
if ($arrayName) {
return $arrayName.'['.implode('][', HtmlHelper::nameToArray($this->fieldName)).']';
}
return $this->fieldName;
}
/**
* getId returns a value suitable for the field id property.
* @param string $suffix Specify a suffix string
* @return string
*/
public function getId($suffix = null)
{
$id = 'field';
if ($this->arrayName) {
$id .= '-'.$this->arrayName;
}
$id .= '-'.$this->fieldName;
if ($suffix) {
$id .= '-'.$suffix;
}
if ($this->idPrefix) {
$id = $this->idPrefix . '-' . $id;
}
return HtmlHelper::nameToId($id);
}
/**
* getDisplayValue checks to see if display values (model attributes) should be translated,
* and also escapes the value
*/
public function getDisplayValue($value)
{
if ($this->valueTrans) {
return e(__($value));
}
return e($value);
}
/**
* getTranslatableMessage
*/
public function getTranslatableMessage(): string
{
if ($editSite = Site::getEditSite()) {
return e($editSite->name);
}
return '';
}
/**
* getCallableMethodFromValue checks for a string "Class::method" or as an
* array [Class, method] usually from a YAML definition
*/
public function getCallableMethodFromValue($value): ?array
{
if (
is_string($value) &&
count($staticMethod = explode('::', $value)) === 2 &&
is_callable($value)
) {
return $staticMethod;
}
if (
is_array($value) &&
count($value) === 2 &&
Arr::isList($value)
) {
return $value;
}
return null;
}
/**
* getValueFromData returns this fields value from a supplied data set, which can be
* an array or a model or another generic collection.
* @param mixed $data
* @param mixed $default
* @return mixed
*/
public function getValueFromData($data, $default = null)
{
$fieldName = $this->valueFrom ?: $this->fieldName;
return $this->getFieldNameFromData($fieldName, $data, $default);
}
/**
* getDefaultFromData returns the default value for this field, the supplied data is used
* to source data when defaultFrom is specified.
* @param mixed $data
* @return mixed
*/
public function getDefaultFromData($data)
{
if ($this->defaultFrom) {
return $this->getFieldNameFromData($this->defaultFrom, $data);
}
if ($this->defaults !== '') {
return $this->defaults;
}
return null;
}
/**
* resolveModelAttribute returns the final model and attribute name of a nested attribute. Eg:
*
* [$model, $attribute] = $this->resolveAttribute('person[phone]');
*
* @param string $attribute
* @return array
*/
public function resolveModelAttribute($model, $attribute = null)
{
return $this->resolveModelAttributeInternal($model, $attribute);
}
/**
* nearestModelAttribute returns the nearest model and attribute name of a nested attribute,
* which is useful for checking if an attribute is jsonable or a relation.
*/
public function nearestModelAttribute($model, $attribute = null)
{
return $this->resolveModelAttributeInternal($model, $attribute, [
'nearMatch' => true,
'objectOnly' => true
]);
}
/**
* resolveModelAttributeInternal is an internal method resolver for resolveModelAttribute
*/
protected function resolveModelAttributeInternal($model, $attribute = null, $options = [])
{
extract(array_merge([
'objectOnly' => false,
'nearMatch' => false
], $options));
if ($attribute === null) {
$attribute = $this->valueFrom ?: $this->fieldName;
}
$parts = is_array($attribute) ? $attribute : HtmlHelper::nameToArray($attribute);
$last = array_pop($parts);
foreach ($parts as $part) {
if ($objectOnly && !is_object($model->{$part})) {
if ($nearMatch) {
return [$model, $part];
}
continue;
}
$model = $model->{$part};
}
return [$model, $last];
}
/**
* getFieldNameFromData is an internal method to extract the value of a field name
* from a data set.
* @param string $fieldName
* @param mixed $data
* @param mixed $default
* @return mixed
*/
protected function getFieldNameFromData($fieldName, $data, $default = null)
{
// Array field name, eg: field[key][key2][key3]
$keyParts = HtmlHelper::nameToArray($fieldName);
$lastField = end($keyParts);
$result = $data;
// Loop the field key parts and build a value.
// To support relations only the last field should return the
// relation value, all others will look up the relation object as normal.
foreach ($keyParts as $key) {
if ($result instanceof Model && $result->hasRelation($key)) {
if ($key === $lastField) {
$result = $result->getRelationSimpleValue($key) ?: $default;
}
else {
$result = $result->{$key};
}
}
elseif (is_array($result)) {
if (!array_key_exists($key, $result)) {
return $default;
}
$result = $result[$key];
}
else {
if (!isset($result->{$key})) {
return $default;
}
$result = $result->{$key};
}
}
return $result;
}
/**
* getOptionsFromModel looks at the model for defined options.
*/
public function getOptionsFromModel($model, $fieldOptions, $data)
{
// Preset
if (is_string($fieldOptions) && str_starts_with($fieldOptions, 'preset:')) {
$fieldOptions = \System\Classes\PresetManager::instance()->getPreset($fieldOptions);
}
// Method name
elseif (is_string($fieldOptions)) {
$fieldOptions = $this->getOptionsFromModelAsString($model, $fieldOptions, $data);
}
// Default collection
elseif ($fieldOptions === null || $fieldOptions === true) {
$fieldOptions = $this->getOptionsFromModelAsDefault($model, $data);
}
// Cast collections to array
if ($fieldOptions instanceof Collection) {
$fieldOptions = $fieldOptions->all();
}
return $fieldOptions;
}
/**
* getOptionsFromModelAsString where options are an explicit method reference
*/
protected function getOptionsFromModelAsString($model, string $methodName, $data)
{
// Calling via ClassName::method
if ($callableMethod = $this->getCallableMethodFromValue($methodName)) {
$fieldOptions = $callableMethod($model, $this);
if (!is_array($fieldOptions)) {
throw new SystemException(Lang::get('backend::lang.field.options_static_method_invalid_value', [
'class' => $callableMethod[0],
'method' => $callableMethod[1]
]));
}
}
// Calling via $model->method
else {
if (!$this->objectMethodExists($model, $methodName)) {
throw new SystemException(Lang::get('backend::lang.field.options_method_not_exists', [
'model' => get_class($model),
'method' => $methodName,
'field' => $this->fieldName
]));
}
$fieldOptions = $model->$methodName($this->value, $this->fieldName, $data);
}
return $fieldOptions;
}
/**
* getOptionsFromModelAsDefault refers to the model method or any of its behaviors
*/
protected function getOptionsFromModelAsDefault($model, $data)
{
try {
[$model, $attribute] = $this->resolveModelAttributeInternal($model, $this->fieldName, ['objectOnly' => true]);
}
catch (Exception $ex) {
throw new SystemException(Lang::get('backend::lang.field.options_method_invalid_model', [
'model' => get_class($model),
'field' => $this->fieldName
]));
}
$methodName = 'get'.studly_case($attribute).'Options';
if (
!$this->objectMethodExists($model, $methodName) &&
!$this->objectMethodExists($model, 'getDropdownOptions')
) {
throw new SystemException(Lang::get('backend::lang.field.options_method_not_exists', [
'model' => get_class($model),
'method' => $methodName,
'field' => $this->fieldName
]));
}
if ($this->objectMethodExists($model, $methodName)) {
$fieldOptions = $model->$methodName($this->value, $data);
}
else {
$fieldOptions = $model->getDropdownOptions($attribute, $this->value, $data);
}
return $fieldOptions;
}
/**
* objectMethodExists is an internal helper for method existence checks.
*
* @param object $object
* @param string $method
* @return boolean
*/
protected function objectMethodExists($object, $method)
{
if (method_exists($object, 'methodExists')) {
return $object->methodExists($method);
}
return method_exists($object, $method);
}
}
================================================
FILE: modules/backend/classes/FormTabs.php
================================================
section(self::SECTION_OUTSIDE)
->defaultTab('backend::lang.form.undefined_tab')
->linkable()
->icons([])
->lazy([])
->adaptive([])
;
}
/**
* evalConfig
*/
public function evalConfig(array $config)
{
if (isset($config['section']) && $config['section'] === self::SECTION_OUTSIDE) {
$this->suppressTabs();
}
}
/**
* isLazy checks if a tab should be lazy loaded
*/
public function isLazy($tabName): bool
{
return in_array($tabName, $this->config['lazy']);
}
/**
* addLazy flags a tab to be lazy loaded
*/
public function addLazy($tabName)
{
$this->config['lazy'] = array_merge((array) $this->config['lazy'], (array) $tabName);
}
/**
* isAdaptive checks if a tab uses adaptive sizing
*/
public function isAdaptive($tabName): bool
{
return in_array($tabName, $this->config['adaptive']);
}
/**
* addAdaptive flags a tab to use adaptive sizing
*/
public function addAdaptive($tabName)
{
$this->config['adaptive'] = array_merge((array) $this->config['adaptive'], (array) $tabName);
}
/**
* getIcon returns an icon for the tab based on the tab's name
* @param string $name
* @return string
*/
public function getIcon($name)
{
if (!empty($this->config['icons'][$name])) {
return $this->config['icons'][$name];
}
}
/**
* getPaneCssClass returns a tab pane CSS class
* @param string $index
* @param string $label
* @return string
*/
public function getPaneCssClass($index = null, $label = null)
{
if (!isset($this->config['paneCssClass'])) {
return '';
}
if (is_string($this->config['paneCssClass'])) {
return $this->config['paneCssClass'];
}
if ($index !== null && isset($this->config['paneCssClass'][$index])) {
return $this->config['paneCssClass'][$index];
}
if ($label !== null && isset($this->config['paneCssClass'][$label])) {
return $this->config['paneCssClass'][$label];
}
return $this->config['paneCssClass']['*'] ?? '';
}
/**
* setPaneCssClass appends a CSS class to the tab pane
*/
public function setPaneCssClass($tabNameOrIndex, string $cssClass, bool $overwrite = false)
{
if (is_string($this->config['paneCssClass'])) {
$this->config['paneCssClass'] = ['*' => $this->config['paneCssClass']];
}
if ($overwrite) {
$this->config['paneCssClass'][$tabNameOrIndex] = $cssClass;
}
else {
$currentValue = $this->config['paneCssClass'][$tabNameOrIndex] ?? '';
$this->config['paneCssClass'][$tabNameOrIndex] = trim($currentValue . ' ' . $cssClass);
}
}
/**
* isPaneActive returns a tab pane CSS class
*/
public function isPaneActive($index = null, $label = null): bool
{
if ($this->activeTab === null) {
return $index === 1;
}
if ($index !== null && $this->activeTab === $index) {
return true;
}
if ($label !== null && $this->activeTab === $label) {
return true;
}
return false;
}
/**
* getPaneAnchorId returns a value suitable for the pane id property.
* @return string
*/
public function getPaneAnchorId($index = null, $label = null)
{
$id = $this->section . 'tab';
if ($this->linkable) {
$id .= '-' . (Str::slug(__($label)) ?: $index);
}
else {
$id .= '-' . $index;
}
return HtmlHelper::nameToId($id);
}
/**
* getPaneId
*/
public function getPaneId($tabName)
{
if ($id = $this->getIdentifier($tabName)) {
return "pane-{$id}";
}
return null;
}
/**
* getTabId
*/
public function getTabId($tabName)
{
if ($id = $this->getIdentifier($tabName)) {
return "tab-{$id}";
}
return null;
}
/**
* getIdentifier returns an API identifier for the specified tab
*/
public function getIdentifier($tabName): ?string
{
return $this->config['identifiers'][$tabName] ?? null;
}
/**
* addIdentifier adds a new API identifier for the specified tab
*/
public function addIdentifier($tabName, $identifier)
{
$this->config['identifiers'][$tabName] = $identifier;
}
}
================================================
FILE: modules/backend/classes/FormWidgetBase.php
================================================
formField = $formField;
$this->fieldName = $formField->fieldName;
$this->valueFrom = $formField->valueFrom ?: $this->fieldName;
$this->config = $this->makeConfig($configuration);
$this->fillFromConfig([
'model',
'data',
'sessionKey',
'sessionKeySuffix',
'previewMode',
'showLabels',
'parentForm',
]);
parent::__construct($controller, $configuration);
}
/**
* getParentForm retrieves the parent form for this formwidget
* @return \Backend\Widgets\Form|null
*/
public function getParentForm()
{
return $this->parentForm;
}
/**
* getFieldName returns the HTML element field name for this widget, used for
* capturing user input, passed back to the getSaveValue method when saving.
* @return string
*/
public function getFieldName()
{
return $this->formField->getName();
}
/**
* getId returns a unique ID for this widget. Useful in creating HTML markup.
*/
public function getId($suffix = null)
{
$id = parent::getId($suffix);
$id .= '-' . $this->fieldName;
return HtmlHelper::nameToId($id);
}
/**
* getSaveValue processes the postback value for this widget. If the value is omitted from
* postback data, the form widget will be skipped.
* @param mixed $value The existing value for this widget.
* @return string The new value for this widget.
*/
public function getSaveValue($value)
{
return $value;
}
/**
* getLoadValue returns the value for this form field,
* supports nesting via HTML array.
* @return string
*/
public function getLoadValue()
{
if ($this->formField->value !== null) {
return $this->formField->value;
}
$defaultValue = $this->model && !$this->model->exists
? $this->formField->getDefaultFromData($this->data ?: $this->model)
: null;
return $this->formField->getValueFromData($this->data ?: $this->model, $defaultValue);
}
/**
* resetFormValue from the form field, triggered by the parent form calling `setFormValues`
* and the new value is in the formField object `value` property.
*/
public function resetFormValue()
{
}
/**
* getSessionKey returns the active session key, including suffix.
* @return string
*/
public function getSessionKey()
{
return $this->sessionKey . $this->sessionKeySuffix;
}
}
================================================
FILE: modules/backend/classes/ListColumn.php
================================================
$config,
'label' => $label
]);
}
parent::__construct($config);
}
/**
* initDefaultValues for this field
*/
protected function initDefaultValues()
{
parent::initDefaultValues();
$this
->valueTrans(true)
;
}
/**
* evalConfig
*/
public function evalConfig(array $config)
{
if (isset($config['select'])) {
// @deprecated use below
$this->sqlSelect($config['select']);
// $this->sqlSelect(array_pull($config, 'select'));
}
if (isset($config['default'])) {
$this->defaults($config['default']);
}
}
/**
* select
*/
public function select($column)
{
$this->sqlSelect($column);
}
/**
* getName returns a HTML valid name for the column name.
* @return string
*/
public function getName()
{
return HtmlHelper::nameToId($this->columnName);
}
/**
* getId returns a value suitable for the column id property.
* @param string $suffix Specify a suffix string
* @return string
*/
public function getId($suffix = null)
{
$id = 'column';
$id .= '-'.$this->columnName;
if ($suffix) {
$id .= '-'.$suffix;
}
return HtmlHelper::nameToId($id);
}
/**
* getDisplayValue checks to see if display values (model attributes) should be translated,
* and also escapes the value
*/
public function getDisplayValue($value)
{
if ($this->valueTrans) {
return e(__($value));
}
return e($value);
}
/**
* getAlignClass returns the column specific alignment css class.
* @return string
*/
public function getAlignClass()
{
return $this->align ? 'list-cell-align-' . $this->align : '';
}
/**
* useRelationCount
*/
public function useRelationCount(): bool
{
if (!$this->relation) {
return false;
}
// @deprecated use relationCount instead
if (($value = $this->getConfig('useRelationCount')) !== null) {
return $value;
}
return (bool) $this->relationCount;
}
/**
* getValueFromData returns this columns value from a supplied data set, which can be
* an array or a model or another generic collection.
* @param mixed $data
* @param mixed $default
* @return mixed
*/
public function getValueFromData($data, $default = null)
{
$columnName = $this->valueFrom ?: $this->columnName;
return $this->getColumnNameFromData($columnName, $data, $default);
}
/**
* Internal method to extract the value of a column name from a data set.
* @param string $columnName
* @param mixed $data
* @param mixed $default
* @return mixed
*/
protected function getColumnNameFromData($columnName, $data, $default = null)
{
/*
* Array column name, eg: column[key][key2][key3]
*/
$keyParts = HtmlHelper::nameToArray($columnName);
$result = $data;
/*
* Loop the column key parts and build a value.
* To support relations only the last column should return the
* relation value, all others will look up the relation object as normal.
*/
foreach ($keyParts as $key) {
if ($result instanceof Model && $result->hasRelation($key)) {
$result = $result->{$key};
}
else {
if (is_array($result) && array_key_exists($key, $result)) {
$result = $result[$key];
}
elseif ($result instanceof Model) {
$result = $result->getAttribute($key);
if ($result === null) {
return $default;
}
}
elseif (!isset($result->{$key})) {
return $default;
}
else {
$result = $result->{$key};
}
}
}
return $result;
}
}
================================================
FILE: modules/backend/classes/LoginCustomization.php
================================================
$index.'/image.png',
'background' => File::get($backgroundPath)
];
}
}
================================================
FILE: modules/backend/classes/MainMenuItem.php
================================================
order(500)
->permissions([])
->sideMenu([])
->useDropdown(true)
;
}
/**
* addPermission
* @param string $permission
* @param array $definition
*/
public function addPermission(string $permission, array $definition)
{
$this->config['permissions'][$permission] = $definition;
}
/**
* addSideMenuItem
* @param SideMenuItem $sideMenu
*/
public function addSideMenuItem(SideMenuItem $sideMenu)
{
$this->config['sideMenu'][$sideMenu->code] = $sideMenu;
}
/**
* getSideMenuItem
*/
public function getSideMenuItem(string $code): ?SideMenuItem
{
return $this->config['sideMenu'][$code] ?? null;
}
/**
* removeSideMenuItem
* @param string $code
*/
public function removeSideMenuItem(string $code)
{
unset($this->config['sideMenu'][$code]);
}
/**
* itemAttributes returns HTML attributes for the list item
*/
public function itemAttributes(): string
{
if ($this->attributes === null) {
return '';
}
return Html::attributes(array_except($this->attributes, ['target']));
}
/**
* linkAttributes returns HTML for the anchor link
*/
public function linkAttributes(): string
{
if (!isset($this->attributes['target'])) {
return '';
}
return Html::attributes(array_only($this->attributes, ['target']));
}
}
================================================
FILE: modules/backend/classes/NavigationManager.php
================================================
registerMenuItems([...]);
* });
*
* @deprecated this will be removed in a later version
* @param callable $callback A callable function.
*/
public function registerCallback(callable $callback)
{
App::extendInstance('backend.menu', $callback);
}
/**
* init this class items
*/
public function init()
{
if ($this->items === null) {
$this->loadItems();
}
}
/**
* loadItems from modules and plugins
*/
protected function loadItems()
{
$this->items = [];
// Load module items
foreach (System::listModules() as $module) {
if ($provider = App::getProvider($module . '\\ServiceProvider')) {
$items = $provider->registerNavigation();
if (is_array($items)) {
$this->registerMenuItems('October.'.$module, $items);
}
}
}
// Load plugin items, prevent system crashes
foreach (PluginManager::instance()->getPlugins() as $id => $plugin) {
try {
$items = $plugin->registerNavigation();
if (is_array($items)) {
$this->registerMenuItems($id, $items);
}
}
catch (Throwable $ex) {
Log::error($ex);
}
}
// Load app items
if ($app = App::getProvider(\App\Provider::class)) {
$items = $app->registerNavigation();
if (is_array($items)) {
$this->registerMenuItems('October.App', $items);
}
}
/**
* @event backend.menu.extendItems
* Provides an opportunity to manipulate the backend navigation
*
* Example usage:
*
* Event::listen('backend.menu.extendItems', function ((\Backend\Classes\NavigationManager) $manager) {
* $manager->addMainMenuItems(...)
* $manager->addSideMenuItems(...)
* $manager->removeMainMenuItem(...)
* });
*
*/
Event::fire('backend.menu.extendItems', [$this]);
// Sort menu items
uasort($this->items, static function ($a, $b) {
return (int) $a->order - (int) $b->order;
});
// Filter items user lacks permission for
$user = BackendAuth::getUser();
$this->items = $this->filterItemPermissions($user, $this->items);
foreach ($this->items as $item) {
$sideMenu = $item->sideMenu;
if (!$sideMenu || !count($sideMenu)) {
continue;
}
// Apply incremental default orders
$orderCount = 0;
foreach ($sideMenu as $sideMenuItem) {
if ($sideMenuItem->order !== -1) {
continue;
}
$sideMenuItem->order = ($orderCount += 100);
}
// Sort side menu items
uasort($sideMenu, static function ($a, $b) {
return $a->order - $b->order;
});
// Filter items user lacks permission for
$item->sideMenu($this->filterItemPermissions($user, $sideMenu));
}
}
/**
* registerMenuItems for the back-end menu items.
* The argument is an array of the main menu items. The array keys represent the
* menu item codes, specific for the plugin/module. Each element in the
* array should be an associative array with the following keys:
* - label - specifies the menu label localization string key, required.
* - icon - an icon name from the Font Awesome icon collection, required.
* - url - the back-end relative URL the menu item should point to, required.
* - permissions - an array of permissions the back-end user should have, optional.
* The item will be displayed if the user has any of the specified permissions.
* - order - a position of the item in the menu, optional.
* - counter - an optional numeric value to output near the menu icon. The value should be
* a number or a callable returning a number.
* - counterLabel - an optional string value to describe the numeric reference in counter.
* - sideMenu - an array of side menu items, optional. If provided, the array items
* should represent the side menu item code, and each value should be an associative
* array with the following keys:
* - label - specifies the menu label localization string key, required.
* - icon - an icon name from the Font Awesome icon collection, required.
* - url - the back-end relative URL the menu item should point to, required.
* - attributes - an array of attributes and values to apply to the menu item, optional.
* - permissions - an array of permissions the back-end user should have, optional.
* - counter - an optional numeric value to output near the menu icon. The value should be
* a number or a callable returning a number.
* - counterLabel - an optional string value to describe the numeric reference in counter.
* @param string $owner Specifies the menu items owner plugin or module in the format Author.Plugin.
* @param array $definitions An array of the menu item definitions.
*/
public function registerMenuItems($owner, array $definitions)
{
$this->init();
$this->addMainMenuItems($owner, $definitions);
}
/**
* addMainMenuItems dynamically adds an array of main menu items.
* @param string $owner
* @param array $definitions
*/
public function addMainMenuItems($owner, array $definitions)
{
foreach ($definitions as $code => $definition) {
$this->addMainMenuItem($owner, $code, $definition);
}
}
/**
* addMainMenuItem dynamically adds a single main menu item.
* @param string $owner
* @param string $code
* @param array $definition
*/
public function addMainMenuItem($owner, $code, array $definition)
{
if ($this->items === null) {
throw new SystemException('Unable to add navigation items before they are loaded.');
}
$itemKey = $this->makeItemKey($owner, $code);
if (isset($this->items[$itemKey])) {
$definition = array_merge(
$this->items[$itemKey]->toArray(),
$definition
);
}
$item = array_merge($definition, [
'code' => $code,
'owner' => $owner
]);
$sideMenu = array_pull($item, 'sideMenu');
$this->items[$itemKey] = $this->defineMainMenuItem($item);
if (is_array($sideMenu)) {
$this->addSideMenuItems($owner, $code, $sideMenu);
}
}
/**
* defineMainMenuItem
*/
protected function defineMainMenuItem(array $config): MainMenuItem
{
return (new MainMenuItem)->useConfig($config);
}
/**
* getMainMenuItem returns a main menu item
*/
public function getMainMenuItem(string $owner, string $code): ?MainMenuItem
{
$itemKey = $this->makeItemKey($owner, $code);
return $this->items[$itemKey] ?? null;
}
/**
* removeMainMenuItem removes a single main menu item
* @param $owner
* @param $code
*/
public function removeMainMenuItem($owner, $code)
{
if ($this->items === null) {
throw new SystemException('Unable to remove navigation items before they are loaded.');
}
$itemKey = $this->makeItemKey($owner, $code);
unset($this->items[$itemKey]);
}
/**
* addSideMenuItems dynamically adds an array of side menu items
* @param string $owner
* @param string $code
* @param array $definitions
*/
public function addSideMenuItems($owner, $code, array $definitions)
{
foreach ($definitions as $sideCode => $definition) {
if (is_array($definition)) {
$this->addSideMenuItem($owner, $code, $sideCode, $definition);
}
}
}
/**
* addSideMenuItem dynamically add a single side menu item
* @param string $owner
* @param string $code
* @param string $sideCode
* @param array $definition
* @return bool
*/
public function addSideMenuItem($owner, $code, $sideCode, array $definition)
{
if ($this->items === null) {
throw new SystemException('Unable to add navigation items before they are loaded.');
}
$itemKey = $this->makeItemKey($owner, $code);
if (!isset($this->items[$itemKey])) {
return false;
}
$mainItem = $this->items[$itemKey];
$definition = array_merge($definition, [
'code' => $sideCode,
'owner' => $owner
]);
if (isset($mainItem->sideMenu[$sideCode])) {
$definition = array_merge(
$mainItem->sideMenu[$sideCode]->toArray(),
$definition
);
}
$item = $this->defineSideMenuItem($definition);
$this->items[$itemKey]->addSideMenuItem($item);
return true;
}
/**
* defineSideMenuItem
*/
protected function defineSideMenuItem(array $config): SideMenuItem
{
return (new SideMenuItem)->useConfig($config);
}
/**
* getSideMenuItem returns a side menu item
*/
public function getSideMenuItem(string $owner, string $code, string $sideCode): ?SideMenuItem
{
return $this->getMainMenuItem($owner, $code)?->getSideMenuItem($sideCode);
}
/**
* removeSideMenuItems with multiple codes
* @param string $owner
* @param string $code
* @param array $sideCodes
* @return void
*/
public function removeSideMenuItems($owner, $code, $sideCodes)
{
foreach ($sideCodes as $sideCode) {
$this->removeSideMenuItem($owner, $code, $sideCode);
}
}
/**
* removeSideMenuItem removes a single main menu item
* @param string $owner
* @param string $code
* @param string $sideCode
* @return bool
*/
public function removeSideMenuItem($owner, $code, $sideCode)
{
if ($this->items === null) {
throw new SystemException('Unable to remove navigation items before they are loaded.');
}
$itemKey = $this->makeItemKey($owner, $code);
if (!isset($this->items[$itemKey])) {
return false;
}
$mainItem = $this->items[$itemKey];
$mainItem->removeSideMenuItem($sideCode);
return true;
}
/**
* listMainMenuItems returns a list of the main menu items.
* @return array
*/
public function listMainMenuItems()
{
$this->init();
foreach ($this->items as $item) {
if ($item->counter === false) {
continue;
}
// Counter specified
$item->counter = $this->getCallableCounterValue($item);
// Guess counter from sub items
if ($item->counter === null && ($sideItems = $this->listSideMenuItems($item->owner, $item->code))) {
$subCount = 0;
foreach ($sideItems as $sideItem) {
if ($sideItem->counter !== null) {
$subCount += $sideItem->counter;
}
}
if ($subCount > 0) {
$item->counter = $subCount;
}
}
}
return $this->items;
}
/**
* listSideMenuItems returns a list of side menu items for the currently active main menu item.
* The currently active main menu item is set with the setContext methods.
* @param null $owner
* @param null $code
* @return SideMenuItem[]
* @throws SystemException
*/
public function listSideMenuItems($owner = null, $code = null)
{
$this->init();
$activeItem = null;
if ($owner !== null && $code !== null) {
$activeItem = $this->items[$this->makeItemKey($owner, $code)] ?? null;
}
else {
foreach ($this->listMainMenuItems() as $item) {
if ($this->isMainMenuItemActive($item)) {
$activeItem = $item;
break;
}
}
}
if (!$activeItem) {
return [];
}
$items = $activeItem->sideMenu;
// Process counters
foreach ($items as $item) {
$item->counter = $this->getCallableCounterValue($item);
}
return $items;
}
/**
* listMainMenuItemsWithSubitems prepares data for displaying the top menu and side
* (collapsable) menu. Uses caching to avoid running counter functions twice.
*/
public function listMainMenuItemsWithSubitems()
{
if ($this->menuDisplayTree !== null) {
return $this->menuDisplayTree;
}
$mainMenuItems = $this->listMainMenuItems();
$this->menuDisplayTree = [];
foreach ($mainMenuItems as $mainMenuItem) {
$subMenuItems = $this->listSideMenuItems($mainMenuItem->owner, $mainMenuItem->code);
$this->menuDisplayTree[] = (object)[
'mainMenuItem' => $mainMenuItem,
'subMenuItems' => $subMenuItems,
'subMenuHasDropdown' => $mainMenuItem->useDropdown && count($subMenuItems)
];
}
return $this->menuDisplayTree;
}
/**
* listMainMenuSubItems uses cached result of listMainMenuItemsWithSubitems to return
* submenu items and avoid duplicate counter calls.
*/
public function listMainMenuSubItems()
{
$allItems = $this->listMainMenuItemsWithSubitems();
foreach ($allItems as $itemInfo) {
if ($this->isMainMenuItemActive($itemInfo->mainMenuItem)) {
return $itemInfo->subMenuItems;
}
}
return [];
}
/**
* getActiveMainMenuItem returns the currently active main menu item
* @return null|MainMenuItem $item Returns the item object or null.
* @throws SystemException
*/
public function getActiveMainMenuItem()
{
foreach ($this->listMainMenuItems() as $item) {
if ($this->isMainMenuItemActive($item)) {
return $item;
}
}
return null;
}
/**
* getCallableCounterValue returns the counter value for a menu item
*/
protected function getCallableCounterValue($item)
{
$counterValue = $item->counter;
if (empty($counterValue)) {
return null;
}
if (is_int($counterValue)) {
return $counterValue;
}
if (
is_string($counterValue) &&
strpos($counterValue, '::') !== false &&
($staticMethod = explode('::', $counterValue)) &&
count($staticMethod) === 2 &&
is_callable($staticMethod)
) {
return $staticMethod($item);
}
if (is_callable($counterValue)) {
return $counterValue($item);
}
return (int) $item->counter;
}
/**
* filterItemPermissions removes menu items from an array if the supplied user lacks permission.
* @param \Backend\Models\User $user A user object
* @param MainMenuItem[]|SideMenuItem[] $items A collection of menu items
* @return array The filtered menu items
*/
protected function filterItemPermissions($user, array $items)
{
if (!$user) {
return $items;
}
$items = array_filter($items, static function ($item) use ($user) {
if (!$item->permissions || !count($item->permissions)) {
return true;
}
return $user->hasAnyAccess($item->permissions);
});
return $items;
}
/**
* makeItemKey is an internal method to make a unique key for an item.
* @param string $owner
* @param string $code
* @return string
*/
protected function makeItemKey($owner, $code)
{
return strtoupper($owner).'.'.strtoupper($code);
}
/**
* resetCache resets any memory or cache involved with the sites
*/
public function resetCache()
{
$this->items = null;
$this->menuDisplayTree = null;
}
}
================================================
FILE: modules/backend/classes/ReportWidgetBase.php
================================================
registerPermissions([...]);
* });
*
* @deprecated this will be removed in a later version
* @param callable $callback A callable function.
*/
public function registerCallback(callable $callback)
{
App::extendInstance('backend.roles', $callback);
}
/**
* init this class items
*/
public function init()
{
if ($this->permissions === null) {
$this->loadPermissions();
}
}
/**
* loadItems from modules and plugins
*/
protected function loadPermissions()
{
$this->permissions = [];
// Load module items
foreach (System::listModules() as $module) {
if ($provider = App::getProvider($module . '\\ServiceProvider')) {
$items = $provider->registerPermissions();
if (is_array($items)) {
$this->registerPermissions('October.'.$module, $items);
}
}
}
// Load plugin items
foreach (PluginManager::instance()->getPlugins() as $id => $plugin) {
$items = $plugin->registerPermissions();
if (is_array($items)) {
$this->registerPermissions($id, $items);
}
}
// Load app items
if ($app = App::getProvider(\App\Provider::class)) {
$items = $app->registerPermissions();
if (is_array($items)) {
$this->registerPermissions('October.App', $items);
}
}
/**
* @event backend.roles.extendPermissions
* Provides an opportunity to manipulate the permissions
*
* Example usage:
*
* Event::listen('backend.roles.extendPermissions', function ((\Backend\Classes\RoleManager) $manager) {
* $manager->registerPermissions(...)
* });
*
*/
Event::fire('backend.roles.extendPermissions', [$this]);
// Sort permission items
usort($this->permissions, function ($a, $b) {
if ($a->order === $b->order) {
return 0;
}
return $a->order > $b->order ? 1 : -1;
});
}
/**
* registerPermissions registers the back-end permission items.
* The argument is an array of the permissions. The array keys represent the
* permission codes, specific for the plugin/module. Each element in the
* array should be an associative array with the following keys:
* - label - specifies the menu label localization string key, required.
* - order - a position of the item in the menu, optional.
* - comment - a brief comment that describes the permission, optional.
* - tab - assign this permission to a tabbed group, optional.
* @param string $owner Specifies the permissions' owner plugin or module in the format Author.Plugin
* @param array $definitions An array of the menu item definitions.
*/
public function registerPermissions($owner, array $definitions)
{
$this->init();
foreach ($definitions as $code => $definition) {
if ($definition && is_array($definition)) {
$permission = new RolePermission(array_merge($definition, [
'code' => $code,
'owner' => $owner
]));
$this->permissions[] = $permission;
}
}
}
/**
* removePermission removes a single back-end permission. Where owner specifies the
* permissions' owner plugin or module in the format Author.Plugin. Where code is
* the permission to remove.
*/
public function removePermission(string $owner, string $code)
{
if ($this->permissions === null) {
throw new SystemException('Unable to remove permissions before they are loaded.');
}
$ownerPermissions = array_filter($this->permissions, function ($permission) use ($owner) {
return $permission->owner === $owner;
});
foreach ($ownerPermissions as $key => $permission) {
if ($permission->code === $code) {
unset($this->permissions[$key]);
}
}
}
/**
* listPermissions returns a list of the registered permissions items.
*/
public function listPermissions(): array
{
$this->init();
return $this->permissions;
}
/**
* listPermissionsForUser returns permissions that the user has access to.
*/
public function listPermissionsForUser($user): array
{
return array_filter($this->listPermissions(), function($permission) use ($user) {
return $user->hasAccess($permission->code);
});
}
/**
* listPermissionsForRole returns an array of registered permissions belonging to a
* given role code.
* @param string $role
* @param bool $includeOrphans
*/
public function listPermissionsForRole($role, $includeOrphans = true): array
{
if ($this->permissionRoles === null) {
$this->permissionRoles = [];
foreach ($this->listPermissions() as $permission) {
if ($permission->roles) {
foreach ((array) $permission->roles as $_role) {
$this->permissionRoles[$_role][$permission->code] = 1;
}
}
else {
$this->permissionRoles['*'][$permission->code] = 1;
}
}
}
$result = $this->permissionRoles[$role] ?? [];
if ($includeOrphans) {
$result += $this->permissionRoles['*'] ?? [];
}
return $result;
}
/**
* hasPermissionsForRole checks if the user has the permissions for a role.
*/
public function hasPermissionsForRole($role): bool
{
return !!$this->listPermissionsForRole($role, false);
}
/**
* resetCache resets any memory or cache involved with the sites
*/
public function resetCache()
{
$this->permissions = null;
$this->permissionRoles = null;
}
}
================================================
FILE: modules/backend/classes/RolePermission.php
================================================
order(500);
}
/**
* addChild
*/
public function addChild($permission): static
{
$children = $this->children;
$children[$permission->code] = $permission;
$this->children($children);
return $this;
}
}
================================================
FILE: modules/backend/classes/SettingsController.php
================================================
findSettingsContextFromClass($this), $this->settingsItemCode);
parent::__construct();
}
/**
* findSettingsContextFromClass converts a controller class to a plugin code,
* if the author code is a module name, then we assume it is a module.
*/
protected function findSettingsContextFromClass()
{
$classNameArray = explode('\\', get_class($this));
$authorCode = array_shift($classNameArray);
$pluginCode = array_shift($classNameArray);
if (System::hasModule($authorCode)) {
return 'October.'.$authorCode;
}
return $authorCode.'.'.$pluginCode;
}
}
================================================
FILE: modules/backend/classes/SideMenuItem.php
================================================
attributes([])
->permissions([])
;
}
/**
* addAttribute
* @param null|string|int $attribute
* @param null|string|array $value
*/
public function addAttribute($attribute, $value)
{
$this->config['attributes'][$attribute] = $value;
}
/**
* removeAttribute
*/
public function removeAttribute($attribute)
{
unset($this->config['attributes'][$attribute]);
}
/**
* addPermission
* @deprecated recommend not using this method until v4 when signature is fixed
* should be a non-associative array
*/
public function addPermission(string $permission, array $definition)
{
$this->config['permissions'][$permission] = $definition;
}
/**
* removePermission
* @deprecated recommend not using this method until v4 when signature is fixed
* should spin over every value and remove via located key
* @param string $permission
* @return void
*/
public function removePermission(string $permission)
{
unset($this->config['permissions'][$permission]);
}
/**
* itemAttributes returns HTML attributes for the list item
*/
public function itemAttributes(): string
{
if ($this->attributes === null) {
return '';
}
return Html::attributes(array_except($this->attributes, ['target']));
}
/**
* linkAttributes returns HTML for the anchor link
*/
public function linkAttributes(): string
{
if (!isset($this->attributes['target'])) {
return '';
}
return Html::attributes(array_only($this->attributes, ['target']));
}
}
================================================
FILE: modules/backend/classes/Skin.php
================================================
defaultSkinPath = base_path() . '/modules/backend';
/*
* Guess the skin path
*/
$class = get_called_class();
$classFolder = strtolower(class_basename($class));
$classFile = realpath(dirname(File::fromClass($class)));
$this->skinPath = $classFile
? $classFile . '/' . $classFolder
: $this->defaultSkinPath;
$this->publicSkinPath = File::localToPublic($this->skinPath);
$this->defaultPublicSkinPath = File::localToPublic($this->defaultSkinPath);
}
/**
* getPath looks up a path to a skin-based file, if it doesn't exist, the default path is used.
* @param string $path
* @param boolean $isPublic
* @return string
*/
public function getPath($path = null, $isPublic = false)
{
$path = RouterHelper::normalizeUrl($path);
$assetFile = $this->skinPath . $path;
if (File::isFile($assetFile)) {
return $isPublic
? $this->publicSkinPath . $path
: $this->skinPath . $path;
}
return $isPublic
? $this->defaultPublicSkinPath . $path
: $this->defaultSkinPath . $path;
}
/**
* getLayoutPaths returns an array of paths where skin layouts can be found.
* @return array
*/
public function getLayoutPaths()
{
return [$this->skinPath.'/layouts', $this->defaultSkinPath.'/layouts'];
}
/**
* getActive returns the active skin.
*/
public static function getActive()
{
if (self::$skinCache !== null) {
return self::$skinCache;
}
$skinClass = Config::get('backend.skin', \Backend\Skins\Standard::class);
$skinObject = new $skinClass();
return self::$skinCache = $skinObject;
}
}
================================================
FILE: modules/backend/classes/VueComponentBase.php
================================================
controller = $controller;
$this->viewPath = $this->guessViewPath('/partials');
$this->assetPath = $this->guessViewPath('/assets', true);
// Prepare assets used by this widget.
$this->loadDependencyAssets();
$this->loadDefaultAssets();
$this->loadAssets();
$this->registerSubcomponents();
$this->prepareVars();
parent::__construct();
}
/**
* render the default component partial.
*/
public function render()
{
return $this->makePartial($this->getComponentBaseName());
}
/**
* renderSubcomponent
*/
public function renderSubcomponent($name)
{
if (!array_key_exists($name, $this->subcomponents)) {
throw new SystemException(sprintf('Subcomponent not registered: %s', $name));
}
return $this->makePartial($name);
}
/**
* getDependencies
*/
public function getDependencies()
{
return $this->require;
}
/**
* getSubcomponents
*/
public function getSubcomponents()
{
return array_keys($this->subcomponents);
}
/**
* getEsmModules returns JS-only ESM modules (no templates)
*/
public function getEsmModules()
{
return array_keys($this->esmModules);
}
/**
* getEsmModulePath returns the ESM module path for this component.
* By default, derived from the component's asset path.
*/
public function getEsmModulePath(): string
{
$baseName = $this->getComponentBaseName();
return $this->assetPath . "/js/{$baseName}.js";
}
/**
* getSubcomponentEsmPath returns the ESM path for a specific subcomponent.
*/
public function getSubcomponentEsmPath(string $name): string
{
return $this->assetPath . "/js/{$name}.js";
}
/**
* loadDefaultAssets adds the default CSS file for this component.
* JavaScript is loaded via ESM imports in VueMaker::outputVueComponentTemplates()
*/
protected function loadDefaultAssets()
{
$baseName = $this->getComponentBaseName();
$cssPath = "css/{$baseName}.css";
if (File::exists(base_path($this->assetPath.'/'.$cssPath))) {
$this->addCss($cssPath);
}
}
/**
* prepareVars required by the component's partials
*/
protected function prepareVars()
{
}
/**
* loadAssets adds component specific asset files. Use $this->addJs() and
* $this->addCss() to register new assets to include on the page.
* The default component script and CSS file are loaded automatically.
* @return void
*/
protected function loadAssets()
{
}
/**
* loadDependencyAssets adds dependency assets required for the component.
* This method is called before the component's default resources are loaded.
* Use $this->addJs() and $this->addCss() to register new assets to include
* on the page.
* @return void
*/
protected function loadDependencyAssets()
{
}
/**
* getComponentBaseName
*/
protected function getComponentBaseName()
{
$classNameArray = explode('\\', get_class($this));
return strtolower(end($classNameArray));
}
/**
* getComponentName returns the Vue component tag name.
*/
public function getComponentName(): string
{
if (!$this->componentName) {
throw new SystemException(sprintf(
'Vue component [%s] must define a $componentName property.',
get_class($this)
));
}
return $this->componentName;
}
/**
* getSubcomponentName returns the Vue component tag name for a subcomponent.
*/
public function getSubcomponentName(string $subcomponent): string
{
return $this->getComponentName() . '-' . $subcomponent;
}
/**
* registerSubcomponent adds a subcomponent.
* @param string $name The component name.
* A JavaScript file and partial with the same name must exist.
* JavaScript is loaded via ESM imports in VueMaker::outputVueComponentTemplates()
*/
protected function registerSubcomponent($name)
{
$name = strtolower($name);
$this->subcomponents[$name] = true;
}
/**
* registerEsmModule adds a JavaScript-only ESM module (no template).
* Use this for utility/helper modules that don't have Vue templates.
* @param string $name The module name (without .js extension).
*/
protected function registerEsmModule($name)
{
$name = strtolower($name);
$this->esmModules[$name] = true;
}
/**
* registerSubcomponents
*/
protected function registerSubcomponents()
{
}
}
================================================
FILE: modules/backend/classes/WidgetBase.php
================================================
controller = $controller;
$this->viewPath = $this->configPath = $this->guessViewPath('/partials');
$this->assetPath = $this->guessViewPath('/assets', true);
// Apply configuration values to a new config object, if a parent
// constructor hasn't done it already.
if ($this->config === null) {
$this->config = $this->makeConfig($config);
}
// If no alias is set by the configuration.
if (!isset($this->alias)) {
$this->alias = $this->config->alias ?? $this->defaultAlias;
}
// Prepare assets used by this widget.
$this->loadAssets();
parent::__construct();
// Initialize the widget.
if (!$this->getConfig('noInit', false)) {
$this->init();
}
}
/**
* init the widget, called by the constructor and free from its parameters.
* @return void
*/
public function init()
{
}
/**
* render the widget's primary contents.
* @return string HTML markup supplied by this widget.
*/
public function render()
{
}
/**
* loadAssets adds widget specific asset files. Use $this->addJs() and $this->addCss()
* to register new assets to include on the page.
* @return void
*/
protected function loadAssets()
{
}
/**
* fillFromConfig transfers config values stored inside the $config property directly
* on to the root object properties. If no properties are defined
* all config will be transferred if it finds a matching property.
* @param array $properties
* @return void
*/
protected function fillFromConfig($properties = null)
{
if ($properties === null) {
$properties = array_keys((array) $this->config);
}
foreach ($properties as $property) {
if (property_exists($this, $property)) {
$this->{$property} = $this->getConfig($property, $this->{$property});
}
}
}
/**
* getId returns a unique ID for this widget. Useful in creating HTML markup.
* @param string $suffix An extra string to append to the ID.
* @return string A unique identifier.
*/
public function getId($suffix = null)
{
$id = class_basename(get_called_class());
if ($this->alias !== $this->defaultAlias) {
$id .= '-' . $this->alias;
}
if ($suffix !== null) {
$id .= '-' . $suffix;
}
return HtmlHelper::nameToId($id);
}
/**
* getEventHandler returns a fully qualified event handler name for this widget.
* @param string $name The ajax event handler name.
* @return string
*/
public function getEventHandler($name)
{
return $this->alias . '::' . $name;
}
/**
* getConfig is a safe accessor for configuration values
* @param string $name Config name, supports array names like "field[key]"
* @param mixed $default Default value if nothing is found
* @return string
*/
public function getConfig($name = null, $default = null)
{
if (!$this->config) {
return $default;
}
return $this->getConfigValueFrom($this->config, $name, $default);
}
/**
* getController returns the controller using this widget.
*/
public function getController()
{
return $this->controller;
}
}
================================================
FILE: modules/backend/classes/WidgetManager.php
================================================
pluginManager = PluginManager::instance();
}
/**
* instance creates a new instance of this singleton
*/
public static function instance(): static
{
return App::make('backend.widgets');
}
}
================================================
FILE: modules/backend/classes/WildcardController.php
================================================
setContextOwner($owner);
$this->setContextMainMenu($mainMenuItemCode);
$this->setContextSideMenu($sideMenuItemCode);
}
/**
* setContextOwner sets the navigation context owner.
* The navigation owner is in the format of Author.Plugin or Module name.
* @param string $owner
*/
public function setContextOwner($owner)
{
$this->contextOwner = $owner;
}
/**
* setContextMainMenu specifies a code of the main menu item in the current
* navigation context.
* @param string $mainMenuItemCode
*/
public function setContextMainMenu($mainMenuItemCode)
{
$this->contextMainMenuItemCode = $mainMenuItemCode;
}
/**
* setContextSideMenu specifies a code of the side menu item in the current navigation context.
* If the code is set to TRUE, the first item will be flagged as active.
* @param string $sideMenuItemCode
*/
public function setContextSideMenu($sideMenuItemCode)
{
$this->contextSideMenuItemCode = $sideMenuItemCode;
}
/**
* getContext returns information about the current navigation context.
* @return mixed Returns an object with the following fields:
* - mainMenuCode
* - sideMenuCode
* - owner
*/
public function getContext()
{
return (object)[
'mainMenuCode' => $this->contextMainMenuItemCode,
'sideMenuCode' => $this->contextSideMenuItemCode,
'owner' => $this->contextOwner
];
}
/**
* isMainMenuItemActive determines if a main menu item is active.
* Returns true if the menu item is active.
* @param \Backend\Classes\MainMenuItem $item
* @return bool
*/
public function isMainMenuItemActive($item)
{
return $this->contextOwner === $item->owner && $this->contextMainMenuItemCode === $item->code;
}
/**
* isDashboardItemActive determines if the dashboard is active.
* @return bool
*/
public function isDashboardItemActive()
{
return $this->contextOwner === 'October.Dashboard' && $this->contextMainMenuItemCode === 'dashboard';
}
/**
* isSideMenuItemActive determines if a side menu item is active.
* @param SideMenuItem $item
*/
public function isSideMenuItemActive($item): bool
{
if ($this->contextSideMenuItemCode === true) {
$this->contextSideMenuItemCode = null;
return true;
}
return $this->contextOwner === $item->owner && $this->contextSideMenuItemCode === $item->code;
}
/**
* isSideMenuItemActive determines if a side menu item is active.
* @param SideMenuItem $item
*/
public function isSideMenuItemVisible($item): bool
{
if (!$item->visibleOn) {
return true;
}
if ($this->contextOwner === $item->owner && in_array($this->contextSideMenuItemCode, (array) $item->visibleOn)) {
return true;
}
return false;
}
/**
* registerContextSidenavPartial registers a special side navigation partial for a specific
* main menu. The sidenav partial replaces the standard side navigation.
* @param string $owner Specifies the navigation owner in the format Vendor/Module.
* @param string $mainMenuItemCode Specifies the main menu item code.
* @param string $partial Specifies the partial name.
*/
public function registerContextSidenavPartial($owner, $mainMenuItemCode, $partial)
{
$this->contextSidenavPartials[$owner.$mainMenuItemCode] = $partial;
}
/**
* getContextSidenavPartial returns the side navigation partial for a specific main menu
* previously registered with the registerContextSidenavPartial() method.
* @param string $owner Specifies the navigation owner in the format Vendor/Module.
* @param string $mainMenuItemCode Specifies the main menu item code.
* @return mixed Returns the partial name or null.
*/
public function getContextSidenavPartial($owner, $mainMenuItemCode)
{
$key = $owner.$mainMenuItemCode;
return $this->contextSidenavPartials[$key] ?? null;
}
}
================================================
FILE: modules/backend/classes/navigationmanager/HasTailorNavigationContext.php
================================================
find($uuid);
if (!$blueprint) {
return;
}
$this->setContext(
'October.Tailor',
$blueprint->getNavigationCodeName(),
$sideMenuItemCode
);
}
/**
* setTailorContext
*/
public function setTailorContext(string $handle, ?string $sideMenuItemCode = null)
{
if (!System::hasModule('Tailor')) {
return;
}
$blueprint = BlueprintIndexer::instance()->findByHandle($handle);
if (!$blueprint) {
return;
}
$this->setContext(
'October.Tailor',
$blueprint->getNavigationCodeName(),
$sideMenuItemCode
);
}
}
================================================
FILE: modules/backend/classes/widgetmanager/HasFilterWidgets.php
================================================
'FilterWidgetClass'].
*/
protected $filterWidgetHints;
/**
* listFilterWidgets returns a list of registered filter widgets.
* @return array Array keys are class names.
*/
public function listFilterWidgets()
{
if ($this->filterWidgets === null) {
$this->filterWidgets = [];
// Load external widgets
foreach ($this->filterWidgetCallbacks as $callback) {
$callback($this);
}
// Load module items
foreach (System::listModules() as $module) {
if ($provider = App::getProvider($module . '\\ServiceProvider')) {
$widgets = $provider->registerFilterWidgets();
if (is_array($widgets)) {
foreach ($widgets as $className => $widgetInfo) {
$this->registerFilterWidget($className, $widgetInfo);
}
}
}
}
// Load plugin widgets
foreach ($this->pluginManager->getPlugins() as $plugin) {
$widgets = $plugin->registerFilterWidgets();
if (!is_array($widgets)) {
continue;
}
foreach ($widgets as $className => $widgetInfo) {
$this->registerFilterWidget($className, $widgetInfo);
}
}
// Load app widgets
if ($app = App::getProvider(\App\Provider::class)) {
$widgets = $app->registerFilterWidgets();
if (is_array($widgets)) {
foreach ($widgets as $className => $widgetInfo) {
$this->registerFilterWidget($className, $widgetInfo);
}
}
}
}
return $this->filterWidgets;
}
/**
* getFilterWidgets returns the raw array of registered filter widgets.
* @return array Array keys are class names.
*/
public function getFilterWidgets()
{
return $this->filterWidgets;
}
/*
* registerFilterWidget registers a single filter widget.
*/
public function registerFilterWidget($className, $widgetInfo)
{
if (!is_array($widgetInfo)) {
$widgetInfo = ['code' => $widgetInfo];
}
$widgetCode = $widgetInfo['code'] ?? null;
if (!$widgetCode) {
$widgetCode = Str::getClassId($className);
}
$this->filterWidgets[$className] = $widgetInfo;
$this->filterWidgetHints[$widgetCode] = $className;
}
/**
* registerFilterWidgets manually registers filter widget for consideration. Usage:
*
* WidgetManager::registerFilterWidgets(function ($manager) {
* $manager->registerFilterWidget(\Backend\FilterWidgets\DateRange::class, 'daterange');
* });
*
*/
public function registerFilterWidgets(callable $definitions)
{
$this->filterWidgetCallbacks[] = $definitions;
}
/**
* resolveFilterWidget returns a class name from a filter widget code
* Normalizes a class name or converts an code to its class name.
* Returns the class name resolved, or the original name.
* @param string $name
* @return string
*/
public function resolveFilterWidget($name)
{
if ($this->filterWidgets === null) {
$this->listFilterWidgets();
}
$hints = $this->filterWidgetHints;
if (isset($hints[$name])) {
return $hints[$name];
}
$_name = Str::normalizeClassName($name);
if (isset($this->filterWidgets[$_name])) {
return $_name;
}
return $name;
}
}
================================================
FILE: modules/backend/classes/widgetmanager/HasFormWidgets.php
================================================
$formWidgetInfo]
*/
protected $formWidgets;
/**
* @var array formWidgetCallbacks cache
*/
protected $formWidgetCallbacks = [];
/**
* @var array formWidgetHints keyed by their code.
* Stored in the form of ['formwidgetcode' => 'FormWidgetClass'].
*/
protected $formWidgetHints;
/**
* listFormWidgets returns a list of registered form widgets.
* @return array Array keys are class names.
*/
public function listFormWidgets()
{
if ($this->formWidgets === null) {
$this->formWidgets = [];
// Load external widgets
foreach ($this->formWidgetCallbacks as $callback) {
$callback($this);
}
// Load module items
foreach (System::listModules() as $module) {
if ($provider = App::getProvider($module . '\\ServiceProvider')) {
$widgets = $provider->registerFormWidgets();
if (is_array($widgets)) {
foreach ($widgets as $className => $widgetInfo) {
$this->registerFormWidget($className, $widgetInfo);
}
}
}
}
// Load plugin widgets
foreach ($this->pluginManager->getPlugins() as $plugin) {
$widgets = $plugin->registerFormWidgets();
if (!is_array($widgets)) {
continue;
}
foreach ($widgets as $className => $widgetInfo) {
$this->registerFormWidget($className, $widgetInfo);
}
}
// Load app widgets
if ($app = App::getProvider(\App\Provider::class)) {
$widgets = $app->registerFormWidgets();
if (is_array($widgets)) {
foreach ($widgets as $className => $widgetInfo) {
$this->registerFormWidget($className, $widgetInfo);
}
}
}
}
return $this->formWidgets;
}
/**
* registerFormWidget registers a single form widget.
* @param string $className Widget class name.
* @param array $widgetInfo Registration information, can contain a `code` key.
* @return void
*/
public function registerFormWidget($className, $widgetInfo = null)
{
if (!is_array($widgetInfo)) {
$widgetInfo = ['code' => $widgetInfo];
}
$widgetCode = $widgetInfo['code'] ?? null;
if (!$widgetCode) {
$widgetCode = Str::getClassId($className);
}
$this->formWidgets[$className] = $widgetInfo;
$this->formWidgetHints[$widgetCode] = $className;
}
/**
* registerFormWidgets manually registers form widget for consideration. Usage:
*
* WidgetManager::registerFormWidgets(function ($manager) {
* $manager->registerFormWidget(\Backend\FormWidgets\CodeEditor::class, 'codeeditor');
* });
*
*/
public function registerFormWidgets(callable $definitions)
{
$this->formWidgetCallbacks[] = $definitions;
}
/**
* resolveFormWidget returns a class name from a form widget code
* Normalizes a class name or converts an code to its class name.
* Returns the class name resolved, or the original name.
* @param string $name
* @return string
*/
public function resolveFormWidget($name)
{
if ($this->formWidgets === null) {
$this->listFormWidgets();
}
$hints = $this->formWidgetHints;
if (isset($hints[$name])) {
return $hints[$name];
}
$_name = Str::normalizeClassName($name);
if (isset($this->formWidgets[$_name])) {
return $_name;
}
return $name;
}
}
================================================
FILE: modules/backend/classes/widgetmanager/HasReportWidgets.php
================================================
'FormWidgetClass'].
*/
protected $reportWidgetHints;
/**
* listReportWidgets returns a list of registered report widgets.
* @return array Array keys are class names.
*/
public function listReportWidgets()
{
if ($this->reportWidgets === null) {
$this->reportWidgets = [];
// Load external widgets
foreach ($this->reportWidgetCallbacks as $callback) {
$callback($this);
}
// Load module items
foreach (System::listModules() as $module) {
if ($provider = App::getProvider($module . '\\ServiceProvider')) {
$widgets = $provider->registerReportWidgets();
if (is_array($widgets)) {
foreach ($widgets as $className => $widgetInfo) {
$this->registerReportWidget($className, $widgetInfo);
}
}
}
}
// Load plugin widgets
foreach ($this->pluginManager->getPlugins() as $plugin) {
$widgets = $plugin->registerReportWidgets();
if (!is_array($widgets)) {
continue;
}
foreach ($widgets as $className => $widgetInfo) {
$this->registerReportWidget($className, $widgetInfo);
}
}
// Load app widgets
if ($app = App::getProvider(\App\Provider::class)) {
$widgets = $app->registerReportWidgets();
if (is_array($widgets)) {
foreach ($widgets as $className => $widgetInfo) {
$this->registerReportWidget($className, $widgetInfo);
}
}
}
}
/**
* @event system.reportwidgets.extendItems
* Enables adding or removing report widgets.
*
* You will have access to the WidgetManager instance and be able to call the appropriate methods
* $manager->registerReportWidget();
* $manager->removeReportWidget();
*
* Example usage:
*
* Event::listen('system.reportwidgets.extendItems', function ($manager) {
* $manager->removeReportWidget('Acme\ReportWidgets\YourWidget');
* });
*
*/
Event::fire('system.reportwidgets.extendItems', [$this]);
$user = BackendAuth::getUser();
foreach ($this->reportWidgets as $widget => $config) {
if (!empty($config['permissions'])) {
if (!$user->hasAccess($config['permissions'], false)) {
unset($this->reportWidgets[$widget]);
}
}
}
return $this->reportWidgets;
}
/**
* getReportWidgets returns the raw array of registered report widgets.
* @return array Array keys are class names.
*/
public function getReportWidgets()
{
return $this->reportWidgets;
}
/*
* registerReportWidget registers a single report widget.
*/
public function registerReportWidget($className, $widgetInfo)
{
if (!is_array($widgetInfo)) {
$widgetInfo = ['code' => $widgetInfo];
}
$widgetCode = $widgetInfo['code'] ?? null;
if (!$widgetCode) {
$widgetCode = Str::getClassId($className);
}
$this->reportWidgets[$className] = $widgetInfo;
$this->reportWidgetHints[$widgetCode] = $className;
}
/**
* registerReportWidgets manually registers report widget for consideration. Usage:
*
* WidgetManager::registerReportWidgets(function ($manager) {
* $manager->registerReportWidget(\RainLab\GoogleAnalytics\ReportWidgets\TrafficOverview::class, [
* 'name' => 'Google Analytics traffic overview',
* 'context' => 'dashboard'
* ]);
* });
*
*/
public function registerReportWidgets(callable $definitions)
{
$this->reportWidgetCallbacks[] = $definitions;
}
/**
* resolveReportWidget returns a class name from a report widget code
* Normalizes a class name or converts an code to its class name.
* Returns the class name resolved, or the original name.
* @param string $name
* @return string
*/
public function resolveReportWidget($name)
{
if ($this->reportWidgets === null) {
$this->listReportWidgets();
}
$hints = $this->reportWidgetHints;
if (isset($hints[$name])) {
return $hints[$name];
}
$_name = Str::normalizeClassName($name);
if (isset($this->reportWidgets[$_name])) {
return $_name;
}
return $name;
}
/**
* removeReportWidget removes a registered ReportWidget.
* @param string $className Widget class name.
* @return void
*/
public function removeReportWidget($className)
{
if (!$this->reportWidgets) {
throw new SystemException('Unable to remove a widget before widgets are loaded.');
}
unset($this->reportWidgets[$className]);
}
}
================================================
FILE: modules/backend/composer.json
================================================
{
"name": "october/backend",
"type": "october-module",
"description": "Backend module for October CMS",
"homepage": "https://octobercms.com",
"keywords": ["october cms", "october", "backend"],
"authors": [
{
"name": "Alexey Bobkov",
"email": "aleksey.bobkov@gmail.com"
},
{
"name": "Samuel Georges",
"email": "daftspunky@gmail.com"
}
]
}
================================================
FILE: modules/backend/controllers/AccessLogs.php
================================================
listRefresh();
}
}
================================================
FILE: modules/backend/controllers/Auth.php
================================================
layout = 'auth';
}
/**
* index is the default route, redirects to signin or migrate
*/
public function index()
{
if ($this->checkAdminAccounts()) {
return Backend::redirect('backend/auth/migrate');
}
else {
return Backend::redirect('backend/auth/signin');
}
}
/**
* signin displays the log in page
*/
public function signin()
{
$this->bodyClass = 'signin';
// Clear Cache and any previous data to fix invalid security token issue
$this->setResponseHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
try {
if ($this->checkPostbackFlag()) {
return $this->handleSubmitSignin();
}
}
catch (Exception $ex) {
Flash::error($ex->getMessage());
}
}
/**
* handleSubmitSignin handles the submission of the sign in form
*/
protected function handleSubmitSignin()
{
$rules = [
'login' => 'required|between:2,255',
'password' => 'required|between:4,255'
];
$validation = Validator::make(post(), $rules, [], [
'login' => __('Username'),
'password' => __('Password'),
]);
if ($validation->fails()) {
throw new ValidationException($validation);
}
// Determine remember policy
$remember = Config::get('backend.force_remember');
if ($remember === null) {
$remember = post('remember');
}
// Authenticate user
$user = BackendAuth::authenticate([
'login' => post('login'),
'password' => post('password')
], (bool) $remember);
// Log the sign in event
AccessLog::add($user);
// Redirect to the intended page after successful sign in
return Backend::redirectIntended('backend');
}
/**
* signout logs out a backend user
*/
public function signout()
{
BackendAuth::logout();
// Add HTTP Header 'Clear Site Data' to purge all sensitive data upon signout
if (Request::secure()) {
$this->setResponseHeader(
'Clear-Site-Data',
'cache, cookies, storage, executionContexts'
);
}
return Backend::redirect('backend');
}
/**
* restore displays a page to request a password reset verification code
*/
public function restore()
{
if (!$this->checkCanReset()) {
throw new NotFoundException;
}
try {
if ($this->checkPostbackFlag()) {
return $this->handleSubmitRestore();
}
}
catch (Exception $ex) {
Flash::error($ex->getMessage());
}
}
/**
* handleSubmitRestore submits the restore form
*/
protected function handleSubmitRestore()
{
$rules = [
'login' => 'required|between:2,255'
];
$validation = Validator::make(post(), $rules, [], ['login' => __('Username')]);
if ($validation->fails()) {
throw new ValidationException($validation);
}
$user = BackendAuth::findUserByLogin(post('login'));
if (!$user) {
// For security reasons, only show detailed error when debug mode is on
if (System::checkDebugMode()) {
throw new ValidationException([
'login' => __("A user could not be found with a login value of ':login'", ['login' => post('login')])
]);
}
}
else {
// User found, send reset email
$code = $user->getResetPasswordCode();
$link = Backend::url('backend/auth/reset/' . $user->id . '/' . $code);
$data = [
'name' => $user->full_name,
'link' => $link,
];
Mail::send('backend:restore', $data, function ($message) use ($user) {
$message->to($user->email, $user->full_name)->subject(__('Password Reset'));
});
}
Flash::success(__('If your account was found, a message has been sent to your email address with instructions.'));
return Backend::redirect('backend/auth/signin');
}
/**
* reset backend user password using verification code
*/
public function reset($userId = null, $code = null)
{
if (!$this->checkCanReset()) {
throw new NotFoundException;
}
try {
if ($this->checkPostbackFlag()) {
return $this->handleSubmitReset();
}
if (!$userId || !$code) {
throw new ApplicationException(__('Invalid password reset data supplied. Please try again!'));
}
}
catch (Exception $ex) {
Flash::error($ex->getMessage());
}
$this->vars['code'] = $code;
$this->vars['id'] = $userId;
}
/**
* handleSubmitReset submits the reset form
*/
protected function handleSubmitReset()
{
if (!post('id') || !post('code')) {
throw new ApplicationException(__('Invalid password reset data supplied. Please try again!'));
}
$rules = [
'password' => 'required|between:4,255'
];
$validation = Validator::make(post(), $rules, [], [
'password' => __('Password'),
]);
if ($validation->fails()) {
throw new ValidationException($validation);
}
$code = post('code');
$user = BackendAuth::findUserById(post('id'));
if (!$user || !$user->checkResetPasswordCode($code)) {
throw new ApplicationException(__('Invalid password reset data supplied. Please try again!'));
}
// Validate password against policy
$user->validatePasswordPolicy(post('password'));
if (!$user->attemptResetPassword($code, post('password'))) {
throw new ApplicationException(__('Unable to reset your password!'));
}
// Clear the code used to reset the password
$user->clearResetPassword();
// Clear throttles
BackendAuth::clearThrottleForUserId($user->id);
Flash::success(__('Password has been reset. You may now sign in.'));
return Backend::redirect('backend/auth/signin');
}
/**
* setup will allow a user to create the first admin account
*/
public function setup()
{
$this->bodyClass = 'setup';
if (!$this->checkAdminAccounts()) {
return Backend::redirect('backend/auth/signin');
}
try {
if ($this->checkPostbackFlag()) {
return $this->handleSubmitSetup();
}
}
catch (Exception $ex) {
Flash::error($ex->getMessage());
}
}
/**
* handleSubmitSetup creates a new admin
*/
protected function handleSubmitSetup()
{
if (!$this->checkAdminAccounts()) {
return Backend::redirect('backend/auth/signin');
}
// Validate user input
$rules = [
'first_name' => 'required',
'last_name' => 'required',
'email' => 'required|between:6,255|email|unique:backend_users',
'login' => 'required|between:2,255|unique:backend_users',
'password' => 'required:create|between:4,255|confirmed',
'password_confirmation' => 'required_with:password|between:4,255'
];
$validation = Validator::make(post(), $rules, [], [
'first_name' => __('First name'),
'last_name' => __('Last name'),
'email' => __('Email'),
'login' => __('Username'),
'password' => __('Password'),
'password_confirmation' => __('Confirm Password'),
]);
if ($validation->fails()) {
throw new ValidationException($validation);
}
// Validate password against policy
(new UserModel)->validatePasswordPolicy(post('password'));
// Create user and sign in
$user = UserModel::createDefaultAdmin(post());
BackendAuth::login($user);
// Redirect
Flash::success(__('Welcome to your Administration Area, :name', ['name' => e(post('first_name'))]));
return Backend::redirectIntended('backend');
}
/**
* migrate shows a progress bar while the database migrates
*/
public function migrate()
{
$this->bodyClass = 'setup';
if (!$this->checkAdminAccounts()) {
return Backend::redirect('backend/auth/signin');
}
}
/**
* migrate_onMigrate migrates the database
*/
public function migrate_onMigrate()
{
if (!$this->checkAdminAccounts()) {
return Backend::redirect('backend/auth/signin');
}
try {
UpdateManager::instance()->update();
}
catch (Exception $ex) {
Log::error($ex);
Flash::error($ex->getMessage());
}
return Backend::redirect('backend/auth/setup');
}
/**
* checkPostbackFlag checks to see if the form has been submitted
*/
protected function checkPostbackFlag(): bool
{
return Request::method() === 'POST' && post('postback');
}
/**
* checkAdminAccounts will determine if this is a new installation
*/
protected function checkAdminAccounts(): bool
{
// Debug mode must be turned on
if (!System::checkDebugMode()) {
return false;
}
// There must be no admin accounts, with database migrated
if (System::hasDatabase() && UserModel::count() > 0) {
return false;
}
// Ensures database hasn't fallen over
if (!App::hasDatabase()) {
return false;
}
return true;
}
/**
* checkCanReset password via self service
*/
protected function checkCanReset(): bool
{
return (bool) Config::get('backend.password_policy.allow_reset', true);
}
}
================================================
FILE: modules/backend/controllers/AuthGates.php
================================================
layout = 'auth';
}
/**
* expired shows a password reset screen when the password has expired
*/
public function expired()
{
}
/**
* expired_onSubmit submits the expired password form
*/
protected function expired_onSubmit()
{
$rules = [
'current_password' => 'required|between:4,255',
'password' => 'required|between:4,255|confirmed|different:current_password',
'password_confirmation' => 'required_with:password|between:4,255'
];
$validation = Validator::make(post(), $rules, [], [
'current_password' => __("Current Password"),
'password' => __("New Password"),
'password_confirmation' => __("Confirm Password"),
]);
if ($validation->fails()) {
throw new ValidationException($validation);
}
$user = $this->user;
if (!$user || !$user->checkPassword(post('current_password'))) {
throw new ApplicationException(__("Current password does not match. Please try again!"));
}
// Validate password against policy
$user->validatePasswordPolicy(post('password'));
// Reset the user password and clear any code used to reset the password
$user->password = post('password');
$user->password_changed_at = $user->freshTimestamp();
$user->is_password_expired = false;
$user->reset_password_code = null;
$user->forceSave();
// Clear throttles
BackendAuth::clearThrottleForUserId($user->id);
Flash::success(__("Password has been reset. You may now sign in."));
return Backend::redirect('backend/auth/signin');
}
}
================================================
FILE: modules/backend/controllers/Files.php
================================================
findFileObject($code)->output();
}
catch (ForbiddenException $ex) {
throw $ex;
}
catch (Exception $ex) {
}
return Response::make(View::make('backend::404'), 404);
}
/**
* thumb will output a thumbnail, or fall back on the 404 page
*/
public function thumb($code = null, $width = 100, $height = 100, $mode = 'auto', $extension = 'auto')
{
try {
return $this->findFileObject($code)->outputThumb(
$width,
$height,
compact('mode', 'extension')
);
}
catch (ForbiddenException $ex) {
throw $ex;
}
catch (Exception $ex) {
}
return Response::make(View::make('backend::404'), 404);
}
/**
* getTemporaryUrl attempts to return a redirect to a temporary URL to the asset instead
* of streaming the asset - if supported
*
* @param System|Models\File $file
* @param string|null $path Optional, defaults to the getDiskPath() of the file
* @return string|null
*/
protected static function getTemporaryUrl($file, $path = null)
{
// Get the disk and adapter used
$url = null;
$disk = $file->getDisk();
$adapter = $disk->getAdapter();
if (class_exists('\League\Flysystem\Cached\CachedAdapter') && $adapter instanceof \League\Flysystem\Cached\CachedAdapter) {
$adapter = $adapter->getAdapter();
}
if (
(class_exists('\League\Flysystem\AwsS3v3\AwsS3V3Adapter') && $adapter instanceof \League\Flysystem\AwsS3v3\AwsS3V3Adapter) ||
(class_exists('\League\Flysystem\Rackspace\RackspaceAdapter') && $adapter instanceof \League\Flysystem\Rackspace\RackspaceAdapter) ||
method_exists($adapter, 'getTemporaryUrl')
) {
if (empty($path)) {
$path = $file->getDiskPath();
}
// Check to see if the URL has already been generated
$pathKey = 'backend.file:' . $path;
$url = Cache::get($pathKey);
// The AWS S3 storage drivers will return a valid temporary URL
// even if the file does not exist
if (!$url && $disk->exists($path)) {
$expires = now()->addSeconds(Config::get('filesystems.disks.uploads.ttl', 3600));
$url = Cache::remember($pathKey, $expires, function () use ($disk, $path, $expires) {
return $disk->temporaryUrl($path, $expires);
});
}
}
return $url;
}
/**
* getDownloadUrl returns the URL for downloading a system file.
* @param $file System\Models\File
* @return string
*/
public static function getDownloadUrl($file)
{
if ($url = static::getTemporaryUrl($file)) {
return $url;
}
return Backend::url('backend/files/get/' . self::getUniqueCode($file));
}
/**
* Returns the URL for downloading a system file.
* @param $file System\Models\File
* @param $width int
* @param $height int
* @param $options array
* @return string
*/
public static function getThumbUrl($file, $width, $height, $options)
{
if ($url = static::getTemporaryUrl($file, $file->getDiskPath($file->getThumbFilename($width, $height, $options)))) {
return $url;
}
return Backend::url('backend/files/thumb/' . self::getUniqueCode($file)) . '/' . $width . '/' . $height . '/' . $options['mode'] . '/' . $options['extension'];
}
/**
* Returns a unique code used for masking the file identifier.
* @param $file System\Models\File
* @return string
*/
public static function getUniqueCode($file)
{
if (!$file) {
return null;
}
$hash = md5($file->file_name . '!' . $file->disk_name);
return base64_encode($file->id . '!' . $hash);
}
/**
* findFileObject locates a file model based on the unique code.
* @param $code string
* @return System\Models\File
*/
protected function findFileObject($code)
{
if (!$code) {
throw new ApplicationException('Missing code');
}
$parts = explode('!', base64_decode($code));
if (count($parts) < 2) {
throw new ApplicationException('Invalid code');
}
[$id, $hash] = $parts;
if (!$file = FileModel::find((int) $id)) {
throw new ApplicationException('File not found');
}
// Ensure that the file model utilized for this request is
// the one specified in the relationship configuration
if ($file->attachment) {
$fileModel = $file->attachment->{$file->field}()->getRelated();
// Only attempt to get file model through its assigned class
// when the assigned class differs from the default one that
// the file has already been loaded from
if (get_class($file) !== get_class($fileModel)) {
$file = $fileModel->find($file->id);
}
}
$verifyCode = self::getUniqueCode($file);
if ($code != $verifyCode) {
throw new ApplicationException('Invalid hash');
}
/**
* @event backend.files.get
* Fires before a file is output.
*
* Example usage:
*
* Event::listen('backend.files.get', function ((\System\Models\File) $file) {
* // Block access to this file
* return false;
*
* // Or signal access denied
* throw new \ForbiddenException;
* });
*
*/
$response = Event::fire('backend.files.get', [$file], true);
if ($response === false) {
throw new ApplicationException('File access halted');
}
return $file;
}
}
================================================
FILE: modules/backend/controllers/Index.php
================================================
url);
}
return Response::make(View::make('backend::404'), 404);
}
}
================================================
FILE: modules/backend/controllers/Preferences.php
================================================
addCss('/modules/backend/formwidgets/codeeditor/assets/css/codeeditor.css');
$this->addJs('/modules/backend/formwidgets/codeeditor/assets/js/build-min.js');
$this->addJs('/modules/backend/assets/js/preferences/preferences.js');
BackendMenu::setContext('October.System', 'system', 'mysettings');
SettingsManager::setContext('October.Backend', 'preferences');
}
/**
* index
*/
public function index()
{
$this->pageTitle = "Backend Preferences";
$this->asExtension('FormController')->update();
}
/**
* formExtendFields removes the code editor tab if there is no permission.
*/
public function formExtendFields($form)
{
if (!$this->user->hasAccess('preferences.code_editor')) {
$form->removeTab('Code Editor');
}
}
/**
* index_onSave
*/
public function index_onSave()
{
return $this->asExtension('FormController')->update_onSave();
}
/**
* index_onResetDefault
*/
public function index_onResetDefault()
{
$model = $this->formFindModelObject();
$model->resetDefault();
Flash::success(Lang::get('backend::lang.form.reset_success'));
return Backend::redirect('backend/preferences');
}
/**
* formFindModelObject
*/
public function formFindModelObject()
{
return PreferenceModel::instance();
}
}
================================================
FILE: modules/backend/controllers/UserGroups.php
================================================
formFindModelObject($roleId)) {
BackendAuth::impersonateRole($role);
}
return Backend::redirect('');
}
/**
* listExtendQuery
*/
public function listExtendQuery($query)
{
$this->applyRankPermissionsToQuery($query);
}
/**
* formExtendQuery
*/
public function formExtendQuery($query)
{
$this->applyRankPermissionsToQuery($query);
}
/**
* applyRankPermissionsToQuery
*/
protected function applyRankPermissionsToQuery($query)
{
// Super users have no restrictions
if ($this->user->isSuperUser()) {
return;
}
// Fetch user role, including impersonation
$userRole = $this->user->getRoleImpersonation() ?: $this->user->role;
// User has no role and therefore cannot manage roles
if (!$userRole || !$userRole->sort_order) {
$query->whereRaw('1 = 2');
return;
}
$query->where(
'sort_order',
$this->allowPeerManagement() ? '>=' : '>',
$userRole->sort_order
);
}
/**
* allowPeerManagement returns true if users can manage other peers
*/
public function allowPeerManagement(): bool
{
return Config::get('backend.user_peer_management', false);
}
}
================================================
FILE: modules/backend/controllers/Users.php
================================================
isMyAccount()) {
$this->requiredPermissions = null;
}
}
/**
* index controller action
* @return void
*/
public function index()
{
$this->bodyClass = 'slim-container';
return $this->asExtension('ListController')->index();
}
/**
* formExtendFields adds available permission fields to the User form.
* Mark default groups as checked for new Users.
*/
public function formExtendFields($form)
{
// Remove permissions on own account
if ($this->isMyAccount()) {
$form->removeField('role');
$form->removeField('permissions');
return;
}
// Add super user flag
if ($this->user->isSuperUser()) {
$form->addField('is_superuser')
->context(['create', 'update'])
->tab('backend::lang.user.permissions')
->label('backend::lang.user.superuser')
->comment('backend::lang.user.superuser_comment')
->displayAs('switch');
}
// Manage other admins
if ($form->getContext() !== 'create' && !BackendAuth::userHasAccess('admins.manage.other_admins')) {
$form->removeField('password');
$form->removeField('password_confirmation');
$form->getField('email')->disabled();
}
// Filter the role options to those below rank
if (!$this->user->isSuperUser()) {
$form->getField('role')->options(function() {
return $this->getRankedRoleOptions();
});
}
// Mark default groups
if (!$form->model->exists) {
$defaultGroupIds = UserGroup::where('is_new_user_default', true)->pluck('id')->all();
if ($groupField = $form->getField('groups')) {
$groupField->value($defaultGroupIds);
}
}
}
/**
* listExtendQuery extends the list query to hide superusers if the current user is not a superuser themselves
*/
public function listExtendQuery($query)
{
$this->applyRankPermissionsToQuery($query);
}
/**
* listFilterExtendScopes prevents non-superusers from even seeing the is_superuser filter
*/
public function listFilterExtendScopes($filterWidget)
{
if (!$this->user->isSuperUser()) {
$filterWidget->removeScope('is_superuser');
}
}
/**
* listInjectRowClass strikes out deleted records
*/
public function listInjectRowClass($record, $definition = null)
{
if ($record->trashed()) {
return 'strike';
}
}
/**
* formExtendModel
*/
public function formExtendModel($model)
{
if (!$this->user->isSuperUser() && !$this->isMyAccount()) {
$model->addValidationRule('role', 'required');
}
}
/**
* formExtendQuery extends the form query to prevent non-superusers from accessing superusers at all
*/
public function formExtendQuery($query)
{
$this->applyRankPermissionsToQuery($query);
// Ensure soft-deleted records can still be managed
$query->withTrashed();
}
/**
* formBeforeSave
*/
public function formBeforeSave($model)
{
// Prevent outranked roles from being selected
if (
!$this->user->isSuperUser() &&
($role = UserRole::find(post('User[role]')))
) {
if (
!$this->user->role ||
($this->allowPeerManagement()
? $role->sort_order < $this->user->role->sort_order
: $role->sort_order <= $this->user->role->sort_order)
) {
throw new ForbiddenException;
}
}
}
/**
* formBeforeCreate
*/
public function formBeforeCreate($model)
{
// In production, we assume the user creating the new user is not the
// new user so password must always be reset for security reasons
if (!System::checkDebugMode()) {
$model->is_password_expired = true;
}
}
/**
* getRoleOptions returns available role options
*/
protected function getRankedRoleOptions()
{
$user = BackendAuth::getUser();
if (!$user || !$user->role || !$user->role->sort_order) {
return [];
}
$roles = UserRole::where(
'sort_order',
$this->allowPeerManagement() ? '>=' : '>',
$user->role->sort_order
)->get();
$result = [];
foreach ($roles as $role) {
$result[$role->id] = [$role->name, $role->description];
}
return $result;
}
/**
* applyRankPermissionsToQuery
*/
protected function applyRankPermissionsToQuery($query)
{
// Super users have no restrictions
if ($this->user->isSuperUser()) {
return;
}
// Hide super users
$query->where('is_superuser', false);
// Hide users above rank, not including self
$query->where(function($q) {
$q->where('id', $this->user->id);
if ($this->user->role && $this->user->role->sort_order) {
$q->orWhereHas('role', function($q) {
$q->where(
'sort_order',
$this->allowPeerManagement() ? '>=' : '>',
$this->user->role->sort_order
);
});
}
});
}
/**
* update controller
*/
public function update($recordId, $context = null)
{
// Users cannot edit themselves, only use My Settings
if (!$this->isMyAccount() && $recordId == $this->user->id) {
return Backend::redirect('backend/users/myaccount');
}
return $this->asExtension('FormController')->update($recordId, $context);
}
/**
* update_onRestore handles restoring users
*/
public function update_onRestore($recordId)
{
$this->formFindModelObject($recordId)->restore();
Flash::success(Lang::get('backend::lang.form.restore_success', ['name' => Lang::get('backend::lang.user.name')]));
return Redirect::refresh();
}
/**
* myaccount controller
*/
public function myaccount()
{
$this->pageTitle = "My Account";
return $this->update($this->user->id, 'myaccount');
}
/**
* myaccount_onSave proxies the update onSave event
*/
public function myaccount_onSave()
{
$result = $this->asExtension('FormController')->update_onSave($this->user->id, 'myaccount');
// If the password or login name has been updated, reauthenticate the user
$loginChanged = $this->user->login != post('User[login]');
$passwordChanged = strlen(post('User[password]'));
if ($loginChanged || $passwordChanged) {
// Determine remember policy
$remember = Config::get('backend.force_remember');
if ($remember === null) {
$remember = BackendAuth::hasRemember();
}
BackendAuth::login($this->user->reload(), (bool) $remember);
}
return $result;
}
/**
* allowPeerManagement returns true if users can manage other peers
*/
protected function allowPeerManagement(): bool
{
return Config::get('backend.user_peer_management', false);
}
/**
* isMyAccount returns true if self managing
*/
protected function isMyAccount(): bool
{
return $this->action == 'myaccount';
}
}
================================================
FILE: modules/backend/controllers/accesslogs/_hint.php
================================================
= __('This log displays a list of successful sign in attempts by administrators. Records are kept for a total of :days days.', ['days' => 60]) ?>