Page MenuHomePhorge

No OneTemporary

Size
320 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/plugins/enigma/skins/elastic/templates/keys.html b/plugins/enigma/skins/elastic/templates/keys.html
index a57279523..fcef199b1 100644
--- a/plugins/enigma/skins/elastic/templates/keys.html
+++ b/plugins/enigma/skins/elastic/templates/keys.html
@@ -1,84 +1,84 @@
<roundcube:include file="includes/layout.html" />
<roundcube:include file="includes/menu.html" />
<roundcube:include file="includes/settings-menu.html" />
<h1 class="voice"><roundcube:label name="settings" /> : <roundcube:label name="enigma.enigmakeys" /></h1>
<!-- keys list -->
<div class="list listbox selected" aria-labelledby="aria-label-enigmakeyslist">
<div class="header">
<a class="button icon menu-button" href="#menu"><span class="inner"><roundcube:label name="menu" /></span></a>
<a class="button icon back-sidebar-button" href="#sidebar"><span class="inner"><roundcube:label name="settings" /></span></a>
<span id="aria-label-enigmakeyslist" class="header-title"><roundcube:label name="enigma.enigmakeys" /></span>
- <roundcube:object name="searchform" id="keysearch" wrapper="searchbar toolbar"
- label="keysearchform" buttontitle="findkeys" ariatag="h2" />
<a class="button icon toolbar-menu-button" href="#list-menu"><span class="inner"><roundcube:label name="menu" /></span></a>
</div>
- <div class="pagenav toolbar" role="toolbar">
+ <roundcube:object name="searchform" id="keysearch" wrapper="searchbar toolbar"
+ label="keysearchform" buttontitle="findkeys" ariatag="h2" />
+ <div class="scroller">
+ <roundcube:object name="keyslist" id="keys-table" class="listing" role="listbox" noheader="true" data-list="keys_list" />
+ </div>
+ <div class="pagenav footer toolbar small" role="toolbar">
<roundcube:button command="firstpage" type="link"
class="button firstpage disabled" classAct="button firstpage"
title="firstpage" label="first" innerclass="inner" />
<roundcube:button command="previouspage" type="link"
class="button prevpage disabled" classAct="button prevpage"
title="previouspage" label="previous" innerclass="inner" />
<span class="pagenav-text" aria-live="polite" aria-relevant="text">
<roundcube:object name="countdisplay" />
</span>
<roundcube:endif />
<roundcube:button command="nextpage" type="link"
class="button nextpage disabled" classAct="button nextpage"
title="nextpage" label="next" innerclass="inner" />
<roundcube:button command="lastpage" type="link"
class="button lastpage disabled" classAct="button lastpage"
title="lastpage" label="last" innerclass="inner" />
</div>
- <div class="scroller">
- <roundcube:object name="keyslist" id="keys-table" class="listing" role="listbox" noheader="true" data-list="keys_list" />
- </div>
</div>
<!-- key info frame -->
<div class="content" role="main">
<h2 id="aria-label-toolbar" class="voice"><roundcube:label name="arialabeltoolbar" /></h2>
<div class="header" role="toolbar" aria-labelledby="aria-label-toolbar">
<a class="button icon back-list-button" href="#back"><span class="inner"><roundcube:label name="back" /></span></a>
<span class="header-title"></span>
<!-- toolbar -->
<div id="folderstoolbar" class="toolbar">
<roundcube:button command="plugin.enigma-key-create" type="link"
class="button create disabled" classAct="button create"
label="create" title="enigma.createkeys" innerClass="inner" />
<roundcube:button command="plugin.enigma-key-delete" type="link"
class="button delete disabled" classAct="button delete"
label="delete" title="enigma.keyremove" innerClass="inner" />
<span class="spacer"></span>
<roundcube:button command="plugin.enigma-key-import-search" type="link"
class="button search disabled" classAct="button search"
label="search" title="enigma.keyimportsearchlabel" innerClass="inner" />
<roundcube:button command="plugin.enigma-key-import" type="link"
class="button import disabled" classAct="button import"
label="import" title="enigma.importkeys" innerClass="inner" />
<span class="dropbutton">
<roundcube:button command="plugin.enigma-key-export" type="link"
class="button export disabled" classAct="button export"
label="export" title="enigma.exportkeys" innerclass="inner" />
<a href="#export" class="button dropdown" data-popup="export-menu">
<span class="inner"><roundcube:label name="enigma.arialabelkeyexportoptions" /></span>
</a>
</span>
</div>
</div>
<div class="iframe-wrapper">
<roundcube:object name="keyframe" id="keyframe" src="/watermark.html" />
</div>
</div>
<div id="export-menu" class="popupmenu">
<h3 id="aria-label-exportmenu" class="voice"><roundcube:label name="enigma.arialabelkeyexportoptions" /></h3>
<ul class="toolbarmenu listing" role="menu" aria-labelledby="aria-label-export-menu">
<roundcube:button type="link-menuitem" command="plugin.enigma-key-export" label="exportall" prop="sub" class="export all" classAct="export all active" />
<roundcube:button type="link-menuitem" command="plugin.enigma-key-export-selected" label="exportsel" prop="sub" class="export selection" classAct="export selection active" />
</ul>
</div>
<roundcube:include file="includes/footer.html" />
diff --git a/program/include/rcmail_output_html.php b/program/include/rcmail_output_html.php
index 1dd4838da..3a83a53ca 100644
--- a/program/include/rcmail_output_html.php
+++ b/program/include/rcmail_output_html.php
@@ -1,2254 +1,2254 @@
<?php
/**
+-----------------------------------------------------------------------+
| program/include/rcmail_output_html.php |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2006-2014, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Class to handle HTML page output using a skin template. |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Class to create HTML page output using a skin template
*
* @package Webmail
* @subpackage View
*/
class rcmail_output_html extends rcmail_output
{
public $type = 'html';
protected $message;
protected $template_name;
protected $objects = array();
protected $js_env = array();
protected $js_labels = array();
protected $js_commands = array();
protected $skin_paths = array();
protected $scripts_path = '';
protected $script_files = array();
protected $css_files = array();
protected $scripts = array();
protected $header = '';
protected $footer = '';
protected $body = '';
protected $base_path = '';
protected $assets_path;
protected $assets_dir = RCUBE_INSTALL_PATH;
protected $devel_mode = false;
protected $default_template = "<html>\n<head><title></title></head>\n<body></body>\n</html>";
// deprecated names of templates used before 0.5
protected $deprecated_templates = array(
'contact' => 'showcontact',
'contactadd' => 'addcontact',
'contactedit' => 'editcontact',
'identityedit' => 'editidentity',
'messageprint' => 'printmessage',
);
/**
* Constructor
*/
public function __construct($task = null, $framed = false)
{
parent::__construct();
$this->devel_mode = $this->config->get('devel_mode');
$this->set_env('task', $task);
$this->set_env('standard_windows', (bool) $this->config->get('standard_windows'));
$this->set_env('locale', $_SESSION['language']);
$this->set_env('devel_mode', $this->devel_mode);
// Version number e.g. 1.4.2 will be 10402
$version = explode('.', preg_replace('/[^0-9.].*/', '', RCMAIL_VERSION));
$this->set_env('rcversion', $version[0] * 10000 + $version[1] * 100 + $version[2]);
// add cookie info
$this->set_env('cookie_domain', ini_get('session.cookie_domain'));
$this->set_env('cookie_path', ini_get('session.cookie_path'));
$this->set_env('cookie_secure', filter_var(ini_get('session.cookie_secure'), FILTER_VALIDATE_BOOLEAN));
// load and setup the skin
$this->set_skin($this->config->get('skin'));
$this->set_assets_path($this->config->get('assets_path'), $this->config->get('assets_dir'));
if (!empty($_REQUEST['_extwin']))
$this->set_env('extwin', 1);
if ($this->framed || $framed)
$this->set_env('framed', 1);
$lic = <<<EOF
/*
@licstart The following is the entire license notice for the
JavaScript code in this page.
Copyright (C) 2005-2014 The Roundcube Dev Team
The JavaScript code in this page is free software: you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
The code is distributed WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU GPL for more details.
@licend The above is the entire license notice
for the JavaScript code in this page.
*/
EOF;
// add common javascripts
$this->add_script($lic, 'head_top');
$this->add_script('var '.self::JS_OBJECT_NAME.' = new rcube_webmail();', 'head_top');
// don't wait for page onload. Call init at the bottom of the page (delayed)
$this->add_script(self::JS_OBJECT_NAME.'.init();', 'docready');
$this->scripts_path = 'program/js/';
$this->include_script('jquery.min.js');
$this->include_script('common.js');
$this->include_script('app.js');
// register common UI objects
$this->add_handlers(array(
'loginform' => array($this, 'login_form'),
'preloader' => array($this, 'preloader'),
'username' => array($this, 'current_username'),
'message' => array($this, 'message_container'),
'charsetselector' => array($this, 'charset_selector'),
'aboutcontent' => array($this, 'about_content'),
));
}
/**
* Set environment variable
*
* @param string $name Property name
* @param mixed $value Property value
* @param boolean $addtojs True if this property should be added
* to client environment
*/
public function set_env($name, $value, $addtojs = true)
{
$this->env[$name] = $value;
if ($addtojs || isset($this->js_env[$name])) {
$this->js_env[$name] = $value;
}
}
/**
* Parse and set assets path
*
* @param string $path Assets path URL (relative or absolute)
* @param string $fs_dif Assets path in filesystem
*/
public function set_assets_path($path, $fs_dir = null)
{
if (empty($path)) {
return;
}
$path = rtrim($path, '/') . '/';
// handle relative assets path
if (!preg_match('|^https?://|', $path) && $path[0] != '/') {
// save the path to search for asset files later
$this->assets_dir = $path;
$base = preg_replace('/[?#&].*$/', '', $_SERVER['REQUEST_URI']);
$base = rtrim($base, '/');
// remove url token if exists
if ($len = intval($this->config->get('use_secure_urls'))) {
$_base = explode('/', $base);
$last = count($_base) - 1;
$length = $len > 1 ? $len : 16; // as in rcube::get_secure_url_token()
// we can't use real token here because it
// does not exists in unauthenticated state,
// hope this will not produce false-positive matches
if ($last > -1 && preg_match('/^[a-f0-9]{' . $length . '}$/', $_base[$last])) {
$path = '../' . $path;
}
}
}
// set filesystem path for assets
if ($fs_dir) {
if ($fs_dir[0] != '/') {
$fs_dir = realpath(RCUBE_INSTALL_PATH . $fs_dir);
}
// ensure the path ends with a slash
$this->assets_dir = rtrim($fs_dir, '/') . '/';
}
$this->assets_path = $path;
$this->set_env('assets_path', $path);
}
/**
* Getter for the current page title
*
* @return string The page title
*/
protected function get_pagetitle()
{
if (!empty($this->pagetitle)) {
$title = $this->pagetitle;
}
else if ($this->env['task'] == 'login') {
$title = $this->app->gettext(array(
'name' => 'welcome',
'vars' => array('product' => $this->config->get('product_name')
)));
}
else {
$title = ucfirst($this->env['task']);
}
return $title;
}
/**
* Set skin
*
* @param string $skin Skin name
*/
public function set_skin($skin)
{
if (!$this->check_skin($skin)) {
$skin = rcube_config::DEFAULT_SKIN;
}
$skin_path = 'skins/' . $skin;
$this->config->set('skin_path', $skin_path);
$this->base_path = $skin_path;
// register skin path(s)
$this->skin_paths = array();
$this->load_skin($skin_path);
$this->set_env('skin', $skin);
}
/**
* Check skin validity/existence
*
* @param string $skin Skin name
*
* @return bool True if the skin exist and is readable, False otherwise
*/
public function check_skin($skin)
{
// Sanity check to prevent from path traversal vulnerability (#1490620)
if (strpos($skin, '/') !== false || strpos($skin, "\\") !== false) {
rcube::raise_error(array(
'file' => __FILE__,
'line' => __LINE__,
'message' => 'Invalid skin name'
), true, false);
return false;
}
$path = RCUBE_INSTALL_PATH . 'skins/';
return !empty($skin) && is_dir($path . $skin) && is_readable($path . $skin);
}
/**
* Helper method to recursively read skin meta files and register search paths
*/
private function load_skin($skin_path)
{
$this->skin_paths[] = $skin_path;
// read meta file and check for dependencies
$meta = @file_get_contents(RCUBE_INSTALL_PATH . $skin_path . '/meta.json');
$meta = @json_decode($meta, true);
$meta['path'] = $skin_path;
$path_elements = explode('/', $skin_path);
$skin_id = end($path_elements);
if (!$meta['name']) {
$meta['name'] = $skin_id;
}
$this->skins[$skin_id] = $meta;
if ($meta['extends']) {
$path = RCUBE_INSTALL_PATH . 'skins/';
if (is_dir($path . $meta['extends']) && is_readable($path . $meta['extends'])) {
$this->load_skin('skins/' . $meta['extends']);
}
}
foreach ((array) $meta['config'] as $key => $value) {
$this->config->set($key, $value, true);
}
if (!empty($meta['config'])) {
$value = array_merge((array) $this->config->get('dont_override'), array_keys($meta['config']));
$this->config->set('dont_override', $value, true);
}
}
/**
* Check if a specific template exists
*
* @param string $name Template name
*
* @return boolean True if template exists
*/
public function template_exists($name)
{
foreach ($this->skin_paths as $skin_path) {
$filename = RCUBE_INSTALL_PATH . $skin_path . '/templates/' . $name . '.html';
if ((is_file($filename) && is_readable($filename))
|| ($this->deprecated_templates[$name] && $this->template_exists($this->deprecated_templates[$name]))
) {
return true;
}
}
return false;
}
/**
* Find the given file in the current skin path stack
*
* @param string $file File name/path to resolve (starting with /)
* @param string &$skin_path Reference to the base path of the matching skin
* @param string $add_path Additional path to search in
*
* @return mixed Relative path to the requested file or False if not found
*/
public function get_skin_file($file, &$skin_path = null, $add_path = null)
{
$skin_paths = $this->skin_paths;
if ($add_path) {
array_unshift($skin_paths, $add_path);
$skin_paths = array_unique($skin_paths);
}
if ($skin_path = $this->find_file_path($file, $skin_paths)) {
return $skin_path . $file;
}
return false;
}
/**
* Find path of the asset file
*/
protected function find_file_path($file, $skin_paths)
{
foreach ($skin_paths as $skin_path) {
if ($this->assets_dir != RCUBE_INSTALL_PATH) {
if (realpath($this->assets_dir . $skin_path . $file)) {
return $skin_path;
}
}
if (realpath(RCUBE_INSTALL_PATH . $skin_path . $file)) {
return $skin_path;
}
}
}
/**
* Register a GUI object to the client script
*
* @param string $obj Object name
* @param string $id Object ID
*/
public function add_gui_object($obj, $id)
{
$this->add_script(self::JS_OBJECT_NAME.".gui_object('$obj', '$id');");
}
/**
* Call a client method
*
* @param string Method to call
* @param ... Additional arguments
*/
public function command()
{
$cmd = func_get_args();
if (strpos($cmd[0], 'plugin.') !== false)
$this->js_commands[] = array('triggerEvent', $cmd[0], $cmd[1]);
else
$this->js_commands[] = $cmd;
}
/**
* Add a localized label to the client environment
*/
public function add_label()
{
$args = func_get_args();
if (count($args) == 1 && is_array($args[0])) {
$args = $args[0];
}
foreach ($args as $name) {
$this->js_labels[$name] = $this->app->gettext($name);
}
}
/**
* Invoke display_message command
*
* @param string $message Message to display
* @param string $type Message type [notice|confirm|error]
* @param array $vars Key-value pairs to be replaced in localized text
* @param boolean $override Override last set message
* @param int $timeout Message display time in seconds
*
* @uses self::command()
*/
public function show_message($message, $type='notice', $vars=null, $override=true, $timeout=0)
{
if ($override || !$this->message) {
if ($this->app->text_exists($message)) {
if (!empty($vars))
$vars = array_map(array('rcube','Q'), $vars);
$msgtext = $this->app->gettext(array('name' => $message, 'vars' => $vars));
}
else
$msgtext = $message;
$this->message = $message;
$this->command('display_message', $msgtext, $type, $timeout * 1000);
}
}
/**
* Delete all stored env variables and commands
*
* @param bool $all Reset all env variables (including internal)
*/
public function reset($all = false)
{
$framed = $this->framed;
$env = $all ? null : array_intersect_key($this->env, array('extwin'=>1, 'framed'=>1));
parent::reset();
// let some env variables survive
$this->env = $this->js_env = $env;
$this->framed = $framed || $this->env['framed'];
$this->js_labels = array();
$this->js_commands = array();
$this->script_files = array();
$this->scripts = array();
$this->header = '';
$this->footer = '';
$this->body = '';
// load defaults
if (!$all) {
$this->__construct();
}
}
/**
* Redirect to a certain url
*
* @param mixed $p Either a string with the action or url parameters as key-value pairs
* @param int $delay Delay in seconds
* @param bool $secure Redirect to secure location (see rcmail::url())
*/
public function redirect($p = array(), $delay = 1, $secure = false)
{
if ($this->env['extwin'])
$p['extwin'] = 1;
$location = $this->app->url($p, false, false, $secure);
header('Location: ' . $location);
exit;
}
/**
* Send the request output to the client.
* This will either parse a skin tempalte or send an AJAX response
*
* @param string $templ Template name
* @param boolean $exit True if script should terminate (default)
*/
public function send($templ = null, $exit = true)
{
if ($templ != 'iframe') {
// prevent from endless loops
if ($exit != 'recur' && $this->app->plugins->is_processing('render_page')) {
rcube::raise_error(array('code' => 505, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => 'Recursion alert: ignoring output->send()'), true, false);
return;
}
$this->parse($templ, false);
}
else {
$this->framed = true;
$this->write();
}
// set output asap
ob_flush();
flush();
if ($exit) {
exit;
}
}
/**
* Process template and write to stdOut
*
* @param string $template HTML template content
*/
public function write($template = '')
{
if (!empty($this->script_files)) {
$this->set_env('request_token', $this->app->get_request_token());
}
$commands = $this->get_js_commands($framed);
// if all js commands go to parent window we can ignore all
// script files and skip rcube_webmail initialization (#1489792)
if ($framed) {
$this->scripts = array();
$this->script_files = array();
$this->header = '';
$this->footer = '';
}
// write all javascript commands
if (!empty($commands)) {
$this->add_script($commands, 'head_top');
}
// allow (legal) iframe content to be loaded
$iframe = $this->framed || $this->env['framed'];
if (!headers_sent() && $iframe && ($xopt = $this->app->config->get('x_frame_options', 'sameorigin'))) {
if (strtolower($xopt) != 'sameorigin') {
header('X-Frame-Options: sameorigin', true);
}
}
// call super method
$this->_write($template, $this->config->get('skin_path'));
}
/**
* Parse a specific skin template and deliver to stdout (or return)
*
* @param string $name Template name
* @param boolean $exit Exit script
* @param boolean $write Don't write to stdout, return parsed content instead
*
* @link http://php.net/manual/en/function.exit.php
*/
function parse($name = 'main', $exit = true, $write = true)
{
$plugin = false;
$realname = $name;
$plugin_skin_paths = array();
$this->template_name = $realname;
$temp = explode('.', $name, 2);
if (count($temp) > 1) {
$plugin = $temp[0];
$name = $temp[1];
$skin_dir = $plugin . '/skins/' . $this->config->get('skin');
// apply skin search escalation list to plugin directory
foreach ($this->skin_paths as $skin_path) {
$plugin_skin_paths[] = $this->app->plugins->url . $plugin . '/' . $skin_path;
}
// add fallback to default skin
if (is_dir($this->app->plugins->dir . $plugin . '/skins/default')) {
$skin_dir = $plugin . '/skins/default';
$plugin_skin_paths[] = $this->app->plugins->url . $skin_dir;
}
// prepend plugin skin paths to search list
$this->skin_paths = array_merge($plugin_skin_paths, $this->skin_paths);
}
// find skin template
$path = false;
foreach ($this->skin_paths as $skin_path) {
$path = RCUBE_INSTALL_PATH . "$skin_path/templates/$name.html";
// fallback to deprecated template names
if (!is_readable($path) && ($dname = $this->deprecated_templates[$realname])) {
$path = RCUBE_INSTALL_PATH . "$skin_path/templates/$dname.html";
if (is_readable($path)) {
rcube::raise_error(array(
'code' => 502, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Using deprecated template '$dname' in $skin_path/templates. Please rename to '$realname'"
), true, false);
}
}
if (is_readable($path)) {
$this->config->set('skin_path', $skin_path);
// set base_path to core skin directory (not plugin's skin)
$this->base_path = preg_replace('!plugins/\w+/!', '', $skin_path);
$skin_dir = preg_replace('!^plugins/!', '', $skin_path);
break;
}
else {
$path = false;
}
}
// read template file
if (!$path || ($templ = @file_get_contents($path)) === false) {
rcube::raise_error(array(
'code' => 404,
'type' => 'php',
'line' => __LINE__,
'file' => __FILE__,
'message' => 'Error loading template for '.$realname
), true, $write);
$this->skin_paths = array_slice($this->skin_paths, count($plugin_skin_paths));
return false;
}
// replace all path references to plugins/... with the configured plugins dir
// and /this/ to the current plugin skin directory
if ($plugin) {
$templ = preg_replace(
array('/\bplugins\//', '/(["\']?)\/this\//'),
array($this->app->plugins->url, '\\1'.$this->app->plugins->url.$skin_dir.'/'),
$templ
);
}
// parse for specialtags
$output = $this->parse_conditions($templ);
$output = $this->parse_xml($output);
// trigger generic hook where plugins can put additional content to the page
$hook = $this->app->plugins->exec_hook("render_page", array(
'template' => $realname, 'content' => $output, 'write' => $write));
// save some memory
$output = $hook['content'];
unset($hook['content']);
// remove plugin skin paths from current context
$this->skin_paths = array_slice($this->skin_paths, count($plugin_skin_paths));
if (!$write) {
return $this->postrender($output);
}
$this->write(trim($output));
if ($exit) {
exit;
}
}
/**
* Return executable javascript code for all registered commands
*/
protected function get_js_commands(&$framed = null)
{
$out = '';
$parent_commands = 0;
$top_commands = array();
// these should be always on top,
// e.g. hide_message() below depends on env.framed
if (!$this->framed && !empty($this->js_env)) {
$top_commands[] = array('set_env', $this->js_env);
}
if (!empty($this->js_labels)) {
$top_commands[] = array('add_label', $this->js_labels);
}
// unlock interface after iframe load
$unlock = preg_replace('/[^a-z0-9]/i', '', $_REQUEST['_unlock']);
if ($this->framed) {
$top_commands[] = array('iframe_loaded', $unlock);
}
else if ($unlock) {
$top_commands[] = array('hide_message', $unlock);
}
$commands = array_merge($top_commands, $this->js_commands);
foreach ($commands as $i => $args) {
$method = array_shift($args);
$parent = $this->framed || preg_match('/^parent\./', $method);
foreach ($args as $i => $arg) {
$args[$i] = self::json_serialize($arg, $this->devel_mode);
}
if ($parent) {
$parent_commands++;
$method = preg_replace('/^parent\./', '', $method);
$parent_prefix = 'if (window.parent && parent.' . self::JS_OBJECT_NAME . ') parent.';
$method = $parent_prefix . self::JS_OBJECT_NAME . '.' . $method;
}
else {
$method = self::JS_OBJECT_NAME . '.' . $method;
}
$out .= sprintf("%s(%s);\n", $method, implode(',', $args));
}
$framed = $parent_prefix && $parent_commands == count($commands);
// make the output more compact if all commands go to parent window
if ($framed) {
$out = "if (window.parent && parent." . self::JS_OBJECT_NAME . ") {\n"
. str_replace($parent_prefix, "\tparent.", $out)
. "}\n";
}
return $out;
}
/**
* Make URLs starting with a slash point to skin directory
*
* @param string $str Input string
* @param bool $search_path True if URL should be resolved using the current skin path stack
*
* @return string URL
*/
public function abs_url($str, $search_path = false)
{
if ($str[0] == '/') {
if ($search_path && ($file_url = $this->get_skin_file($str, $skin_path))) {
return $file_url;
}
return $this->base_path . $str;
}
return $str;
}
/**
* Show error page and terminate script execution
*
* @param int $code Error code
* @param string $message Error message
*/
public function raise_error($code, $message)
{
global $__page_content, $ERROR_CODE, $ERROR_MESSAGE;
$ERROR_CODE = $code;
$ERROR_MESSAGE = $message;
include RCUBE_INSTALL_PATH . 'program/steps/utils/error.inc';
exit;
}
/**
* Modify path by adding URL prefix if configured
*/
public function asset_url($path)
{
// iframe content can't be in a different domain
// @TODO: check if assests are on a different domain
if (!$this->assets_path || in_array($path[0], array('?', '/', '.')) || strpos($path, '://')) {
return $path;
}
return $this->assets_path . $path;
}
/***** Template parsing methods *****/
/**
* Replace all strings ($varname)
* with the content of the according global variable.
*/
protected function parse_with_globals($input)
{
$GLOBALS['__version'] = html::quote(RCMAIL_VERSION);
$GLOBALS['__comm_path'] = html::quote($this->app->comm_path);
$GLOBALS['__skin_path'] = html::quote($this->base_path);
return preg_replace_callback('/\$(__[a-z0-9_\-]+)/',
array($this, 'globals_callback'), $input);
}
/**
* Callback function for preg_replace_callback() in parse_with_globals()
*/
protected function globals_callback($matches)
{
return $GLOBALS[$matches[1]];
}
/**
* Correct absolute paths in images and other tags
* add timestamp to .js and .css filename
*/
protected function fix_paths($output)
{
return preg_replace_callback(
'!(src|href|background)=(["\']?)([a-z0-9/_.-]+)(["\'\s>])!i',
array($this, 'file_callback'), $output);
}
/**
* Callback function for preg_replace_callback in fix_paths()
*
* @return string Parsed string
*/
protected function file_callback($matches)
{
$file = $matches[3];
$file = preg_replace('!^/this/!', '/', $file);
// correct absolute paths
if ($file[0] == '/') {
$this->get_skin_file($file, $skin_path, $this->base_path);
$file = ($skin_path ?: $this->base_path) . $file;
}
// add file modification timestamp
if (preg_match('/\.(js|css)$/', $file, $m)) {
$file = $this->file_mod($file);
}
return $matches[1] . '=' . $matches[2] . $file . $matches[4];
}
/**
* Correct paths of asset files according to assets_path
*/
protected function fix_assets_paths($output)
{
return preg_replace_callback(
'!(src|href|background)=(["\']?)([a-z0-9/_.?=-]+)(["\'\s>])!i',
array($this, 'assets_callback'), $output);
}
/**
* Callback function for preg_replace_callback in fix_assets_paths()
*
* @return string Parsed string
*/
protected function assets_callback($matches)
{
$file = $this->asset_url($matches[3]);
return $matches[1] . '=' . $matches[2] . $file . $matches[4];
}
/**
* Modify file by adding mtime indicator
*/
protected function file_mod($file)
{
$fs = false;
$ext = substr($file, strrpos($file, '.') + 1);
// use minified file if exists (not in development mode)
if (!$this->devel_mode && !preg_match('/\.min\.' . $ext . '$/', $file)) {
$minified_file = substr($file, 0, strlen($ext) * -1) . 'min.' . $ext;
if ($fs = @filemtime($this->assets_dir . $minified_file)) {
return $minified_file . '?s=' . $fs;
}
}
if ($fs = @filemtime($this->assets_dir . $file)) {
$file .= '?s=' . $fs;
}
return $file;
}
/**
* Public wrapper to dipp into template parsing.
*
* @param string $input Template content
*
* @return string
* @uses rcmail_output_html::parse_xml()
* @since 0.1-rc1
*/
public function just_parse($input)
{
$input = $this->parse_conditions($input);
$input = $this->parse_xml($input);
$input = $this->postrender($input);
return $input;
}
/**
* Parse for conditional tags
*/
protected function parse_conditions($input)
{
$matches = preg_split('/<roundcube:(if|elseif|else|endif)\s+([^>]+)>\n?/is', $input, 2, PREG_SPLIT_DELIM_CAPTURE);
if ($matches && count($matches) == 4) {
if (preg_match('/^(else|endif)$/i', $matches[1])) {
return $matches[0] . $this->parse_conditions($matches[3]);
}
$attrib = html::parse_attrib_string($matches[2]);
if (isset($attrib['condition'])) {
$condmet = $this->check_condition($attrib['condition']);
$submatches = preg_split('/<roundcube:(elseif|else|endif)\s+([^>]+)>\n?/is', $matches[3], 2, PREG_SPLIT_DELIM_CAPTURE);
if ($condmet) {
$result = $submatches[0];
if ($submatches[1] != 'endif') {
$result .= preg_replace('/.*<roundcube:endif\s+[^>]+>\n?/Uis', '', $submatches[3], 1);
}
else {
$result .= $submatches[3];
}
}
else {
$result = "<roundcube:$submatches[1] $submatches[2]>" . $submatches[3];
}
return $matches[0] . $this->parse_conditions($result);
}
rcube::raise_error(array(
'code' => 500, 'line' => __LINE__, 'file' => __FILE__,
'message' => "Unable to parse conditional tag " . $matches[2]
), true, false);
}
return $input;
}
/**
* Determines if a given condition is met
*
* @param string $condition Condition statement
*
* @return boolean True if condition is met, False if not
* @todo Extend this to allow real conditions, not just "set"
*/
protected function check_condition($condition)
{
return $this->eval_expression($condition);
}
/**
* Inserts hidden field with CSRF-prevention-token into POST forms
*/
protected function alter_form_tag($matches)
{
$out = $matches[0];
$attrib = html::parse_attrib_string($matches[1]);
if (strtolower($attrib['method']) == 'post') {
$hidden = new html_hiddenfield(array('name' => '_token', 'value' => $this->app->get_request_token()));
$out .= "\n" . $hidden->show();
}
return $out;
}
/**
* Parse & evaluate a given expression and return its result.
*
* @param string $expression Expression statement
*
* @return mixed Expression result
*/
protected function eval_expression($expression)
{
$expression = preg_replace(
array(
'/session:([a-z0-9_]+)/i',
'/config:([a-z0-9_]+)(:([a-z0-9_]+))?/i',
'/env:([a-z0-9_]+)/i',
'/request:([a-z0-9_]+)/i',
'/cookie:([a-z0-9_]+)/i',
'/browser:([a-z0-9_]+)/i',
'/template:name/i',
),
array(
"\$_SESSION['\\1']",
"\$this->app->config->get('\\1',rcube_utils::get_boolean('\\3'))",
"\$this->env['\\1']",
"rcube_utils::get_input_value('\\1', rcube_utils::INPUT_GPC)",
"\$_COOKIE['\\1']",
"\$this->browser->{'\\1'}",
"'{$this->template_name}'",
),
$expression
);
// Note: We used create_function() before but it's deprecated in PHP 7.2
// and really it was just a wrapper on eval().
return eval("return ($expression);");
}
/**
* Search for special tags in input and replace them
* with the appropriate content
*
* @param string $input Input string to parse
*
* @return string Altered input string
* @todo Use DOM-parser to traverse template HTML
* @todo Maybe a cache.
*/
protected function parse_xml($input)
{
return preg_replace_callback('/<roundcube:([-_a-z]+)\s+((?:[^>]|\\\\>)+)(?<!\\\\)>/Ui', array($this, 'xml_command'), $input);
}
/**
* Callback function for parsing an xml command tag
* and turn it into real html content
*
* @param array $matches Matches array of preg_replace_callback
*
* @return string Tag/Object content
*/
protected function xml_command($matches)
{
$command = strtolower($matches[1]);
$attrib = html::parse_attrib_string($matches[2]);
// empty output if required condition is not met
if (!empty($attrib['condition']) && !$this->check_condition($attrib['condition'])) {
return '';
}
// localize title and summary attributes
if ($command != 'button' && !empty($attrib['title']) && $this->app->text_exists($attrib['title'])) {
$attrib['title'] = $this->app->gettext($attrib['title']);
}
if ($command != 'button' && !empty($attrib['summary']) && $this->app->text_exists($attrib['summary'])) {
$attrib['summary'] = $this->app->gettext($attrib['summary']);
}
// execute command
switch ($command) {
// return a button
case 'button':
if ($attrib['name'] || $attrib['command']) {
return $this->button($attrib);
}
break;
// frame
case 'frame':
return $this->frame($attrib);
break;
// show a label
case 'label':
if ($attrib['expression'])
$attrib['name'] = $this->eval_expression($attrib['expression']);
if ($attrib['name'] || $attrib['command']) {
$vars = $attrib + array('product' => $this->config->get('product_name'));
unset($vars['name'], $vars['command']);
$label = $this->app->gettext($attrib + array('vars' => $vars));
$quoting = !empty($attrib['quoting']) ? strtolower($attrib['quoting']) : (rcube_utils::get_boolean((string)$attrib['html']) ? 'no' : '');
// 'noshow' can be used in skins to define new labels
if ($attrib['noshow']) {
return '';
}
switch ($quoting) {
case 'no':
case 'raw':
break;
case 'javascript':
case 'js':
$label = rcube::JQ($label);
break;
default:
$label = html::quote($label);
break;
}
return $label;
}
break;
case 'add_label':
$this->add_label($attrib['name']);
break;
// include a file
case 'include':
if ($attrib['condition'] && !$this->check_condition($attrib['condition'])) {
break;
}
if ($attrib['file'][0] != '/') {
$attrib['file'] = '/templates/' . $attrib['file'];
}
$old_base_path = $this->base_path;
$include = '';
if (!empty($attrib['skin_path'])) {
$attrib['skinpath'] = $attrib['skin_path'];
}
if ($path = $this->get_skin_file($attrib['file'], $skin_path, $attrib['skinpath'])) {
// set base_path to core skin directory (not plugin's skin)
$this->base_path = preg_replace('!plugins/\w+/!', '', $skin_path);
$path = realpath(RCUBE_INSTALL_PATH . $path);
}
if (is_readable($path)) {
$allow_php = $this->config->get('skin_include_php');
$include = $allow_php ? $this->include_php($path) : file_get_contents($path);
$include = $this->parse_conditions($include);
$include = $this->parse_xml($include);
$include = $this->fix_paths($include);
}
$this->base_path = $old_base_path;
return $include;
case 'plugin.include':
$hook = $this->app->plugins->exec_hook("template_plugin_include", $attrib);
return $hook['content'];
// define a container block
case 'container':
if ($attrib['name'] && $attrib['id']) {
$this->command('gui_container', $attrib['name'], $attrib['id']);
// let plugins insert some content here
$hook = $this->app->plugins->exec_hook("template_container", $attrib);
return $hook['content'];
}
break;
// return code for a specific application object
case 'object':
$object = strtolower($attrib['name']);
$content = '';
// we are calling a class/method
if (($handler = $this->object_handlers[$object]) && is_array($handler)) {
if (is_callable($handler)) {
$this->prepare_object_attribs($attrib);
// We assume that objects with src attribute are internal (in most
// cases this is a watermark frame). We need this to make sure assets_path
// is added to the internal assets paths
$external = empty($attrib['src']);
$content = call_user_func($handler, $attrib);
}
}
// execute object handler function
else if (function_exists($handler)) {
$this->prepare_object_attribs($attrib);
$content = call_user_func($handler, $attrib);
}
else if ($object == 'doctype') {
$content = html::doctype($attrib['value']);
}
else if ($object == 'logo') {
$attrib += array('alt' => $this->xml_command(array('', 'object', 'name="productname"')));
if ($logo = $this->config->get('skin_logo')) {
if (is_array($logo)) {
if ($template_logo = $logo[$this->template_name]) {
$attrib['src'] = $template_logo;
}
elseif ($template_logo = $logo['*']) {
$attrib['src'] = $template_logo;
}
}
else {
$attrib['src'] = $logo;
}
}
$content = html::img($attrib);
}
else if ($object == 'productname') {
$name = $this->config->get('product_name', 'Roundcube Webmail');
$content = html::quote($name);
}
else if ($object == 'version') {
$ver = (string)RCMAIL_VERSION;
if (is_file(RCUBE_INSTALL_PATH . '.svn/entries')) {
if (preg_match('/Revision:\s(\d+)/', @shell_exec('svn info'), $regs))
$ver .= ' [SVN r'.$regs[1].']';
}
else if (is_file(RCUBE_INSTALL_PATH . '.git/index')) {
if (preg_match('/Date:\s+([^\n]+)/', @shell_exec('git log -1'), $regs)) {
if ($date = date('Ymd.Hi', strtotime($regs[1]))) {
$ver .= ' [GIT '.$date.']';
}
}
}
$content = html::quote($ver);
}
else if ($object == 'steptitle') {
$content = html::quote($this->get_pagetitle());
}
else if ($object == 'pagetitle') {
if ($this->devel_mode && !empty($_SESSION['username']))
$title = $_SESSION['username'].' :: ';
else if ($prod_name = $this->config->get('product_name'))
$title = $prod_name . ' :: ';
else
$title = '';
$title .= $this->get_pagetitle();
$content = html::quote($title);
}
// exec plugin hooks for this template object
$hook = $this->app->plugins->exec_hook("template_object_$object", $attrib + array('content' => $content));
if (strlen($hook['content']) && !empty($external)) {
$object_id = uniqid('TEMPLOBJECT:', true);
$this->objects[$object_id] = $hook['content'];
$hook['content'] = $object_id;
}
return $hook['content'];
// return <link> element
case 'link':
if ($attrib['condition'] && !$this->check_condition($attrib['condition'])) {
break;
}
unset($attrib['condition']);
return html::tag('link', $attrib);
// return code for a specified eval expression
case 'exp':
return html::quote($this->eval_expression($attrib['expression']));
// return variable
case 'var':
$var = explode(':', $attrib['name']);
$name = $var[1];
$value = '';
switch ($var[0]) {
case 'env':
$value = $this->env[$name];
break;
case 'config':
$value = $this->config->get($name);
if (is_array($value) && $value[$_SESSION['storage_host']]) {
$value = $value[$_SESSION['storage_host']];
}
break;
case 'request':
$value = rcube_utils::get_input_value($name, rcube_utils::INPUT_GPC);
break;
case 'session':
$value = $_SESSION[$name];
break;
case 'cookie':
$value = htmlspecialchars($_COOKIE[$name], ENT_COMPAT | ENT_HTML401, RCUBE_CHARSET);
break;
case 'browser':
$value = $this->browser->{$name};
break;
}
if (is_array($value)) {
$value = implode(', ', $value);
}
return html::quote($value);
case 'form':
return $this->form_tag($attrib);
}
return '';
}
/**
* Prepares template object attributes
*
* @param array &$attribs Attributes
*/
protected function prepare_object_attribs(&$attribs)
{
// Localize data-label-* attributes
array_walk($attribs, function(&$value, $key, $rcube) {
if (strpos($key, 'data-label-') === 0) {
$value = $rcube->gettext($value);
}
}, $this->app);
}
/**
* Include a specific file and return it's contents
*
* @param string $file File path
*
* @return string Contents of the processed file
*/
protected function include_php($file)
{
ob_start();
include $file;
$out = ob_get_contents();
ob_end_clean();
return $out;
}
/**
* Put objects' content back into template output
*/
protected function postrender($output)
{
// insert objects' contents
foreach ($this->objects as $key => $val) {
$output = str_replace($key, $val, $output, $count);
if ($count) {
$this->objects[$key] = null;
}
}
// make sure all <form> tags have a valid request token
$output = preg_replace_callback('/<form\s+([^>]+)>/Ui', array($this, 'alter_form_tag'), $output);
return $output;
}
/**
* Create and register a button
*
* @param array $attrib Named button attributes
*
* @return string HTML button
* @todo Remove all inline JS calls and use jQuery instead.
* @todo Remove all sprintf()'s - they are pretty, but also slow.
*/
public function button($attrib)
{
static $s_button_count = 100;
static $disabled_actions = null;
// these commands can be called directly via url
$a_static_commands = array('compose', 'list', 'preferences', 'folders', 'identities');
if (!($attrib['command'] || $attrib['name'] || $attrib['href'])) {
return '';
}
// try to find out the button type
if ($attrib['type']) {
$attrib['type'] = strtolower($attrib['type']);
if ($pos = strpos($attrib['type'], '-menuitem')) {
$attrib['type'] = substr($attrib['type'], 0, -9);
$menuitem = true;
}
}
else {
$attrib['type'] = ($attrib['image'] || $attrib['imagepas'] || $attrib['imageact']) ? 'image' : 'button';
}
$command = $attrib['command'];
$action = $command ?: $attrib['name'];
if ($attrib['task']) {
$command = $attrib['task'] . '.' . $command;
$element = $attrib['task'] . '.' . $action;
}
else {
$element = ($this->env['task'] ? $this->env['task'] . '.' : '') . $action;
}
if ($disabled_actions === null) {
$disabled_actions = (array) $this->config->get('disabled_actions');
}
// remove buttons for disabled actions
if (in_array($element, $disabled_actions) || in_array($action, $disabled_actions)) {
return '';
}
if (!$attrib['image']) {
$attrib['image'] = $attrib['imagepas'] ? $attrib['imagepas'] : $attrib['imageact'];
}
if (!$attrib['id']) {
$attrib['id'] = sprintf('rcmbtn%d', $s_button_count++);
}
// get localized text for labels and titles
if ($attrib['title']) {
$attrib['title'] = html::quote($this->app->gettext($attrib['title'], $attrib['domain']));
}
if ($attrib['label']) {
$attrib['label'] = html::quote($this->app->gettext($attrib['label'], $attrib['domain']));
}
if ($attrib['alt']) {
$attrib['alt'] = html::quote($this->app->gettext($attrib['alt'], $attrib['domain']));
}
// set accessibility attributes
if (!$attrib['role']) {
$attrib['role'] = 'button';
}
if (!empty($attrib['class']) && !empty($attrib['classact']) || !empty($attrib['imagepas']) && !empty($attrib['imageact'])) {
if (array_key_exists('tabindex', $attrib))
$attrib['data-tabindex'] = $attrib['tabindex'];
$attrib['tabindex'] = '-1'; // disable button by default
$attrib['aria-disabled'] = 'true';
}
// set title to alt attribute for IE browsers
if ($this->browser->ie && !$attrib['title'] && $attrib['alt']) {
$attrib['title'] = $attrib['alt'];
}
// add empty alt attribute for XHTML compatibility
if (!isset($attrib['alt'])) {
$attrib['alt'] = '';
}
// register button in the system
if ($attrib['command']) {
$this->add_script(sprintf(
"%s.register_button('%s', '%s', '%s', '%s', '%s', '%s');",
self::JS_OBJECT_NAME,
$command,
$attrib['id'],
$attrib['type'],
$attrib['imageact'] ? $this->abs_url($attrib['imageact']) : $attrib['classact'],
$attrib['imagesel'] ? $this->abs_url($attrib['imagesel']) : $attrib['classsel'],
$attrib['imageover'] ? $this->abs_url($attrib['imageover']) : ''
));
// make valid href to specific buttons
if (in_array($attrib['command'], rcmail::$main_tasks)) {
$attrib['href'] = $this->app->url(array('task' => $attrib['command']));
$attrib['onclick'] = sprintf("return %s.command('switch-task','%s',this,event)", self::JS_OBJECT_NAME, $attrib['command']);
}
else if ($attrib['task'] && in_array($attrib['task'], rcmail::$main_tasks)) {
$attrib['href'] = $this->app->url(array('action' => $attrib['command'], 'task' => $attrib['task']));
}
else if (in_array($attrib['command'], $a_static_commands)) {
$attrib['href'] = $this->app->url(array('action' => $attrib['command']));
}
else if (($attrib['command'] == 'permaurl' || $attrib['command'] == 'extwin') && !empty($this->env['permaurl'])) {
$attrib['href'] = $this->env['permaurl'];
}
}
// overwrite attributes
if (!$attrib['href']) {
$attrib['href'] = '#';
}
if ($attrib['task']) {
if ($attrib['classact'])
$attrib['class'] = $attrib['classact'];
}
else if ($command && !$attrib['onclick']) {
$attrib['onclick'] = sprintf(
"return %s.command('%s','%s',this,event)",
self::JS_OBJECT_NAME,
$command,
$attrib['prop']
);
}
$out = '';
// generate image tag
if ($attrib['type'] == 'image') {
$attrib_str = html::attrib_string(
$attrib,
array(
'style', 'class', 'id', 'width', 'height', 'border', 'hspace',
'vspace', 'align', 'alt', 'tabindex', 'title'
)
);
$btn_content = sprintf('<img src="%s"%s />', $this->abs_url($attrib['image']), $attrib_str);
if ($attrib['label']) {
$btn_content .= ' '.$attrib['label'];
}
$link_attrib = array('href', 'onclick', 'onmouseover', 'onmouseout', 'onmousedown', 'onmouseup', 'target');
}
else if ($attrib['type'] == 'link') {
$btn_content = isset($attrib['content']) ? $attrib['content'] : ($attrib['label'] ? $attrib['label'] : $attrib['command']);
$link_attrib = array_merge(html::$common_attrib, array('href', 'onclick', 'tabindex', 'target', 'rel'));
if ($attrib['innerclass'])
$btn_content = html::span($attrib['innerclass'], $btn_content);
}
else if ($attrib['type'] == 'input') {
$attrib['type'] = 'button';
if ($attrib['label']) {
$attrib['value'] = $attrib['label'];
}
if ($attrib['command']) {
$attrib['disabled'] = 'disabled';
}
$out = html::tag('input', $attrib, null, array('type', 'value', 'onclick', 'id', 'class', 'style', 'tabindex', 'disabled'));
}
else {
if ($attrib['label']) {
$attrib['value'] = $attrib['label'];
}
if ($attrib['command']) {
$attrib['disabled'] = 'disabled';
}
$content = isset($attrib['content']) ? $attrib['content'] : $attrib['label'];
$out = html::tag('button', $attrib, $content, array('type', 'value', 'onclick', 'id', 'class', 'style', 'tabindex', 'disabled'));
}
// generate html code for button
if ($btn_content) {
$attrib_str = html::attrib_string($attrib, $link_attrib);
$out = sprintf('<a%s>%s</a>', $attrib_str, $btn_content);
}
if ($attrib['wrapper']) {
$out = html::tag($attrib['wrapper'], null, $out);
}
if ($menuitem) {
$class = $attrib['menuitem-class'] ? ' class="' . $attrib['menuitem-class'] . '"' : '';
$out = '<li role="menuitem"' . $class . '>' . $out . '</li>';
}
return $out;
}
/**
* Link an external script file
*
* @param string $file File URL
* @param string $position Target position [head|foot]
*/
public function include_script($file, $position='head')
{
if (!preg_match('|^https?://|i', $file) && $file[0] != '/') {
$file = $this->file_mod($this->scripts_path . $file);
}
if (!is_array($this->script_files[$position])) {
$this->script_files[$position] = array();
}
if (!in_array($file, $this->script_files[$position])) {
$this->script_files[$position][] = $file;
}
}
/**
* Add inline javascript code
*
* @param string $script JS code snippet
* @param string $position Target position [head|head_top|foot]
*/
public function add_script($script, $position = 'head')
{
if (!isset($this->scripts[$position])) {
$this->scripts[$position] = "\n" . rtrim($script);
}
else {
$this->scripts[$position] .= "\n" . rtrim($script);
}
}
/**
* Link an external css file
*
* @param string $file File URL
*/
public function include_css($file)
{
$this->css_files[] = $file;
}
/**
* Add HTML code to the page header
*
* @param string $str HTML code
*/
public function add_header($str)
{
$this->header .= "\n" . $str;
}
/**
* Add HTML code to the page footer
* To be added right befor </body>
*
* @param string $str HTML code
*/
public function add_footer($str)
{
$this->footer .= "\n" . $str;
}
/**
* Process template and write to stdOut
*
* @param string $output HTML output
* @param string $base_path Base for absolute paths
*/
protected function _write($output = '', $base_path = '')
{
$output = trim($output);
if (empty($output)) {
$output = html::doctype('html5') . "\n" . $this->default_template;
$is_empty = true;
}
// set default page title
if (empty($this->pagetitle)) {
$this->pagetitle = 'Roundcube Mail';
}
// declare page language
if (!empty($_SESSION['language'])) {
$lang = substr($_SESSION['language'], 0, 2);
$output = preg_replace('/<html/', '<html lang="' . html::quote($lang) . '"', $output, 1);
if (!headers_sent()) {
header('Content-Language: ' . $lang);
}
}
// replace specialchars in content
$page_title = html::quote($this->pagetitle);
$page_header = '';
$page_footer = '';
// include meta tag with charset
if (!empty($this->charset)) {
if (!headers_sent()) {
header('Content-Type: text/html; charset=' . $this->charset);
}
$page_header = '<meta http-equiv="content-type"';
$page_header.= ' content="text/html; charset=';
$page_header.= $this->charset . '" />'."\n";
}
// definition of the code to be placed in the document header and footer
if (is_array($this->script_files['head'])) {
foreach ($this->script_files['head'] as $file) {
$page_header .= html::script($file);
}
}
$head_script = $this->scripts['head_top'] . $this->scripts['head'];
if (!empty($head_script)) {
$page_header .= html::script(array(), $head_script);
}
if (!empty($this->header)) {
$page_header .= $this->header;
}
// put docready commands into page footer
if (!empty($this->scripts['docready'])) {
$this->add_script('$(document).ready(function(){ ' . $this->scripts['docready'] . "\n});", 'foot');
}
if (is_array($this->script_files['foot'])) {
foreach ($this->script_files['foot'] as $file) {
$page_footer .= html::script($file);
}
}
if (!empty($this->footer)) {
$page_footer .= $this->footer . "\n";
}
if (!empty($this->scripts['foot'])) {
$page_footer .= html::script(array(), $this->scripts['foot']);
}
// find page header
if ($hpos = stripos($output, '</head>')) {
$page_header .= "\n";
}
else {
if (!is_numeric($hpos)) {
$hpos = stripos($output, '<body');
}
if (!is_numeric($hpos) && ($hpos = stripos($output, '<html'))) {
while ($output[$hpos] != '>') {
$hpos++;
}
$hpos++;
}
$page_header = "<head>\n<title>$page_title</title>\n$page_header\n</head>\n";
}
// add page hader
if ($hpos) {
$output = substr_replace($output, $page_header, $hpos, 0);
}
else {
$output = $page_header . $output;
}
// add page footer
if (($fpos = strripos($output, '</body>')) || ($fpos = strripos($output, '</html>'))) {
// for Elastic: put footer content before "footer scripts"
while (($npos = strripos($output, "\n", -strlen($output) + $fpos - 1))
&& $npos != $fpos
&& ($chunk = substr($output, $npos, $fpos - $npos)) !== ''
&& (trim($chunk) === '' || preg_match('/\s*<script[^>]+><\/script>\s*/', $chunk))
) {
$fpos = $npos;
}
$output = substr_replace($output, $page_footer."\n", $fpos, 0);
}
else {
$output .= "\n".$page_footer;
}
// add css files in head, before scripts, for speed up with parallel downloads
if (!empty($this->css_files) && !$is_empty
&& (($pos = stripos($output, '<script ')) || ($pos = stripos($output, '</head>')))
) {
$css = '';
foreach ($this->css_files as $file) {
$is_less = substr_compare($file, '.less', -5, 5, true) === 0;
$css .= html::tag('link', array(
'rel' => $is_less ? 'stylesheet/less' : 'stylesheet',
'type' => 'text/css',
'href' => $file,
'nl' => true,
));
}
$output = substr_replace($output, $css, $pos, 0);
}
$output = $this->parse_with_globals($this->fix_paths($output));
if ($this->assets_path) {
$output = $this->fix_assets_paths($output);
}
$output = $this->postrender($output);
// trigger hook with final HTML content to be sent
$hook = $this->app->plugins->exec_hook("send_page", array('content' => $output));
if (!$hook['abort']) {
if ($this->charset != RCUBE_CHARSET) {
echo rcube_charset::convert($hook['content'], RCUBE_CHARSET, $this->charset);
}
else {
echo $hook['content'];
}
}
}
/**
* Returns iframe object, registers some related env variables
*
* @param array $attrib HTML attributes
* @param boolean $is_contentframe Register this iframe as the 'contentframe' gui object
*
* @return string IFRAME element
*/
public function frame($attrib, $is_contentframe = false)
{
static $idcount = 0;
if (!$attrib['id']) {
$attrib['id'] = 'rcmframe' . ++$idcount;
}
$attrib['name'] = $attrib['id'];
$attrib['src'] = $attrib['src'] ? $this->abs_url($attrib['src'], true) : 'about:blank';
// register as 'contentframe' object
if ($is_contentframe || $attrib['contentframe']) {
$this->set_env('contentframe', $attrib['contentframe'] ? $attrib['contentframe'] : $attrib['name']);
$this->set_env('blankpage', $this->asset_url($attrib['src']));
}
return html::iframe($attrib);
}
/* ************* common functions delivering gui objects ************** */
/**
* Create a form tag with the necessary hidden fields
*
* @param array $attrib Named tag parameters
* @param string $content HTML content of the form
*
* @return string HTML code for the form
*/
public function form_tag($attrib, $content = null)
{
if ($this->env['extwin']) {
$hiddenfield = new html_hiddenfield(array('name' => '_extwin', 'value' => '1'));
$hidden = $hiddenfield->show();
}
else if ($this->framed || $this->env['framed']) {
$hiddenfield = new html_hiddenfield(array('name' => '_framed', 'value' => '1'));
$hidden = $hiddenfield->show();
}
if (!$content) {
$attrib['noclose'] = true;
}
return html::tag('form',
$attrib + array('action' => $this->app->comm_path, 'method' => "get"),
$hidden . $content,
array('id','class','style','name','method','action','enctype','onsubmit')
);
}
/**
* Build a form tag with a unique request token
*
* @param array $attrib Named tag parameters including 'action' and 'task' values
* which will be put into hidden fields
* @param string $content Form content
*
* @return string HTML code for the form
*/
public function request_form($attrib, $content = '')
{
$hidden = new html_hiddenfield();
if ($attrib['task']) {
$hidden->add(array('name' => '_task', 'value' => $attrib['task']));
}
if ($attrib['action']) {
$hidden->add(array('name' => '_action', 'value' => $attrib['action']));
}
// we already have a <form> tag
if ($attrib['form']) {
if ($this->framed || $this->env['framed']) {
$hidden->add(array('name' => '_framed', 'value' => '1'));
}
return $hidden->show() . $content;
}
unset($attrib['task'], $attrib['request']);
$attrib['action'] = './';
return $this->form_tag($attrib, $hidden->show() . $content);
}
/**
* GUI object 'username'
* Showing IMAP username of the current session
*
* @param array $attrib Named tag parameters (currently not used)
*
* @return string HTML code for the gui object
*/
public function current_username($attrib)
{
static $username;
// alread fetched
if (!empty($username)) {
return $username;
}
// Current username is an e-mail address
if (strpos($_SESSION['username'], '@')) {
$username = $_SESSION['username'];
}
// get e-mail address from default identity
else if ($sql_arr = $this->app->user->get_identity()) {
$username = $sql_arr['email'];
}
else {
$username = $this->app->user->get_username();
}
return rcube_utils::idn_to_utf8($username);
}
/**
* GUI object 'loginform'
* Returns code for the webmail login form
*
* @param array $attrib Named parameters
*
* @return string HTML code for the gui object
*/
protected function login_form($attrib)
{
$default_host = $this->config->get('default_host');
$autocomplete = (int) $this->config->get('login_autocomplete');
$_SESSION['temp'] = true;
// save original url
$url = rcube_utils::get_input_value('_url', rcube_utils::INPUT_POST);
if (empty($url) && !preg_match('/_(task|action)=logout/', $_SERVER['QUERY_STRING']))
$url = $_SERVER['QUERY_STRING'];
// Disable autocapitalization on iPad/iPhone (#1488609)
$attrib['autocapitalize'] = 'off';
$form_name = !empty($attrib['form']) ? $attrib['form'] : 'form';
// set atocomplete attribute
$user_attrib = $autocomplete > 0 ? array() : array('autocomplete' => 'off');
$host_attrib = $autocomplete > 0 ? array() : array('autocomplete' => 'off');
$pass_attrib = $autocomplete > 1 ? array() : array('autocomplete' => 'off');
$input_task = new html_hiddenfield(array('name' => '_task', 'value' => 'login'));
$input_action = new html_hiddenfield(array('name' => '_action', 'value' => 'login'));
$input_tzone = new html_hiddenfield(array('name' => '_timezone', 'id' => 'rcmlogintz', 'value' => '_default_'));
$input_url = new html_hiddenfield(array('name' => '_url', 'id' => 'rcmloginurl', 'value' => $url));
$input_user = new html_inputfield(array('name' => '_user', 'id' => 'rcmloginuser', 'required' => 'required')
+ $attrib + $user_attrib);
$input_pass = new html_passwordfield(array('name' => '_pass', 'id' => 'rcmloginpwd', 'required' => 'required')
+ $attrib + $pass_attrib);
$input_host = null;
if (is_array($default_host) && count($default_host) > 1) {
$input_host = new html_select(array('name' => '_host', 'id' => 'rcmloginhost'));
foreach ($default_host as $key => $value) {
if (!is_array($value)) {
$input_host->add($value, (is_numeric($key) ? $value : $key));
}
else {
$input_host = null;
break;
}
}
}
else if (is_array($default_host) && ($host = key($default_host)) !== null) {
$hide_host = true;
$input_host = new html_hiddenfield(array(
'name' => '_host', 'id' => 'rcmloginhost', 'value' => is_numeric($host) ? $default_host[$host] : $host) + $attrib);
}
else if (empty($default_host)) {
$input_host = new html_inputfield(array('name' => '_host', 'id' => 'rcmloginhost')
+ $attrib + $host_attrib);
}
$this->add_gui_object('loginform', $form_name);
// create HTML table with two cols
$table = new html_table(array('cols' => 2));
$table->add('title', html::label('rcmloginuser', html::quote($this->app->gettext('username'))));
$table->add('input', $input_user->show(rcube_utils::get_input_value('_user', rcube_utils::INPUT_GPC)));
$table->add('title', html::label('rcmloginpwd', html::quote($this->app->gettext('password'))));
$table->add('input', $input_pass->show());
// add host selection row
if (is_object($input_host) && !$hide_host) {
$table->add('title', html::label('rcmloginhost', html::quote($this->app->gettext('server'))));
$table->add('input', $input_host->show(rcube_utils::get_input_value('_host', rcube_utils::INPUT_GPC)));
}
$out = $input_task->show();
$out .= $input_action->show();
$out .= $input_tzone->show();
$out .= $input_url->show();
$out .= $table->show();
if ($hide_host) {
$out .= $input_host->show();
}
if (rcube_utils::get_boolean($attrib['submit'])) {
$button_attr = array('type' => 'submit', 'id' => 'rcmloginsubmit', 'class' => 'button mainaction submit');
$out .= html::p('formbuttons', html::tag('button', $button_attr, $this->app->gettext('login')));
}
// surround html output with a form tag
if (empty($attrib['form'])) {
$out = $this->form_tag(array('name' => $form_name, 'method' => 'post'), $out);
}
// include script for timezone detection
$this->include_script('jstz.min.js');
return $out;
}
/**
* GUI object 'preloader'
* Loads javascript code for images preloading
*
* @param array $attrib Named parameters
* @return void
*/
protected function preloader($attrib)
{
$images = preg_split('/[\s\t\n,]+/', $attrib['images'], -1, PREG_SPLIT_NO_EMPTY);
$images = array_map(array($this, 'abs_url'), $images);
$images = array_map(array($this, 'asset_url'), $images);
if (empty($images) || $_REQUEST['_task'] == 'logout') {
return;
}
$this->add_script('var images = ' . self::json_serialize($images, $this->devel_mode) .';
for (var i=0; i<images.length; i++) {
img = new Image();
img.src = images[i];
}', 'docready');
}
/**
* GUI object 'searchform'
* Returns code for search function
*
* @param array $attrib Named parameters
*
* @return string HTML code for the gui object
*/
public function search_form($attrib)
{
// add some labels to client
$this->add_label('searching');
$attrib['name'] = '_q';
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmqsearchbox';
}
if ($attrib['type'] == 'search' && !$this->browser->khtml) {
unset($attrib['type'], $attrib['results']);
}
if (empty($attrib['placeholder'])) {
$attrib['placeholder'] = $this->app->gettext('searchplaceholder');
}
$label = html::label(array('for' => $attrib['id'], 'class' => 'voice'), rcube::Q($this->app->gettext('arialabelsearchterms')));
$input_q = new html_inputfield($attrib);
$out = $label . $input_q->show();
// Support for multiple searchforms on the same page
if ($attrib['gui-object'] !== false && $attrib['gui-object'] !== 'false') {
$this->add_gui_object($attrib['gui-object'] ?: 'qsearchbox', $attrib['id']);
}
// add form tag around text field
if (empty($attrib['form']) && empty($attrib['no-form'])) {
$out = $this->form_tag(array(
'name' => $attrib['form-name'] ?: 'rcmqsearchform',
'onsubmit' => sprintf("%s.command('%s'); return false", self::JS_OBJECT_NAME, $attrib['command'] ?: 'search'),
// 'style' => "display:inline"
), $out);
}
if (!empty($attrib['wrapper'])) {
$header = html::tag($attrib['ariatag'] ?: 'h2', array(
'id' => 'aria-label-' . $attrib['label'],
'class' => 'voice'
), rcube::Q($this->app->gettext('arialabel' . $attrib['label'], $attrib['label-domain'])));
if ($attrib['options']) {
$options_button = $this->button(array(
'type' => 'link',
'href' => '#search-filter',
'class' => 'button options',
'label' => 'options',
'title' => 'options',
'tabindex' => '0',
'innerclass' => 'inner',
- 'data-popup' => $attrib['options']
+ 'data-target' => $attrib['options']
));
}
$search_button = $this->button(array(
'type' => 'link',
'href' => '#search',
'class' => 'button search',
'label' => $attrib['buttontitle'],
'title' => $attrib['buttontitle'],
'tabindex' => '0',
'innerclass' => 'inner',
));
$reset_button = $this->button(array(
'type' => 'link',
'command' => $attrib['reset-command'] ?: 'reset-search',
'class' => 'button reset',
'label' => 'resetsearch',
'title' => 'resetsearch',
'tabindex' => '0',
'innerclass' => 'inner',
));
$out = html::div(array(
'role' => 'search',
'aria-labelledby' => $attrib['label'] ? 'aria-label-' . $attrib['label'] : null,
'class' => $attrib['wrapper'],
), "$header$out\n$options_button\n$reset_button\n$search_button");
}
return $out;
}
/**
* Builder for GUI object 'message'
*
* @param array Named tag parameters
* @return string HTML code for the gui object
*/
protected function message_container($attrib)
{
if (isset($attrib['id']) === false) {
$attrib['id'] = 'rcmMessageContainer';
}
$this->add_gui_object('message', $attrib['id']);
return html::div($attrib, '');
}
/**
* GUI object 'charsetselector'
*
* @param array $attrib Named parameters for the select tag
*
* @return string HTML code for the gui object
*/
public function charset_selector($attrib)
{
// pass the following attributes to the form class
$field_attrib = array('name' => '_charset');
foreach ($attrib as $attr => $value) {
if (in_array($attr, array('id', 'name', 'class', 'style', 'size', 'tabindex'))) {
$field_attrib[$attr] = $value;
}
}
$charsets = array(
'UTF-8' => 'UTF-8 ('.$this->app->gettext('unicode').')',
'US-ASCII' => 'ASCII ('.$this->app->gettext('english').')',
'ISO-8859-1' => 'ISO-8859-1 ('.$this->app->gettext('westerneuropean').')',
'ISO-8859-2' => 'ISO-8859-2 ('.$this->app->gettext('easterneuropean').')',
'ISO-8859-4' => 'ISO-8859-4 ('.$this->app->gettext('baltic').')',
'ISO-8859-5' => 'ISO-8859-5 ('.$this->app->gettext('cyrillic').')',
'ISO-8859-6' => 'ISO-8859-6 ('.$this->app->gettext('arabic').')',
'ISO-8859-7' => 'ISO-8859-7 ('.$this->app->gettext('greek').')',
'ISO-8859-8' => 'ISO-8859-8 ('.$this->app->gettext('hebrew').')',
'ISO-8859-9' => 'ISO-8859-9 ('.$this->app->gettext('turkish').')',
'ISO-8859-10' => 'ISO-8859-10 ('.$this->app->gettext('nordic').')',
'ISO-8859-11' => 'ISO-8859-11 ('.$this->app->gettext('thai').')',
'ISO-8859-13' => 'ISO-8859-13 ('.$this->app->gettext('baltic').')',
'ISO-8859-14' => 'ISO-8859-14 ('.$this->app->gettext('celtic').')',
'ISO-8859-15' => 'ISO-8859-15 ('.$this->app->gettext('westerneuropean').')',
'ISO-8859-16' => 'ISO-8859-16 ('.$this->app->gettext('southeasterneuropean').')',
'WINDOWS-1250' => 'Windows-1250 ('.$this->app->gettext('easterneuropean').')',
'WINDOWS-1251' => 'Windows-1251 ('.$this->app->gettext('cyrillic').')',
'WINDOWS-1252' => 'Windows-1252 ('.$this->app->gettext('westerneuropean').')',
'WINDOWS-1253' => 'Windows-1253 ('.$this->app->gettext('greek').')',
'WINDOWS-1254' => 'Windows-1254 ('.$this->app->gettext('turkish').')',
'WINDOWS-1255' => 'Windows-1255 ('.$this->app->gettext('hebrew').')',
'WINDOWS-1256' => 'Windows-1256 ('.$this->app->gettext('arabic').')',
'WINDOWS-1257' => 'Windows-1257 ('.$this->app->gettext('baltic').')',
'WINDOWS-1258' => 'Windows-1258 ('.$this->app->gettext('vietnamese').')',
'ISO-2022-JP' => 'ISO-2022-JP ('.$this->app->gettext('japanese').')',
'ISO-2022-KR' => 'ISO-2022-KR ('.$this->app->gettext('korean').')',
'ISO-2022-CN' => 'ISO-2022-CN ('.$this->app->gettext('chinese').')',
'EUC-JP' => 'EUC-JP ('.$this->app->gettext('japanese').')',
'EUC-KR' => 'EUC-KR ('.$this->app->gettext('korean').')',
'EUC-CN' => 'EUC-CN ('.$this->app->gettext('chinese').')',
'BIG5' => 'BIG5 ('.$this->app->gettext('chinese').')',
'GB2312' => 'GB2312 ('.$this->app->gettext('chinese').')',
);
if ($post = rcube_utils::get_input_value('_charset', rcube_utils::INPUT_POST)) {
$set = $post;
}
else if (!empty($attrib['selected'])) {
$set = $attrib['selected'];
}
else {
$set = $this->get_charset();
}
$set = strtoupper($set);
if (!isset($charsets[$set]) && preg_match('/^[A-Z0-9-]+$/', $set)) {
$charsets[$set] = $set;
}
$select = new html_select($field_attrib);
$select->add(array_values($charsets), array_keys($charsets));
return $select->show($set);
}
/**
* Include content from config/about.<LANG>.html if available
*/
protected function about_content($attrib)
{
$content = '';
$filenames = array(
'about.' . $_SESSION['language'] . '.html',
'about.' . substr($_SESSION['language'], 0, 2) . '.html',
'about.html',
);
foreach ($filenames as $file) {
$fn = RCUBE_CONFIG_DIR . $file;
if (is_readable($fn)) {
$content = file_get_contents($fn);
$content = $this->parse_conditions($content);
$content = $this->parse_xml($content);
break;
}
}
return $content;
}
}
diff --git a/skins/elastic/styles/colors.less b/skins/elastic/styles/colors.less
index 15dd09a25..8219f6d39 100644
--- a/skins/elastic/styles/colors.less
+++ b/skins/elastic/styles/colors.less
@@ -1,202 +1,202 @@
/**
* Roundcube webmail styles for the Elastic skin
*
* Copyright (c) 2017-2018, The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
@color-main: #37beff;
@color-main-dark: darken(@color-main, 35%);
@color-black: #161b1d;
@color-font: lighten(@color-black, 10%);
@color-link: #00acff;
@color-link-hover: darken(@color-link, 10%);
@color-border: #ddd;
@color-error: #ff5552;
@color-success: #41b849;
@color-warning: #ffd452;
@color-link-secondary: lighten(@color-font, 60%);
@color-black-shade-text: lighten(@color-black, 35%);
@color-black-shade-border: lighten(@color-black, 75%);
@color-black-shade-bg: lighten(@color-black, 85%);
// Layout elements
@color-layout-border: @color-black-shade-border;
@color-layout-header: @color-font;
@color-layout-sidebar-background: #fff;
@color-layout-list-background: #fff;
@color-layout-content-background: #fff;
-@color-layout-header-background: #fff;
+@color-layout-header-background: #f4f4f4;
@color-layout-footer-background: #fff;
// Task menu
@color-taskmenu-background: #414e54;
@color-taskmenu-button: #fff;
@color-taskmenu-button-selected: @color-taskmenu-button;
@color-taskmenu-button-action: @color-main;
@color-taskmenu-button-special: @color-taskmenu-button;
@color-taskmenu-button-selected-background: lighten(@color-taskmenu-background, 10%);
@color-taskmenu-button-action-background: transparent;
@color-taskmenu-button-special-background: @color-taskmenu-background;
@color-taskmenu-button-hover: #fff;
@color-taskmenu-button-selected-hover: #fff;
@color-taskmenu-button-action-hover: @color-main;
@color-taskmenu-button-special-hover: lighten(@color-taskmenu-button-special, 10%);
@color-taskmenu-button-background-hover: lighten(@color-taskmenu-background, 10%);
@color-taskmenu-button-action-background-hover: @color-taskmenu-button-background-hover;
@color-taskmenu-button-special-background-hover:lighten(@color-taskmenu-button-special-background, 10%);
@color-taskmenu-button-logout-hover: @color-error;
// Toolbar
@color-toolbar-button: @color-font;
@color-toolbar-button-background-hover: tint(@color-main, 96%);
@color-searchbar-icon-active: green;
-
+@color-searchbar-background: #fbfbfb;
// Toolbar menu
@color-toolbarmenu-hover: #fff;
@color-toolbarmenu-hover-background: @color-main;
// Listings
@color-list: @color-font;
@color-list-selected: inherit;
@color-list-selected-background: tint(@color-main, 90%);
@color-list-flagged: @color-error;
@color-list-deleted: lighten(@color-black, 50%);
@color-list-secondary: @color-black-shade-text;
@color-list-droptarget-background: #ffffcc;
@color-list-focus-indicator: lighten(@color-main, 20%);
@color-list-border: @color-black-shade-bg;
@color-list-badge: #fff;
@color-list-badge-background: @color-main;
@color-list-recent: blue;
@color-list-recent-badge: #fff;
@color-list-recent-badge-background: @color-list-recent;
-@color-list-pagenav: @color-black-shade-text;
+//@color-list-pagenav: @color-black-shade-text;
@color-list-icon: fadeout(@color-list-secondary, 25%);
// Drag-n-drop layer
@color-drag-layer: #fff;
@color-drag-layer-background: @color-taskmenu-background;
@color-drag-layer-shadow: @color-black-shade-bg;
// Messages
@color-message: @color-font;
@color-message-border: rgba(0, 0, 0, 0.15);
@color-message-background: fadeout(@color-main, 95%);
@color-message-link: @color-main;
@color-message-information: @color-main;
@color-message-success: @color-success;
@color-message-warning: @color-warning;
@color-message-error: @color-error;
@color-message-loading: #333;
@color-message-shadow: @color-black-shade-bg;
@color-message-error-background: fadeout(@color-message-error, 85%);
@color-message-information-background: fadeout(@color-message-information, 85%);
@color-message-success-background: fadeout(@color-message-success, 85%);
@color-message-warning-background: fadeout(#ffff66, 75%);
// Popovers (menus)
@color-popover-header: @color-black-shade-text;
@color-popover-header-background: transparent;
@color-popover-shadow: @color-black-shade-bg;
@color-popover-separator: @color-black-shade-text;
@color-popover-separator-background: @color-black-shade-bg;
@color-popover-mobile-header: #fff;
@color-popover-mobile-header-background: @color-main-dark;
// Dialogs
@color-dialog-overlay-background: fade(@color-font, 50%);
@color-dialog-header: @color-layout-header;
@color-dialog-header-border: @color-border;
@color-spinner-circle: @color-black-shade-bg;
@color-spinner-item: @color-black-shade-text;
// Forms
@color-input: @color-font;
@color-input-border: rgba(22, 27, 29, 0.15);
@color-input-border-focus: @color-main;
@color-input-border-focus-shadow: fadeout(@color-main, 75);
@color-input-border-invalid: @color-error;
@color-input-border-invalid-shadow: fadeout(@color-error, 75);
@color-input-addon-background: @color-black-shade-bg;
@color-recipient-input-border: @color-input-border;
@color-recipient-input-background: @color-black-shade-bg;
@color-input-placeholder: #bbb;
@color-checkbox: @color-main;
@color-checkbox-checked: @color-main;
@color-checkbox-focus: darken(@color-checkbox, 30%);
@color-checkbox-checked-focus: darken(@color-checkbox-checked, 30%);
@color-form-hint: @color-black-shade-text;
@color-image-upload-background: @color-black-shade-bg;
@color-btn-secondary: #fff;
@color-btn-secondary-background: lighten(@color-black, 50%);
@color-btn-primary: #fff;
@color-btn-primary-background: @color-main;
@color-btn-danger: #fff;
@color-btn-danger-background: @color-error;
@color-quota-background: @color-black-shade-bg;
@color-quota-text: @color-black-shade-text;
@color-quota-value: @color-main;
@color-quota-value-warning: @color-error;
@color-blockquote-background: fadeout(@color-black-shade-bg, 50%);
@color-blockquote-0: darken(@color-main, 30%);
@color-blockquote-1: darken(@color-success, 25%);
@color-blockquote-2: darken(@color-error, 20%);
@color-blockquote-0-border: @color-blockquote-0;
@color-blockquote-1-border: @color-blockquote-1;
@color-blockquote-2-border: @color-blockquote-2;
@color-mail-signature: @color-black-shade-text;
@color-mail-headers: @color-black-shade-text;
@color-spellcheck-link: @color-error;
@color-table-border: @color-layout-border;
@color-table-selected: @color-list-selected;
@color-table-selected-background: @color-list-selected-background;
// Datepicker
@color-datepicker-border: @color-layout-border;
@color-datepicker-font: @color-font;
@color-datepicker-highlight: !default;
@color-datepicker-highlight-background: lighten(@color-main, 30%);
@color-datepicker-active: #fff;
@color-datepicker-active-background: @color-main;
// Image tools
@color-image-tools: #fff;
@color-image-tools-background: fadeout(@color-main, 60%);
@color-image-tools-hover: fadeout(@color-main, 50%);
diff --git a/skins/elastic/styles/layout.less b/skins/elastic/styles/layout.less
index 642ae9088..5a6643319 100644
--- a/skins/elastic/styles/layout.less
+++ b/skins/elastic/styles/layout.less
@@ -1,293 +1,299 @@
/**
* Roundcube webmail styles for the Elastic skin
*
* Copyright (c) 2017-2018, The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** Responsive design - Layout ***/
/*
- Large screen (width > 1200px)
-----------------------------------------------------------------------------------------------------
| menu | sidebar | list | content |
-----------------------------------------------------------------------------------------------------
- Normal screen (1200px => width => 768px) - typical: 768x1024 (iPad Mini/Air)
-------------------------------------------------------------------
|menu| sidebar/list | content |
-------------------------------------------------------------------
- Small (480px < width < 768px)
------------------------------------------
|menu| sidebar/list/content |
------------------------------------------
- Phone (width <= 480px) - typical: 320x480 (iPhone 5), 375x667 (iPhone 6-7), 360x564 (Galaxy S6)
------------------------
| sidebar/list/content |
------------------------
*/
html {
height: 100%;
font-size: @page-font-size;
}
body {
min-width: @page-min-width;
height: 100%;
color: @color-font;
overflow: hidden;
}
body > #layout {
overflow: hidden;
display: flex;
height: 100%;
width: 100%;
& > div {
&.sidebar,
&.list {
display: flex;
flex-direction: column;
max-width: 30%;
border-right: 1px solid @color-layout-border;
-
- & > .header {
- a.button {
- margin: 0;
- padding: 0 .5rem;
- }
- }
}
&.content {
display: flex;
flex: 6;
flex-direction: column;
min-width: 50%;
background-color: @color-layout-content-background;
& > .formcontent, // e.g. Help plugin
& > .content {
height: 100%;
width: 100%;
overflow: auto;
flex: 1;
}
.iframe-wrapper {
width: 100%;
height: 100%;
flex: 1;
iframe {
width: 100%;
height: 100%;
border: 0;
}
}
}
&.sidebar {
display: flex;
min-width: 220px;
background-color: @color-layout-sidebar-background;
flex: 2;
}
&.list {
display: flex;
flex: 3;
min-width: 300px;
background-color: @color-layout-list-background;
}
+
& > .scroller {
flex: 1;
position: relative; // for .listing-info positioning
}
& > .content.only > .scroller {
overflow: auto;
}
& > .header,
& > .footer {
background-color: @color-layout-header-background;
font-size: @layout-header-font-size;
font-weight: bold;
line-height: @layout-header-height;
height: @layout-header-height;
min-height: @layout-header-height;
padding: 0 .25em;
margin: 0;
position: relative; // for absolute positioning of searchbar
overflow: hidden;
white-space: nowrap;
display: flex;
justify-content: center;
}
+ &.list > .footer,
+ &.sidebar > .footer {
+ height: @layout-footer-height;
+ min-height: @layout-footer-height;
+ line-height: @layout-footer-height;
+
+ &.small {
+ height: @layout-footer-small-height;
+ min-height: @layout-footer-small-height;
+ line-height: @layout-footer-small-height;
+ }
+ }
+
& > .header {
border-bottom: 1px solid @color-layout-border;
color: @color-layout-header;
.header-title {
.overflow-ellipsis;
flex: 1;
text-align: center;
margin: 0 -20rem;
}
+
+ a.toolbar-menu-button {
+ order: 99; // always the last button
+ }
}
& > .footer {
border-top: 1px solid @color-layout-border;
background-color: @color-layout-footer-background;
&:empty {
display: none;
}
}
}
}
html.iframe {
body {
overflow: auto;
#layout > .content {
height: 100%;
}
}
}
@media screen and (max-width: @screen-width-large) {
body > #layout > div.sidebar,
body > #layout > div.list {
min-width: 260px;
flex: 3;
}
+
+ body > #layout > div.list > .header > a.button {
+ padding: 0 .25rem;
+ margin: 0 .25rem;
+ }
}
@media screen and (max-width: @screen-width-medium) {
}
@media screen and (max-width: @screen-width-small) {
body > #layout > div.sidebar,
body > #layout > div.list {
max-width: none;
border: 0;
}
body > #layout > div > .header {
a.button {
// make the button active area smaller on touch devices
margin: 0 .3rem !important;
padding: 0 !important;
&:before {
font-size: 1.75rem;
height: @layout-touch-header-height;
margin: 0;
}
&.filter:before {
font-size: 1.6rem; // this icon is too big in FA5
}
.inner {
display: none;
}
}
}
-
- body > #layout > div > .header {
- &.with-search:not(.no-toolbar) {
- .searchbar {
- right: @layout-touch-icon-width;
- }
-
- .searchfilterbar {
- right: (@layout-touch-icon-width * 2);
- }
- }
- }
}
@media screen and (max-width: @screen-width-xs) {
}
@media screen and (max-width: @screen-width-mini) {
body > #layout > div.sidebar,
body > #layout > div.list {
min-width: @page-min-width;
}
}
@media screen and (min-width: (@screen-width-xs + 1px)) {
- body > #layout > div > .header > a.menu-button {
+ a.menu-button {
display: none;
}
body > #layout > .menu {
// FIXME: we set background color here not in taskmenu.less, because
// otherwise background is partially white on Android/iOS
background-color: @color-taskmenu-background;
}
}
@media screen and (min-width: (@screen-width-small + 1px)) {
.floating-action-buttons,
body > #layout > .content > .header > .header-title,
body > #layout > div > .header > .buttons,
- body > #layout > div > .header > a.toolbar-menu-button {
+ a.toolbar-menu-button {
display: none;
}
body > #layout > .menu {
width: @layout-menu-width/2;
}
}
@media screen and (min-width: (@screen-width-medium + 1px)) {
body > #layout > .menu {
width: @layout-menu-width;
}
}
@media screen and (min-width: (@screen-width-large + 1px)) {
- body > #layout > div > .header > a.back-list-button,
- body > #layout > div > .header > a.back-sidebar-button {
+ body > #layout > .list > .header > .header-title:not(.all-sizes),
+ a.toolbar-list-button,
+ a.back-list-button,
+ a.back-sidebar-button {
display: none;
}
}
html.layout-phone {
.hidden-phone {
display: none !important;
}
}
html.layout-phone,
html.layout-small {
.hidden-small {
display: none !important;
}
}
html.layout-large,
html.layout-normal {
.hidden-big {
display: none !important;
}
}
html.layout-large {
.hidden-large {
display: none !important;
}
}
diff --git a/skins/elastic/styles/styles.less b/skins/elastic/styles/styles.less
index 778401c0d..95bfc0d2e 100644
--- a/skins/elastic/styles/styles.less
+++ b/skins/elastic/styles/styles.less
@@ -1,217 +1,216 @@
/**
* Roundcube webmail styles for the Elastic skin
*
* Copyright (c) 2017-2018, The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
@import (reference) "variables";
@import (reference) "mixins";
/*** Fonts ***/
@font-face {
font-family: 'Icons';
font-style: normal;
font-weight: 900;
src: url("../fonts/fa-solid-900.woff2") format('woff2'),
url("../fonts/fa-solid-900.woff") format('woff');
}
@font-face {
font-family: 'Icons';
font-style: normal;
font-weight: 400;
src: url("../fonts/fa-regular-400.woff2") format('woff2'),
url("../fonts/fa-regular-400.woff") format('woff');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'),
url('../fonts/roboto-v18-greek-ext_cyrillic-ext_cyrillic_greek_latin-ext_latin-regular.woff2') format('woff2'), // Chrome 26+, Opera 23+, Firefox 39+
url('../fonts/roboto-v18-greek-ext_cyrillic-ext_cyrillic_greek_latin-ext_latin-regular.woff') format('woff'); // Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+
}
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 400;
src: local('Roboto Italic'), local('Roboto-Italic'),
url('../fonts/roboto-v18-greek-ext_cyrillic-ext_cyrillic_greek_latin-ext_latin-italic.woff2') format('woff2'), // Chrome 26+, Opera 23+, Firefox 39+
url('../fonts/roboto-v18-greek-ext_cyrillic-ext_cyrillic_greek_latin-ext_latin-italic.woff') format('woff'); // Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'),
url('../fonts/roboto-v18-greek-ext_cyrillic-ext_cyrillic_greek_latin-ext_latin-700.woff2') format('woff2'), // Chrome 26+, Opera 23+, Firefox 39+
url('../fonts/roboto-v18-greek-ext_cyrillic-ext_cyrillic_greek_latin-ext_latin-700.woff') format('woff'); // Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+
}
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 700;
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
url('../fonts/roboto-v18-greek-ext_cyrillic-ext_cyrillic_greek_latin-ext_latin-700italic.woff2') format('woff2'), // Chrome 26+, Opera 23+, Firefox 39+
url('../fonts/roboto-v18-greek-ext_cyrillic-ext_cyrillic_greek_latin-ext_latin-700italic.woff') format('woff'); // Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+
}
/* E.g. for animated 'loading' icon */
@-webkit-keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
/* Reset some Bootstrap style */
body, button, input, optgroup, select, textarea, .popover {
.font-family;
}
button, input, select, textarea {
line-height: initial;
}
input {
vertical-align: middle;
}
a {
color: @color-link;
&:hover {
color: @color-link-hover;
}
&.disabled:not(.btn) {
opacity: .5;
}
}
@import "layout";
@import "widgets/common";
@import "widgets/buttons";
@import "widgets/jqueryui";
@import "widgets/dialogs";
@import "widgets/taskmenu";
@import "widgets/messages";
@import "widgets/toolbar";
-@import "widgets/searchbar";
@import "widgets/lists";
@import "widgets/forms";
@import "widgets/mail";
/*** Login form ***/
.task-login #layout > .content {
text-align: center;
width: 100%;
background: url(../images/watermark.jpg) center -20px no-repeat;
background-size: auto 40%;
}
#login-form {
margin: 0 auto;
top: 35vh;
width: 95%;
max-width: 280px;
position: relative;
// Fixes input width and position in IE11
.row {
max-width: 280px;
margin-right: 0;
margin-left: 0;
}
}
#login-footer {
flex: 1;
color: @color-black-shade-text;
}
#login-addon {
position: absolute;
bottom: 0;
max-height: 30%;
margin: 1rem !important;
width: auto !important;
overflow: auto;
@media screen and (min-width: (@screen-width-small + 1px)) {
max-width: @screen-width-small;
margin: auto !important;
bottom: 1rem;
left: 0;
right: 0;
}
}
/*** Addressbook UI ***/
#contactpic {
min-width: @layout-contact-icon-width;
min-height: @layout-contact-icon-width;
border-radius: .5rem;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
img {
max-width: @layout-contact-icon-width;
max-height: @layout-contact-icon-height;
}
}
#contacthead {
.names {
margin-bottom: .5rem;
span.namefield {
font-size: 1.5rem;
font-weight: bold;
line-height: 1.2;
}
}
&.readonly {
.source.row {
color: @color-form-hint;
font-size: 90%;
margin-bottom: .25rem;
}
}
}
@import "_styles";
diff --git a/skins/elastic/styles/variables.less b/skins/elastic/styles/variables.less
index 800c13e4e..e01060b7c 100644
--- a/skins/elastic/styles/variables.less
+++ b/skins/elastic/styles/variables.less
@@ -1,48 +1,50 @@
/**
* Roundcube webmail styles for the Elastic skin
*
* Copyright (c) 2017-2018, The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
@import (reference) "fontawesome";
@import (reference) "colors";
@import (reference) "_variables";
@screen-width-large: 1200px;
@screen-width-medium: 1024px;
@screen-width-small: 768px;
@screen-width-xs: 480px;
@screen-width-mini: 320px;
@screen-width-touch: @screen-width-medium;
@screen-width-bs-phone: 576px;
@page-font-size: 14px;
@page-min-width: 240px;
@layout-menu-width: 5rem;
@layout-header-height: 4.2rem;
+@layout-footer-height: @layout-header-height;
+@layout-footer-small-height: 2.5rem;
@layout-header-font-size: 1rem;
-@layout-pagenav-height: 2rem;
+@layout-searchbar-height: 2.6rem;
@layout-touch-header-height: @layout-header-height;
@layout-touch-header-font-size: 1.2rem;
@layout-touch-menu-record-height: 3.4rem;
@layout-touch-menu-record-font-size: 1.2rem;
@layout-touch-icon-width: 2.2em;
@layout-mobile-menu-width: (@screen-width-mini * .85);
@layout-contact-icon-width: 112px;
@layout-contact-icon-height: 135px;
@listing-line-height: 2.5rem;
@listing-touch-line-height: 3.4rem;
@listing-treetoggle-width: 1.5em;
// Additional icons
@icon-resize-corner: data-uri("image/svg+xml;charset=utf-8", "../images/corner-handle.svg"); // size: 512x512
diff --git a/skins/elastic/styles/widgets/buttons.less b/skins/elastic/styles/widgets/buttons.less
index 0406b0e22..bd7cea4e9 100644
--- a/skins/elastic/styles/widgets/buttons.less
+++ b/skins/elastic/styles/widgets/buttons.less
@@ -1,295 +1,298 @@
/**
* Roundcube webmail styles for the Elastic skin
*
* Copyright (c) 2017-2018, The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** Buttons ***/
a.rcmaddcontact {
display: none;
}
.button.disabled {
opacity: .5;
}
a.button {
text-decoration: none;
&.active {
cursor: pointer;
}
}
/* font-icons */
a.button.icon,
button.btn {
&:before {
&:extend(.font-icon-class);
}
&.toolbar-menu-button:before {
content: @fa-var-ellipsis-v;
}
+ &.toolbar-list-button:before {
+ content: @fa-var-cog;
+ }
&.menu-button:before {
content: @fa-var-bars;
}
&.back-sidebar-button:before,
&.back-content-button:before,
&.back-list-button:before {
content: @fa-var-chevron-left;
}
&.generate:before,
&.yes:before,
&.submit:before,
&.continue:before,
&.save:before {
content: @fa-var-check;
}
&.create:before {
content: @fa-var-plus-square;
}
&.edit:before {
content: @fa-var-pencil-alt;
}
&.qrcode:before {
content: @fa-var-qrcode;
}
&.search:before {
content: @fa-var-search;
}
&.filter:before {
content: @fa-var-filter;
font-size: 1.2em; // this icon is too-big in FA5
}
&.import:before {
content: @fa-var-upload;
}
&.export:before {
content: @fa-var-download;
}
&.discard:before,
&.delete:before {
.font-icon-regular(@fa-var-trash-alt);
}
&.next:before {
content: @fa-var-arrow-right;
}
&.restore:before {
content: @fa-var-undo;
}
&.send:before,
&.bounce:before {
content: @fa-var-paper-plane;
}
&.attach:before {
content: @fa-var-paperclip;
}
&.no:before,
&.close:before,
&.cancel:before {
content: @fa-var-times;
}
&.mark:before {
.font-icon-regular(@fa-var-star);
}
&.back:before {
content: @fa-var-chevron-left;
}
&.remove:before {
content: @fa-var-times;
}
&.unlock:before {
content: @fa-var-unlock;
}
&.help:before {
.font-icon-regular(@fa-var-life-ring);
}
&.toggleselect:before {
.font-icon-regular(@fa-var-check-square);
}
&.folders:before {
content: @fa-var-folder-open;
}
&.tools:before,
&.settings:before {
content: @fa-var-wrench;
}
&.properties:before {
content: @fa-var-info-circle;
}
}
a.btn,
button.btn {
// FIXME: Maybe button text (and icon) alignment requires some rework
padding-bottom: .5rem;
&:before {
display: inline !important;
float: none !important;
}
}
a.button.icon {
&.dropdown:before {
content: @fa-var-caret-down;
font-size: 1em;
}
& > span.inner {
display: none;
}
}
html.touch {
.btn:focus {
box-shadow: none;
}
}
@floating-action-button-size: 4rem;
.floating-action-buttons {
position: absolute;
right: 0;
bottom: 0;
.footer:not(:empty) + & {
- bottom: @layout-touch-header-height;
+ bottom: @layout-footer-small-height;
}
a.button {
display: block;
float: left;
width: @floating-action-button-size;
height: @floating-action-button-size;
border-radius: 50%;
background: @color-main;
color: white;
opacity: .95;
box-shadow: 0 0 5px 5px lighten(@color-main, 35%);
margin: 0 1rem 1rem 0;
&:before {
&:extend(.font-icon-class);
content: @fa-var-plus;
width: @floating-action-button-size;
height: @floating-action-button-size;
line-height: @floating-action-button-size;
}
.inner {
display: none;
}
}
}
/*** Bootstrap button style overrides ***/
.btn-secondary {
color: @color-btn-secondary;
background: @color-btn-secondary-background;
border-color: @color-btn-secondary-background;
&:focus {
box-shadow: 0 0 0 .2rem fade(@color-btn-secondary-background, 50%);
}
&:hover {
background: darken(@color-btn-secondary-background, 8%);
border-color: darken(@color-btn-secondary-background, 10%);
}
&.disabled,
&:disabled {
background: lighten(@color-btn-secondary-background, 20%);
border-color: lighten(@color-btn-secondary-background, 20%);
opacity: 1;
}
&:not(:disabled):not(.disabled):active {
background: darken(@color-btn-secondary-background, 11%);
border-color: darken(@color-btn-secondary-background, 13%);
box-shadow: 0 0 0 .2rem fade(@color-btn-secondary-background, 53%);
}
&:not(:disabled):not(.disabled).active {
background: darken(@color-btn-secondary-background, 20%);
border-color: darken(@color-btn-secondary-background, 22%);
}
}
.btn-primary {
color: @color-btn-primary;
background: @color-btn-primary-background;
border-color: @color-btn-primary-background;
&:focus {
box-shadow: 0 0 0 .2rem fade(@color-btn-primary-background, 50%);
}
&:hover {
background: darken(@color-btn-primary-background, 8%);
border-color: darken(@color-btn-primary-background, 10%);
}
&.disabled,
&:disabled {
background: lighten(@color-btn-primary-background, 20%);
border-color: lighten(@color-btn-primary-background, 20%);
opacity: 1;
}
&:not(:disabled):not(.disabled):active {
background: darken(@color-btn-primary-background, 11%);
border-color: darken(@color-btn-primary-background, 13%);
box-shadow: 0 0 0 .2rem fade(@color-btn-primary-background, 53%);
}
&:not(:disabled):not(.disabled).active {
background: darken(@color-btn-primary-background, 20%);
border-color: darken(@color-btn-primary-background, 22%);
}
}
.btn-danger {
color: @color-btn-danger;
background: @color-btn-danger-background;
border-color: @color-btn-danger-background;
&:focus {
box-shadow: 0 0 0 .2rem fade(@color-btn-danger-background, 50%);
}
&:hover {
background: darken(@color-btn-danger-background, 8%);
border-color: darken(@color-btn-danger-background, 10%);
}
&.disabled,
&:disabled {
background: lighten(@color-btn-danger-background, 20%);
border-color: lighten(@color-btn-danger-background, 20%);
opacity: 1;
}
&:not(:disabled):not(.disabled):active {
background: darken(@color-btn-danger-background, 11%);
border-color: darken(@color-btn-danger-background, 13%);
box-shadow: 0 0 0 .2rem fade(@color-btn-danger-background, 53%);
}
&:not(:disabled):not(.disabled).active {
background: darken(@color-btn-danger-background, 20%);
border-color: darken(@color-btn-danger-background, 22%);
}
}
diff --git a/skins/elastic/styles/widgets/common.less b/skins/elastic/styles/widgets/common.less
index ef7b776ae..4374c8f90 100644
--- a/skins/elastic/styles/widgets/common.less
+++ b/skins/elastic/styles/widgets/common.less
@@ -1,540 +1,541 @@
/**
* Roundcube webmail styles for the Elastic skin
*
* Copyright (c) 2017-2018, The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** Common UI elements ***/
.hidden,
.voice {
display: none !important;
}
font.bold {
font-weight: bold;
}
#rcmdraglayer {
min-width: 260px;
width: 260px;
background-color: @color-drag-layer-background;
color: @color-drag-layer;
box-shadow: 3px 3px 5px @color-drag-layer-shadow;
border-radius: .3rem;
z-index: 250;
opacity: .92;
padding: .5rem;
white-space: nowrap;
div {
line-height: 1.6em;
.overflow-ellipsis;
}
}
.frame-content {
padding: 1rem;
h2 {
font-weight: bold;
font-size: 1.5em;
}
h3 {
font-weight: bold;
font-size: 1.25em;
}
}
.listbox {
.scroller {
width: 100%;
overflow-x: hidden;
overflow-y: auto;
}
.navlist {
height: 0;
flex: initial !important;
.listing {
tr:last-child td,
li:last-child {
border-bottom: 0;
}
}
}
}
.pagenav.expanded + .navlist {
border-bottom: 1px solid @color-layout-border;
}
.contact-header {
display: flex;
margin-bottom: 1rem;
.contact-photo {
min-width: @layout-contact-icon-width;
}
.contact-head {
margin-left: 1rem;
legend {
display: none;
}
}
}
@image-attachment-size: 250px;
// this is when image thumbnails are enabled
p.image-attachment {
position: relative;
border: 1px solid @color-border;
border-radius: .3rem;
background-color: @color-message-background;
float: left;
margin: .5rem;
min-width: 47%;
min-height: @image-attachment-size;
overflow: hidden;
// use flexbox to center the image
display: flex;
justify-content: center;
@media screen and (max-width: @screen-width-xs) {
float: none;
margin: .5rem 0 .5rem 0;
}
.image-link {
align-self: center;
text-align: center;
margin: 1.6rem .5rem;
}
span {
color: @color-form-hint;
padding: 0 .5rem;
font-size: 90%;
white-space: nowrap;
position: absolute;
line-height: 1.5rem;
}
.image-filename {
.overflow-ellipsis;
left: 0;
top: 0;
right: 0;
padding-right: 4rem;
}
.image-filesize {
right: 0;
top: 0;
}
.attachment-links {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
text-align: center;
a {
text-decoration: none;
display: inline-block;
padding: 0 .5rem;
line-height: 1.5rem;
}
a:before {
&:extend(.font-icon-class);
display: inline-block;
}
a.open:before {
content: @fa-var-external-link-square-alt;
}
a.download:before {
content: @fa-var-download;
}
}
}
// this is when image thumbnails are disabled
fieldset.image-attachment {
margin-top: .5rem;
legend {
color: @color-form-hint;
font-size: .9rem;
border-top: 1px solid lighten(@color-mail-headers, 50%);
margin: 0;
}
img {
max-width: 100%;
}
}
#folder-selector {
overflow-y: auto;
}
#layout > .content .watermark {
background: url(../images/watermark.jpg) center no-repeat;
width: 100%;
height: 100%;
}
.noselect {
user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.iframe-loader {
width: 100%;
position: absolute;
top: 0;
bottom: 0;
background-color: rgba(255, 255, 255, .95);
display: flex;
align-items: center;
justify-content: center;
z-index: 3;
.spinner {
position: relative;
display: inline-block;
width: 7rem;
height: 7rem;
overflow: hidden;
text-indent: -999em;
color: @color-spinner-circle;
border: 1rem solid;
border-color: currentColor @color-spinner-item currentColor currentColor;
border-radius: 50%;
-webkit-animation: fa-spin 1s infinite linear;
animation: fa-spin 1s infinite linear;
}
}
.footer.toolbar + .iframe-loader {
top: @layout-header-height;
bottom: @layout-header-height;
}
// iOS: Fix scrolling of iframe, display scrollbars on scrollable elements
.ios-scroll {
padding: 0;
-webkit-overflow-scrolling: touch !important;
overflow: scroll !important;
&.iframe-wrapper {
margin-top: 1px; // FIXME: without this scrolling hides the wrapper neighbours' border
}
}
.webkit-scroller {
&::-webkit-scrollbar {
-webkit-appearance: none;
}
&::-webkit-scrollbar:vertical {
width: .6rem;
}
&::-webkit-scrollbar:horizontal {
height: .6rem;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, .4);
border-radius: .3rem;
border: 2px solid #fff;
}
}
.quota-widget {
- width: 5rem;
- max-width: 8rem;
- padding: .5rem;
- text-align: center;
- position: relative;
+ width: 100%;
+ padding: .5rem 1rem;
+ display: flex;
+ align-items: center;
+
+ &:before {
+ &:extend(.font-icon-class);
+ content: @fa-var-hdd;
+ line-height: 1;
+ }
.count {
- display: block;
color: @color-quota-text;
- font-size: 1.1rem;
- line-height: 2;
+ font-size: 80%;
+ order: 2;
}
.bar {
- display: block;
+ flex: 1;
height: .5rem;
- position: absolute;
- bottom: .85rem;
- left: .5rem;
- right: .5rem;
+ margin: 0 1rem;
background-color: @color-quota-background;
border-radius: .25rem;
}
.value {
display: block;
background-color: @color-quota-value;
border-radius: .25rem;
height: .5rem;
opacity: .75;
&.warning {
background-color: @color-quota-value-warning;
}
}
}
.quota-info {
width: 100%;
display: table !important;
td,th {
text-align: center;
white-space: nowrap;
}
th {
border-top: 0;
}
.root {
line-height: 1;
font-style: italic;
color: @color-popover-separator;
background-color: @color-popover-separator-background;
}
th:first-child,
.name {
text-align: left;
}
}
// Make Bootstrap tabs non-wrappable
.nav-tabs {
flex-wrap: nowrap;
.nav-item {
white-space: nowrap;
overflow: hidden;
}
.nav-link {
.overflow-ellipsis;
}
}
.props-table {
td.title {
width: 7em;
}
}
.table-widget {
display: flex;
flex-direction: column;
margin-bottom: .5rem;
border: 1px solid @color-table-border;
& > .content {
overflow-x: auto;
flex-grow: 1;
height: 18.5em;
table th {
border-top: 0;
}
}
& > .footer {
height: 3.5rem;
border-top: 1px solid @color-table-border;
a.button {
padding: 0;
height: 3.5rem;
}
}
table {
margin: 0;
max-height: 18.5em;
}
// Options table is a table with first column for identifier/description
// and other columns for a state flag. E.g. ACL table
table.options-table {
td,th {
text-align: center;
vertical-align: middle;
&:first-child {
.overflow-ellipsis;
text-align: left;
}
}
tr:last-child td {
border-bottom: 1px solid @color-table-border;
}
tr.selected td {
background-color: @color-table-selected-background;
color: @color-table-selected;
outline: 0;
}
td:not(:first-child) span {
display: inline-block;
line-height: 1.25;
&:before {
&:extend(.font-icon-class);
}
}
td.enabled span:before {
content: @fa-var-check;
}
td.partial span:before {
opacity: .3;
content: @fa-var-check;
}
}
}
table.compact-table {
margin: 0;
*:not(.invalid-feedback) {
font-size: inherit;
}
td {
padding: .25rem;
border: 0;
}
td:first-child {
padding-left: 0;
}
td:last-child {
padding-right: 0;
}
}
table.table {
.checkbox-cell {
width: 3rem;
white-space: nowrap;
overflow: hidden;
text-align: center;
input.icon-checkbox + label {
padding: 0;
&:before {
line-height: 1;
height: 1em;
}
}
}
th.checkbox-cell {
padding: .75rem 0;
max-width: 1rem;
&:before {
&:extend(.font-icon-class);
cursor: pointer;
margin: 0 1rem;
line-height: 1;
}
&.subscription:before {
content: @fa-var-rss-square;
}
&.alarm:before {
.font-icon-regular(@fa-var-bell);
}
&.read:before {
content: @fa-var-eye;
}
&.write:before {
content: @fa-var-pencil-alt;
}
}
.buttons-cell {
width: 1%;
white-space: nowrap;
text-align: center;
a.button:before {
line-height: 1;
float: none;
display: inline-block;
}
@media screen and (min-width: @screen-width-xs) {
a.button .inner {
display: inline;
}
}
}
label {
margin: 0;
display: inline;
}
fieldset.tab-pane & thead th {
border: 0;
}
}
html.touch {
table.table {
th.checkbox-cell:before {
font-size: 1.5rem;
}
}
}
/* Bootstrap's .table style overwrites */
.table {
thead th {
border-width: 1px;
white-space: nowrap;
}
}
diff --git a/skins/elastic/styles/widgets/searchbar.less b/skins/elastic/styles/widgets/searchbar.less
index a97a54adf..0215c2158 100644
--- a/skins/elastic/styles/widgets/searchbar.less
+++ b/skins/elastic/styles/widgets/searchbar.less
@@ -1,113 +1,135 @@
/**
* Roundcube webmail styles for the Elastic skin
*
* Copyright (c) 2017-2018, The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
-/*** Searchbar and searchfilterbar widgets ***/
+/*** Searchbar and searchoptions widgets ***/
.searchbar {
- position: absolute;
+ height: @layout-searchbar-height;
+ line-height: @layout-searchbar-height;
background-color: @color-layout-header-background;
- right: .25rem;
- height: @layout-header-height;
+ border-bottom: 1px solid @color-list-border;
+ display: flex;
+ align-items: center;
+ white-space: nowrap;
+ padding: 0 .5rem;
+ overflow: hidden;
+ position: relative;
+
+ form {
+ flex: 1;
+ display: flex;
+
+ &:before {
+ &:extend(.font-icon-class);
+ content: @fa-var-search;
+ height: @layout-searchbar-height;
+ }
+ }
+
+ input {
+ width: 100%;
+ border: 0;
+ background: transparent;
+ padding: .5rem;
+ }
a.button {
+ height: @layout-searchbar-height;
min-width: auto;
- padding: 0;
- display: block;
&:before {
- line-height: @layout-header-height;
+ &:extend(.font-icon-class);
}
- span.inner {
- display: none;
+
+ &.options {
+ &:before {
+ content: @fa-var-filter;
+ font-size: 1em;
+ }
}
- }
- .input-group {
- margin-top: calc(@layout-header-height/2 - 2.25rem/2);
- display: none;
- padding: 0 .5rem;
+ &.reset {
+ display: none;
+ position: absolute;
+ right: .5rem;
- .icon:before {
- font-size: 1em;
- line-height: 1.1;
+ &:before {
+ content: @fa-var-times;
+ font-size: 1em;
+ }
}
- .icon.filter,
- .icon.search {
- color: #888;
+ &.options + .reset {
+ right: 2.5rem;
+ }
+
+ &.search {
+ display: none;
}
}
- form {
+ span.inner {
display: none;
}
- a.icon {
- &:before {
- &:extend(.font-icon-class);
- margin: 0;
- }
-
- &.reset:before {
- content: @fa-var-trash-alt;
+ &.active {
+ form:before {
+ color: @color-searchbar-icon-active;
}
- &.options:before {
- content: @fa-var-angle-down;
+ a.button.reset {
+ display: inline;
}
}
- &.active a.button.search {
- color: @color-searchbar-icon-active;
+ &.open a.button.options:before {
+ content: @fa-var-angle-up;
+ font-size: 1.25em;
+ line-height: 1.25;
}
+}
- &.open {
- z-index: 10;
- left: 0 !important;
- right: 0 !important;
+.searchoptions {
+ button.search {
+ width: 100%;
+ }
- .input-group {
- display: flex;
+ ul.proplist {
+ li {
+ margin: 0;
}
- form {
- display: flex;
- }
-
- a.button.filter,
- a.button.search {
- display: none;
+ & + div {
+ margin-top: 1rem;
}
}
- a.button.search {
- padding: 0 .5rem;
- }
-}
+ .input-group {
+ &:not(:last-child) {
+ margin-bottom: .5rem;
+ }
-.searchfilterbar {
- right: @layout-touch-icon-width + .25rem;
+ .input-group-prepend {
+ width: 30%;
+ }
- &.active a.button.filter {
- color: @color-searchbar-icon-active;
+ label {
+ width: 100%;
+ }
}
-}
-html.ie11 {
- .searchbar {
- a.button.filter:before,
- a.button.search:before {
- line-height: 3.2;
- font-size: 1.25rem;
- }
+ .formbuttons {
+ // this is needed because we hide .formbuttons on small devices
+ // we don't want it for search options form
+ display: block !important;
}
}
diff --git a/skins/elastic/styles/widgets/toolbar.less b/skins/elastic/styles/widgets/toolbar.less
index b874d0b42..c0a5f94b1 100644
--- a/skins/elastic/styles/widgets/toolbar.less
+++ b/skins/elastic/styles/widgets/toolbar.less
@@ -1,745 +1,850 @@
/**
* Roundcube webmail styles for the Elastic skin
*
* Copyright (c) 2017-2018, The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** Toolbar widget ***/
.toolbar {
margin: 0;
font-size: 1rem;
+ text-align: center;
&.listing a,
a {
text-decoration: none;
cursor: pointer !important; // TODO: re-consider .listing class use on toolbar
color: @color-toolbar-button;
}
a.button {
display: inline-block;
border: 0 !important;
height: @layout-header-height;
min-width: 3.5rem;
max-width: 6rem;
width: auto; // reset width defined for links in .listing
padding: .45rem;
text-align: center;
text-overflow: ellipsis;
overflow-x: hidden;
line-height: 1.5;
&:hover,
&:focus {
outline: 0;
}
&.selected {
color: @color-success;
}
&:before {
&:extend(.font-icon-class);
display: inline;
float: none;
margin: 0;
line-height: 1.75;
}
&.reply:before {
content: @fa-var-reply;
}
&.reply-all:before {
content: @fa-var-reply-all;
}
&.forward:before {
content: @fa-var-share;
}
&.delete:before {
content: @fa-var-trash-alt;
}
&.markmessage:before {
content: @fa-var-tag;
}
&.more:before {
content: @fa-var-ellipsis-h;
}
&.dropdown:before {
content: @fa-var-caret-down;
}
&.settings:before {
content: @fa-var-sliders-h;
}
&.create:before {
content: @fa-var-plus-square;
}
&.move:before {
content: @fa-var-arrows-alt;
}
&.purge:before {
content: @fa-var-eraser;
}
&.print:before {
content: @fa-var-print;
}
&.search:before {
content: @fa-var-search;
}
&.upload:before,
&.import:before {
content: @fa-var-upload;
}
&.download:before,
&.export:before {
content: @fa-var-download;
}
&.compose:before {
content: @fa-var-edit;
}
&.archive:before {
content: @fa-var-archive;
}
&.junk:before {
content: @fa-var-fire;
}
&.enigma:before,
&.encrypt:before {
content: @fa-var-lock;
}
&.firstpage:before {
content: @fa-var-fast-backward;
}
&.prev:before {
content: @fa-var-arrow-left;
}
&.next:before {
content: @fa-var-arrow-right;
}
&.prevpage:before {
content: @fa-var-backward;
}
&.nextpage:before {
content: @fa-var-forward;
}
&.lastpage:before {
content: @fa-var-fast-forward;
}
&.send:before {
content: @fa-var-paper-plane;
}
&.back:before {
content: @fa-var-arrow-left;
}
&.closewin:before {
content: @fa-var-window-close;
}
&.save:before {
.font-icon-regular(@fa-var-save);
}
&.vcard:before,
&.attach:before {
content: @fa-var-paperclip;
}
&.spellcheck:before {
content: @fa-var-magic; // TODO
}
&.signature:before {
content: @fa-var-id-card;
}
&.responses:before {
content: @fa-var-comment;
}
&.select:before {
.font-icon-regular(@fa-var-check-square);
}
&.threads:before {
content: @fa-var-comments;
}
&.actions:before {
content: @fa-var-cog;
}
&.refresh:before {
content: @fa-var-sync;
}
&.addto:before {
content: @fa-var-user-plus;
}
&.addcc:before {
content: @fa-var-user-plus;
}
&.addbcc:before {
content: @fa-var-user-plus;
}
&.addressbook:before {
content: @fa-var-user;
}
&.expand:before {
content: @fa-var-caret-down;
}
&.collapse:before {
content: @fa-var-caret-up;
}
&.submit:before {
content: @fa-var-check;
}
&.edit:before {
content: @fa-var-pencil-alt;
}
&.qrcode:before {
content: @fa-var-qrcode;
}
&.properties:before {
content: @fa-var-file;
}
&.zoomin:before {
content: @fa-var-search-plus;
}
&.zoomout:before {
content: @fa-var-search-minus;
}
&.rotate:before {
content: @fa-var-redo-alt;
}
}
&:not(.popupmenu) span.inner {
font-size: 90%;
font-weight: normal;
}
.dropbutton {
a.button.dropdown {
font-size: 75%;
overflow: hidden; // for IE11
span.inner {
display: none;
}
}
a.button:first-child {
padding-right: 0;
}
}
& > .spacer {
width: 1em;
}
&.pagenav {
text-align: center;
display: flex;
- border-bottom: 1px solid @color-layout-border;
- height: @layout-pagenav-height;
- overflow: hidden;
a.button {
flex-grow: 1;
font-size: 80%;
min-width: 2rem !important;
- height: @layout-pagenav-height !important;
- color: @color-list-pagenav;
- overflow: hidden;
+ height: @layout-footer-small-height;
&:before {
- line-height: 1 !important;
+ line-height: 1.5 !important;
}
}
.pagenav-text {
.overflow-ellipsis;
flex-grow: 4;
- line-height: @layout-pagenav-height;
font-size: 80%;
- color: @color-list-pagenav;
white-space: nowrap;
}
input {
width: 3rem;
font-size: 90%;
- line-height: 1;
- padding: .25rem;
- margin: .15rem;
- display: inline;
+ margin: .35rem;
text-align: center;
}
span.inner {
display: none;
}
&.pagenav-list {
cursor: pointer;
a.button {
flex-grow: initial;
}
.pagenav-text {
text-align: left;
}
& + .navlist {
background-color: #fbfbfb;
}
}
}
&.footer {
a.button:before {
height: 1.75rem;
float: none;
display: block;
width: auto;
}
}
&.content-frame-navigation.hide-nav-buttons {
a.next,
a.prev {
display: none;
}
}
.listselectors {
max-width: 100%;
display: flex;
justify-content: space-around;
}
}
.header {
a.button {
color: @color-toolbar-button;
}
.buttons {
display: block;
button {
display: block;
float: left;
cursor: pointer;
color: @color-toolbar-button;
background-color: transparent;
border: 0;
padding: 0;
height: @layout-touch-header-height;
line-height: @layout-touch-header-height;
width: 2.5em;
&:before {
font-size: 1.75rem;
}
}
a.button {
display: inline-block;
&:before {
&:extend(.font-icon-class);
float: none;
}
&.reply:before {
content: @fa-var-reply;
}
&.send:before {
content: @fa-var-paper-plane;
}
}
}
}
.popupmenu.toolbar.listing {
a.button {
max-width: 100%;
width: 100%;
text-align: left;
line-height: @layout-touch-menu-record-height;
height: @layout-touch-menu-record-height;
&:before {
display: inline-block;
line-height: inherit;
margin-right: .5rem;
}
}
.dropbutton {
display: flex;
a.button:first-child {
.overflow-ellipsis;
flex: 1;
}
a.button.dropdown {
font-size: 100%;
cursor: pointer;
width: auto;
&:before {
content: @fa-var-angle-right;
margin-left: .5em;
margin-right: 0;
}
span.inner {
display: none;
}
}
}
li.spacer {
display: none;
}
li:last-child {
border: 0;
}
}
.toolbarmenu li {
&.separator {
// TODO: all separator properties
line-height: 1.5rem !important;
font-size: .75rem !important;
padding: 0 .5rem;
color: @color-popover-separator;
background-color: @color-popover-separator-background;
label {
margin: 0; // Unsets Bootstrap label margin, bug?
}
}
&.checkbox > label {
margin: 0;
width: 100%;
input.icon-checkbox {
right: auto;
& + label {
left: 0;
margin: 0 .2em 0 .35em;
font-size: 1.1rem;
}
}
}
a {
opacity: .5;
&.active {
opacity: 1;
}
}
&:last-child {
border-bottom: none;
}
a:before {
&:extend(.font-icon-class);
}
a.print:before {
content: @fa-var-print;
}
a.copy:before {
content: @fa-var-copy;
}
a.move:before {
content: @fa-var-arrows-alt;
}
a.purge:before {
content: @fa-var-eraser;
}
a.source:before {
content: @fa-var-file-code;
}
a.download:before {
content: @fa-var-download;
}
a.extwin:before {
content: @fa-var-external-link-square-alt;
}
a.create:before {
content: @fa-var-plus-square;
}
a.edit:before {
content: @fa-var-edit;
}
a.edit.asnew:before {
content: @fa-var-pencil-alt;
}
a.rename:before {
content: @fa-var-pencil-alt;
}
a.read:before {
.font-icon-regular(@fa-var-star);
}
a.unread:before {
content: @fa-var-star;
}
a.flag:before {
content: @fa-var-flag;
}
a.unflag:before {
.font-icon-regular(@fa-var-flag);
}
a.filterlink:before {
content: @fa-var-filter;
}
a.reply.list:before,
a.reply.all:before {
content: @fa-var-reply-all;
}
a.forward:before,
a.forward.bounce:before,
a.forward.attachment:before,
a.forward.inline:before {
content: @fa-var-share;
}
a.download.mbox:before,
a.download.eml:before,
a.download.maildir:before {
content: @fa-var-download;
}
a.export.selection:before,
a.export.all:before {
content: @fa-var-download;
}
a.select.all:before {
.font-icon-regular(@fa-var-check-square);
}
a.select.none:before {
content: @fa-var-times;
}
a.select.page:before {
content: @fa-var-bars;
}
a.select.flagged:before {
content: @fa-var-flag;
}
a.select.unread:before {
content: @fa-var-star;
}
a.select.invert:before {
.font-icon-regular(@fa-var-square);
}
a.expand.all:before,
a.expand.unread:before,
a.expand.none:before {
content: @fa-var-comments;
}
a.search:before {
content: @fa-var-search;
}
a.delete:before {
content: @fa-var-trash-alt;
}
a.expunge:before {
content: @fa-var-compress;
}
a.import:before {
content: @fa-var-upload;
}
a.settings:before {
content: @fa-var-sliders-h;
}
a.insertresponse:before {
content: @fa-var-comment;
}
a.compose:before {
content: @fa-var-edit;
}
a.addressbook:before {
content: @fa-var-user;
}
a.recipient:before {
.font-icon-regular(@fa-var-envelope);
}
a.status:before {
.font-icon-regular(@fa-var-lightbulb);
}
a.folders:before {
content: @fa-var-folder;
}
a.remove:before {
content: @fa-var-eraser;
}
a.showurl:before {
content: @fa-var-link;
}
a.qrcode:before {
content: @fa-var-qrcode;
}
a.assigngroup:before {
content: @fa-var-user-plus;
}
a.removegroup:before {
content: @fa-var-user-times;
}
a.vcard:before {
content: @fa-var-paperclip;
}
a.encrypt:before {
content: @fa-var-lock;
}
a.encrypt.sign:before {
content: @fa-var-lock; // TODO
}
}
.toolbarmenu.listing li {
&.checkbox > label {
padding: 0 .5rem 0 2.5em;
}
&:hover {
&.checkbox > label,
input.icon-checkbox + label:before,
a.active {
color: @color-toolbarmenu-hover;
background-color: @color-toolbarmenu-hover-background;
}
}
}
-#layout > .sidebar > .header,
-#layout > .list > .header {
- span.inner {
- display: none;
- }
-}
-
-#layout > .content > * > .toolbar {
- text-align: center;
-}
-
html.touch {
.toolbarmenu.listing td,
.toolbarmenu.listing li,
#layout > :not(.content) > .header a.button {
font-size: 1.2rem;
}
.toolbarmenu.listing li {
&.checkbox > label {
padding: 0 .5rem 0 2.75rem;
}
}
.toolbarmenu li {
input.icon-checkbox + label {
font-size: 1.3rem;
}
}
}
html.ie11 .toolbar .dropbutton a.dropdown:before {
font-size: 80%;
}
@media screen and (min-width: (@screen-width-small + 1px)) {
ul.toolbar {
flex: 1;
}
.toolbar {
a.button {
&:not(.disabled):focus,
&:not(.disabled):hover {
background-color: @color-toolbar-button-background-hover;
}
}
&.listing li {
display: inline-block;
border: 0;
a.button:before {
height: 1.75rem;
float: none;
display: block;
width: auto;
margin: 0;
}
}
.dropbutton {
height: @layout-header-height;
display: inline-block;
&:hover {
background-color: @color-toolbar-button-background-hover;
}
a.button.dropdown {
min-width: 1.1rem;
padding: 0 .3rem;
&:hover {
background-color: darken(@color-toolbar-button-background-hover, 5%);
}
&:before {
height: @layout-header-height;
line-height: 4.2;
}
}
}
}
.toolbar.content-frame-navigation {
display: none !important;
}
.header a.button.icon {
&:not(.disabled):focus,
&:not(.disabled):hover {
background-color: @color-toolbar-button-background-hover;
outline: 0;
}
&:before {
margin: 0;
}
}
}
@media screen and (max-width: @screen-width-small) {
body > #layout > div > .toolbar.footer {
justify-content: space-around;
& > * {
flex-grow: 1;
}
.buttons {
display: flex;
justify-content: space-around;
}
.listselectors > * {
flex-grow: 1;
}
}
.toolbar.listing a {
color: @color-font;
}
}
+
+/*** Searchbar and searchoptions widgets ***/
+
+.searchbar {
+ height: @layout-searchbar-height;
+ line-height: @layout-searchbar-height;
+ background-color: @color-searchbar-background;
+ border-bottom: 1px solid @color-list-border;
+ display: flex;
+ align-items: center;
+ white-space: nowrap;
+ padding: 0 .5rem;
+ overflow: hidden;
+ position: relative;
+
+ form {
+ flex: 1;
+ display: flex;
+
+ &:before {
+ &:extend(.font-icon-class);
+ content: @fa-var-search;
+ height: @layout-searchbar-height;
+ }
+ }
+
+ input {
+ width: 100%;
+ border: 0;
+ background: transparent;
+ padding: .5rem;
+ }
+
+ a.button {
+ height: @layout-searchbar-height;
+ min-width: auto;
+
+ &:before {
+ &:extend(.font-icon-class);
+ }
+
+
+ &.options {
+ &:before {
+ content: @fa-var-filter;
+ font-size: 1em;
+ }
+ }
+
+ &.reset {
+ display: none;
+ position: absolute;
+ right: .5rem;
+
+ &:before {
+ content: @fa-var-times;
+ font-size: 1em;
+ }
+ }
+
+ &.options + .reset {
+ right: 2.5rem;
+ }
+
+ &.search {
+ display: none;
+ }
+ }
+
+ span.inner {
+ display: none;
+ }
+
+ &.active {
+ form:before {
+ color: @color-searchbar-icon-active;
+ }
+
+ a.button.reset {
+ display: inline;
+ }
+ }
+
+ &.open a.button.options:before {
+ content: @fa-var-angle-up;
+ font-size: 1.25em;
+ line-height: 1.25;
+ }
+}
+
+.searchoptions {
+ button.search {
+ width: 100%;
+ }
+
+ ul.proplist {
+ li {
+ margin: 0;
+ }
+
+ & + div {
+ margin-top: 1rem;
+ }
+ }
+
+ .input-group {
+ &:not(:last-child) {
+ margin-bottom: .5rem;
+ }
+
+ .input-group-prepend {
+ width: 30%;
+ }
+
+ label {
+ width: 100%;
+ }
+ }
+
+ .formbuttons {
+ // this is needed because we hide .formbuttons on small devices
+ // we don't want it for search options form
+ display: block !important;
+ }
+}
diff --git a/skins/elastic/templates/addressbook.html b/skins/elastic/templates/addressbook.html
index 67377afc5..d26b69176 100644
--- a/skins/elastic/templates/addressbook.html
+++ b/skins/elastic/templates/addressbook.html
@@ -1,152 +1,150 @@
<roundcube:include file="includes/layout.html" />
<roundcube:include file="includes/menu.html" />
<h1 class="voice"><roundcube:label name="addressbook" /></h1>
<!-- sources/groups list -->
<div class="sidebar listbox" role="navigation" aria-labelledby="directorylist-header">
<div class="header">
<a class="button icon back-list-button" href="#back"><span class="inner"><roundcube:label name="back" /></span></a>
<span id="directorylist-header" class="header-title"><roundcube:label name="groups" /></span>
</div>
<div class="scroller">
<roundcube:object name="directorylist" id="directorylist" class="treelist listing iconized" />
<h3 class="voice"><roundcube:label name="savedsearches" /></h3>
<roundcube:object name="savedsearchlist" id="savedsearchlist" class="treelist listing iconized" />
</div>
<div class="footer toolbar" role="toolbar">
<roundcube:button command="group-create" type="link" title="newgroup" label="addgroup"
class="button create disabled" classAct="button create" innerClass="inner" />
<roundcube:button name="groupoptions" type="link" title="arialabelabookgroupoptions" label="actions"
class="button actions" innerClass="inner" data-popup="groupoptions-menu" />
</div>
</div>
<!-- contacts list -->
<div class="list listbox selected" aria-labelledby="aria-label-contactslist">
<div class="header">
<a class="button icon menu-button" href="#menu"><span class="inner"><roundcube:label name="menu" /></span></a>
<a class="button icon back-sidebar-button folders" href="#sidebar"><span class="inner"><roundcube:label name="groups" /></span></a>
<roundcube:object name="addresslisttitle" label="contacts" tag="span" class="header-title" />
- <roundcube:object name="searchform" id="searchform" wrapper="searchbar toolbar"
- label="contactsearchform" buttontitle="findcontacts" options="search-menu" ariatag="h2" />
<a class="button icon toolbar-menu-button" href="#list-menu"><span class="inner"><roundcube:label name="menu" /></span></a>
</div>
- <roundcube:include file="includes/pagenav.html" />
+ <roundcube:object name="searchform" id="searchform" wrapper="searchbar toolbar"
+ label="contactsearchform" buttontitle="findcontacts" options="searchmenu" ariatag="h2" />
+ <div id="searchmenu" class="hidden searchoptions scroller propform formcontainer" aria-labelledby="aria-label-search-menu">
+ <h3 id="aria-label-search-menu" class="voice"><roundcube:label name="searchmod" /></h3>
+ <div class="formcontent">
+ <ul class="proplist">
+ <li><label><input type="checkbox" name="s_mods[]" value="name" /><roundcube:label name="name" /></label></li>
+ <li><label><input type="checkbox" name="s_mods[]" value="firstname" /><roundcube:label name="firstname" /></label></li>
+ <li><label><input type="checkbox" name="s_mods[]" value="surname" /><roundcube:label name="surname" /></label></li>
+ <li><label><input type="checkbox" name="s_mods[]" value="email" /><roundcube:label name="email" /></label></li>
+ <li><label><input type="checkbox" name="s_mods[]" value="*" /><roundcube:label name="allfields" /></label></li>
+ </ul>
+ </div>
+ <div class="formbuttons">
+ <button class="btn btn-primary icon search" href="#" onclick="return rcmail.command('search')"><roundcube:label name="search" /></button>
+ </div>
+ </div>
<div class="scroller">
<h2 id="aria-label-contactslist" class="voice"><roundcube:label name="contacts" /></h2>
<roundcube:object name="addresslist" id="contacts-table" class="listing iconized contactlist"
noheader="true" role="listbox" data-list="contact_list"
data-label-msg="listempty" data-label-ext="listusebutton" data-create-command="add" />
</div>
+ <roundcube:include file="includes/pagenav.html" />
</div>
<!-- contact details frame -->
<div class="content" role="main">
<h2 id="aria-label-toolbar" class="voice"><roundcube:label name="arialabeltoolbar" /></h2>
<div class="header" role="toolbar" aria-labelledby="aria-label-toolbar">
<a class="button icon back-list-button" href="#back"><span class="inner"><roundcube:label name="back" /></span></a>
<span class="header-title"></span>
<!-- toolbar -->
<div id="addressbooktoolbar" class="toolbar">
<roundcube:button command="add" type="link"
class="button create disabled" classAct="button create"
label="create" title="newcontact" innerclass="inner" data-fab="true" />
<roundcube:button command="print" type="link" data-hidden="small"
class="button print disabled" classAct="button print"
label="print" title="printcontact" innerclass="inner" />
<roundcube:button command="delete" type="link"
class="button delete disabled" classAct="button delete"
label="delete" title="deletecontact" innerClass="inner" />
<span class="spacer"></span>
-<!--
- <roundcube:button command="compose" type="link"
- class="button compose disabled" classAct="button compose"
- label="compose" title="writenewmessage" innerclass="inner" />
--->
<roundcube:button command="advanced-search" type="link"
class="button search disabled" classAct="button search"
label="search" title="advsearch" innerclass="inner" />
<roundcube:container name="toolbar" id="addressbooktoolbar" />
<roundcube:button command="import" type="link"
class="button import disabled" classAct="button import"
label="import" title="importcontacts" innerclass="inner" />
<span class="dropbutton">
<roundcube:button command="export" type="link"
class="button export disabled" classAct="button export"
label="export" title="exportvcards" innerclass="inner" />
<a href="#export" class="button dropdown" data-popup="export-menu">
<span class="inner"><roundcube:label name="arialabelcontactexportoptions" /></span>
</a>
</span>
<roundcube:button name="contactmenulink" id="contactmenulink" type="link"
class="button more" label="more" title="moreactions"
data-popup="contact-menu" innerclass="inner" />
</div>
</div>
<h2 id="aria-label-contact-frame" class="voice"><roundcube:label name="contactproperties" /></h2>
<div class="iframe-wrapper">
<roundcube:object name="addressframe" id="contact-frame" src="/watermark.html" title="contactproperties"
aria-labelledby="aria-label-contact-frame" />
</div>
</div>
<!-- popup menus -->
<div id="export-menu" class="popupmenu">
<h3 id="aria-label-export-menu" class="voice"><roundcube:label name="arialabelcontactexportoptions" /></h3>
<ul class="toolbarmenu listing" role="menu" aria-labelledby="aria-label-export-menu">
<roundcube:button type="link-menuitem" command="export" label="exportall" prop="sub" class="export all" classAct="export all active" />
<roundcube:button type="link-menuitem" command="export-selected" label="exportsel" prop="sub" class="export selection" classAct="export selection active" />
</ul>
</div>
<div id="groupoptions-menu" class="popupmenu">
<h3 id="aria-label-groupoptions-menu" class="voice"><roundcube:label name="arialabelabookgroupoptions" /></h3>
<ul class="toolbarmenu listing" role="menu" aria-labelledby="aria-label-groupoptions-menu">
<roundcube:button type="link-menuitem" command="group-rename" label="grouprename" class="group rename" classAct="group rename active" />
<roundcube:button type="link-menuitem" command="group-delete" label="groupdelete" class="group delete" classAct="group delete active" />
<roundcube:button type="link-menuitem" command="search-create" label="searchsave" class="search" classAct="search active" />
<roundcube:button type="link-menuitem" command="search-delete" label="searchdelete" class="search delete" classAct="search delete active" />
<roundcube:container name="groupoptions" id="groupoptionsmenu" />
</ul>
</div>
-<div id="search-menu" class="popupmenu form" data-editable="true" data-popup-init="searchmenu">
- <h3 id="aria-label-search-menu" class="voice"><roundcube:label name="searchmod" /></h3>
- <ul class="toolbarmenu" role="menu" aria-labelledby="aria-label-search-menu">
- <li role="menuitem" class="checkbox"><label><input type="checkbox" name="s_mods[]" value="name" id="s_mod_name" /><roundcube:label name="name" /></label></li>
- <li role="menuitem" class="checkbox"><label><input type="checkbox" name="s_mods[]" value="firstname" id="s_mod_firstname" /><roundcube:label name="firstname" /></label></li>
- <li role="menuitem" class="checkbox"><label><input type="checkbox" name="s_mods[]" value="surname" id="s_mod_surname" /><roundcube:label name="surname" /></label></li>
- <li role="menuitem" class="checkbox"><label><input type="checkbox" name="s_mods[]" value="email" id="s_mod_email" /><roundcube:label name="email" /></label></li>
- <li role="menuitem" class="checkbox"><label><input type="checkbox" name="s_mods[]" value="*" id="s_mod_all" /><roundcube:label name="allfields" /></label></li>
- </ul>
- <div class="buttons"><button class="btn btn-primary icon search" href="#" onclick="if (rcmail.command('search')) UI.menu_hide('search-menu')"><roundcube:label name="search" /></button></div>
-</div>
-
<div id="dragcontact-menu" class="popupmenu">
<h3 id="aria-label-dragcontact-menu" class="voice"><roundcube:label name="arialabeldropactionmenu" /></h3>
<ul class="toolbarmenu listing" role="menu" aria-labelledby="aria-label-dragcontact-menu">
<roundcube:button type="link-menuitem" command="move" onclick="return rcmail.drag_menu_action('move')" label="move" classAct="active" />
<roundcube:button type="link-menuitem" command="copy" onclick="return rcmail.drag_menu_action('copy')" label="copy" classAct="active" />
</ul>
</div>
<div id="contact-menu" class="popupmenu">
<h3 id="aria-label-contact-menu" class="voice"><roundcube:label name="arialabelmorecontactactions" /></h3>
<ul class="toolbarmenu listing" role="menu" aria-labelledby="aria-label-contact-menu">
<roundcube:if condition="env:qrcode" />
<roundcube:button type="link-menuitem" command="qrcode" label="qrcode" class="qrcode" classAct="qrcode active" />
<roundcube:endif />
<roundcube:button type="link-menuitem" command="group-assign-selected" label="groupassign" class="assigngroup" classAct="assigngroup active" innerclass="folder-selector-link" aria-haspopup="true" />
<roundcube:button type="link-menuitem" command="group-remove-selected" label="groupremove" class="removegroup" classAct="removegroup active" />
<roundcube:if condition="env:contact_move_enabled" />
<roundcube:button type="link-menuitem" command="move" label="moveto" class="move" classAct="move active" innerclass="folder-selector-link" aria-haspopup="true" />
<roundcube:endif />
<roundcube:if condition="env:contact_copy_enabled" />
<roundcube:button type="link-menuitem" command="copy" label="copyto" class="copy" classAct="copy active" innerclass="folder-selector-link" aria-haspopup="true" />
<roundcube:endif />
<roundcube:container name="contactmenu" id="contact-menu" />
</ul>
</div>
<roundcube:include file="includes/footer.html" />
diff --git a/skins/elastic/templates/compose.html b/skins/elastic/templates/compose.html
index b72f9321b..fc14c9ba4 100644
--- a/skins/elastic/templates/compose.html
+++ b/skins/elastic/templates/compose.html
@@ -1,284 +1,284 @@
<roundcube:include file="includes/layout.html" />
<roundcube:include file="includes/menu.html" condition="!env:extwin && !env:framed" />
<roundcube:add_label name="recipientsadded" />
<roundcube:add_label name="nocontactselected" />
<roundcube:add_label name="recipient" />
<roundcube:add_label name="recipientedit" />
<h1 class="voice"><roundcube:label name="compose" /></h1>
<!-- inline address book -->
<div class="sidebar listbox" role="region" aria-labelledby="aria-label-composecontacts">
<div class="header no-toolbar">
<a class="button icon back-content-button" href="#content" data-hidden="big"><span class="inner"><roundcube:label name="back" /></span></a>
<span id="aria-label-composecontacts" class="header-title"><roundcube:label name="contacts" /></span>
- <roundcube:object name="searchform" id="searchform" wrapper="searchbar toolbar"
- label="contactsearchform" buttontitle="findcontacts" ariatag="h2" />
</div>
- <roundcube:include file="includes/pagenav.html" />
+ <roundcube:object name="searchform" id="searchform" wrapper="searchbar toolbar"
+ label="contactsearchform" buttontitle="findcontacts" ariatag="h2" />
<div class="scroller" tabindex="-1">
<roundcube:object name="addressbooks" id="directorylist" class="treelist listing iconized"
summary="ariasummarycomposecontacts" />
<roundcube:object name="addresslist" id="contacts-table" class="listing iconized contactlist"
noheader="true" role="listbox" data-list="contact_list" />
</div>
<div class="footer toolbar" role="toolbar">
<roundcube:button command="add-recipient" prop="to" type="link" title="to"
class="button addto disabled" classAct="button addto" innerClass="inner" content="To+" />
<roundcube:button command="add-recipient" prop="cc" type="link" title="cc"
class="button addcc disabled" classAct="button addcc" innerClass="inner" content="Cc+" />
<roundcube:button command="add-recipient" prop="bcc" type="link" title="bcc"
class="button addbcc disabled" classAct="button addbcc" innerClass="inner" content="Bcc+" />
<roundcube:container name="compose-contacts-toolbar" id="compose-contacts-toolbar" />
</div>
+ <roundcube:include file="includes/pagenav.html" />
</div>
<!-- compose options and attachments list -->
<div class="list listbox">
<div class="header">
<a class="button icon back-content-button" href="#content" data-hidden="big"><span class="inner"><roundcube:label name="back" /></span></a>
- <span class="header-title"><roundcube:label name="optionsandattachments" /></span>
+ <span class="header-title all-sizes"><roundcube:label name="optionsandattachments" /></span>
</div>
<div class="scroller">
<!-- compose options -->
<div id="compose-options" class="formcontent" role="region" aria-labelledby="aria-label-composeoptions">
<h2 id="aria-label-composeoptions" class="voice"><roundcube:label name="arialabelcomposeoptions" /></h2>
<roundcube:if condition="!in_array('mdn_default', (array)config:dont_override)" />
<div class="form-group row form-check">
<label for="compose-mdn" class="col-form-label col-6"><roundcube:label name="returnreceipt" /></label>
<div class="col-6 form-check">
<roundcube:object name="mdnCheckBox" id="compose-mdn" noform="true" tabindex="2" class="form-check-input" />
</div>
</div>
<roundcube:endif />
<roundcube:if condition="!in_array('dsn_default', (array)config:dont_override)" />
<div class="form-group row form-check">
<label for="compose-dsn" class="col-form-label col-6"><roundcube:label name="dsn" /></label>
<div class="col-6 form-check">
<roundcube:object name="dsnCheckBox" id="compose-dsn" noform="true" tabindex="2" class="form-check-input" />
</div>
</div>
<roundcube:endif />
<div class="form-group row">
<label for="compose-priority" class="col-form-label col-6"><roundcube:label name="priority" /></label>
<div class="col-6">
<roundcube:object name="prioritySelector" id="compose-priority" noform="true" tabindex="2" class="form-control" />
</div>
</div>
<roundcube:if condition="!config:no_save_sent_messages" />
<div class="form-group row">
<label for="compose-store-target" class="col-form-label col-6"><roundcube:label name="savesentmessagein" /></label>
<div class="col-6">
<roundcube:object name="storetarget" id="compose-store-target" noform="true" tabindex="2" class="form-control" />
</div>
</div>
<roundcube:endif />
<roundcube:container name="composeoptions" id="compose-options" />
<roundcube:if condition="!in_array('htmleditor', (array)config:dont_override)" />
<div class="form-group row hidden">
<label for="editor-selector" class="col-form-label col-6"><roundcube:label name="editortype" /></label>
<div class="col-6">
<roundcube:object name="editorSelector" id="editor-selector" editorid="composebody" noform="true" tabindex="2" class="form-control" />
</div>
</div>
<roundcube:endif />
</div>
<div id="compose-attachments" class="file-upload" role="region" aria-labelledby="aria-label-compose-attachments">
<h2 id="aria-label-compose-attachments" class="voice"><roundcube:label name="attachments" /></h2>
<div class="upload-form">
<roundcube:object name="composeAttachmentForm" mode="hint" />
<button class="btn btn-secondary attach" tabindex="2" href="#" onclick="rcmail.upload_input('uploadform')"><roundcube:label name="addattachment" /></button>
</div>
<roundcube:object name="composeAttachmentList" id="attachment-list" class="attachmentslist" tabindex="2" />
<roundcube:object name="fileDropArea" id="compose-attachments" />
</div>
</div>
</div>
<div class="content listbox selected" role="main">
<h2 id="aria-label-toolbar" class="voice"><roundcube:label name="arialabeltoolbar" /></h2>
<div class="header">
<a class="button icon menu-button" href="#menu"><span class="inner"><roundcube:label name="menu" /></span></a>
<span class="header-title"><roundcube:label name="compose" /></span>
<!-- toolbar -->
<div id="messagetoolbar" class="toolbar" role="toolbar" aria-labelledby="aria-label-toolbar">
<a class="button settings" href="#options" onclick="UI.show_list(true)" data-hidden="big">
<span class="inner"><roundcube:label name="optionsandattachments"></span>
</a>
<a class="button addressbook" href="#contacts" onclick="UI.show_sidebar()" data-hidden="big">
<span class="inner"><roundcube:label name="contacts"></span>
</a>
<roundcube:button command="savedraft" type="link" class="button save draft disabled" classAct="button save draft"
label="save" title="savemessage" tabindex="2" innerclass="inner" data-content-button="true" />
<span class="spacer"></span>
<roundcube:button name="addattachment" type="link" class="button attach"
label="attach" title="addattachment" data-hidden="small"
onclick="if (!$(this).is('.disabled')) rcmail.upload_input('uploadform')"
aria-haspopup="true" aria-expanded="false" tabindex="2" innerclass="inner" />
<roundcube:button command="insert-sig" type="link" class="button signature disabled" classAct="button signature"
label="signature" title="insertsignature" tabindex="2" innerclass="inner" />
<a href="#responses" class="button responses" label="responses" title="<roundcube:label name='insertresponse' />" unselectable="on" tabindex="2" data-popup="responses-menu">
<span class="inner"><roundcube:label name="responses" /></span>
</a>
<a id="composeoptionslink" class="button settings hidden" href="#options" onclick="UI.show_list(); $(this).addClass('hidden'); $('#composecontactslink').removeClass('hidden')" data-hidden="large,small">
<span class="inner"><roundcube:label name="options"></span>
</a>
<a id="composecontactslink" class="button addressbook" href="#contacts" onclick="UI.show_sidebar(true); $(this).addClass('hidden'); $('#composeoptionslink').removeClass('hidden')" data-hidden="large,small">
<span class="inner"><roundcube:label name="contacts"></span>
</a>
<roundcube:if condition="config:enable_spellcheck" />
<span class="dropbutton">
<roundcube:button command="spellcheck" type="link" class="button spellcheck disabled"
classAct="button spellcheck" classSel="button spellcheck pressed"
label="spellcheck" title="checkspelling" tabindex="2" innerclass="inner" />
<a href="#languages" class="button dropdown" tabindex="2" data-popup="spell-menu">
<span class="inner"><roundcube:label name="language" /></span>
</a>
</span>
<roundcube:endif />
<span class="dropbutton" style="display:none">
<roundcube:button command="compose-encrypted" type="link" class="button encrypt disabled"
classAct="button encrypt" classSel="button encrypt selected" innerclass="inner"
label="encrypt" title="encryptmessagemailvelope" tabindex="2" />
<a href="#encryption" id="encryption-menu-button" class="button dropdown" tabindex="2" data-popup="encryption-menu">
<span class="inner"><roundcube:label name="encryptmessagemailvelope" /></span>
</a>
</span>
<roundcube:container name="toolbar" id="compose-toolbar" />
</div>
</div>
<div id="compose-content" class="formcontainer content scroller">
<roundcube:object name="composeFormHead" role="main" class="formcontent" />
<!-- message headers -->
<div id="compose-headers" role="region" aria-labelledby="aria-label-composeheaders">
<h2 id="aria-label-composeheaders" class="voice"><roundcube:label name="arialabelmessageheaders" /></h2>
<div class="compose-headers">
<div id="compose_from" class="form-group row">
<label for="_from" class="col-2 col-form-label"><roundcube:label name="from" /></label>
<div class="col-10">
<div class="input-group">
<roundcube:object name="composeHeaders" part="from" id="_from" form="form" tabindex="1" class="form-control" />
<span class="input-group-append">
<a href="#identities" onclick="return rcmail.command('identities')" class="input-group-text icon edit" title="<roundcube:label name="editidents" />" tabindex="1"><span class="inner"><roundcube:label name="editidents" /></span></a>
</span>
</div>
</div>
</div>
<div id="compose_to" class="form-group row">
<label for="_to" class="col-2 col-form-label"><roundcube:label name="to" /></label>
<div class="col-10">
<div class="input-group">
<roundcube:object name="composeHeaders" part="to" id="_to" form="form" tabindex="1" aria-required="true" data-recipient-input="true" />
<span class="input-group-append">
<a href="#add-header" data-popup="headers-menu" class="input-group-text icon add" title="<roundcube:label name="addheader" />" tabindex="1"><span class="inner"><roundcube:label name="addheader" /></span></a>
</span>
</div>
</div>
</div>
<div id="compose_cc" class="hidden form-group row">
<label for="_cc" class="col-2 col-form-label"><roundcube:label name="cc" /></label>
<div class="col-10">
<div class="input-group">
<roundcube:object name="composeHeaders" part="cc" id="_cc" form="form" tabindex="1" data-recipient-input="true" />
<span class="input-group-append">
<a href="#delete" onclick="$('#_cc').val('').change()" class="input-group-text icon cancel" title="<roundcube:label name='delete' />" tabindex="1"><span class="inner"><roundcube:label name="delete" /></span></a>
</span>
</div>
</div>
</div>
<div id="compose_bcc" class="hidden form-group row">
<label for="_bcc" class="col-2 col-form-label"><roundcube:label name="bcc" /></label>
<div class="col-10">
<div class="input-group">
<roundcube:object name="composeHeaders" part="bcc" id="_bcc" form="form" tabindex="1" data-recipient-input="true" />
<span class="input-group-append">
<a href="#delete" onclick="$('#_bcc').val('').change()" class="input-group-text icon cancel" title="<roundcube:label name='delete' />" tabindex="1"><span class="inner"><roundcube:label name="delete" /></span></a>
</span>
</div>
</div>
</div>
<div id="compose_replyto" class="hidden form-group row">
<label for="_replyto" class="col-2 col-form-label"><roundcube:label name="replyto" /></label>
<div class="col-10">
<div class="input-group">
<roundcube:object name="composeHeaders" part="replyto" id="_replyto" form="form" tabindex="1" data-recipient-input="true" />
<span class="input-group-append">
<a href="#delete" onclick="$('#_replyto').val('').change()" class="input-group-text icon cancel" title="<roundcube:label name='delete' />" tabindex="1"><span class="inner"><roundcube:label name="delete" /></span></a>
</span>
</div>
</div>
</div>
<div id="compose_followupto" class="hidden form-group row">
<label for="_followupto" class="col-2 col-form-label"><roundcube:label name="followupto" /></label>
<div class="col-10">
<div class="input-group">
<roundcube:object name="composeHeaders" part="followupto" id="_followupto" form="form" tabindex="1" data-recipient-input="true" />
<span class="input-group-append">
<a href="#delete" onclick="$('#_followupto').val('').change()" class="input-group-text icon cancel" title="<roundcube:label name='delete' />" tabindex="1"><span class="inner"><roundcube:label name="delete" /></span></a>
</span>
</div>
</div>
</div>
<div id="compose_subject" class="form-group row">
<label for="compose-subject" class="col-2 col-form-label"><roundcube:label name="subject" /></label>
<div class="col-10">
<roundcube:object name="composeSubject" id="compose-subject" form="form" tabindex="1" class="form-control" />
</div>
</div>
</div>
</div>
<!-- message compose body -->
<div id="composebodycontainer">
<label for="composebody" class="voice"><roundcube:label name="arialabelmessagebody" /></label>
<roundcube:object name="composeBody" id="composebody" form="form" cols="70" rows="20" class="form-control" tabindex="1" />
</div>
</form>
<div class="formbuttons">
<roundcube:button command="send" class="btn btn-primary send" label="send" tabindex="1" data-content-button="true" />
</div>
</div>
</div>
<roundcube:object name="composeAttachmentForm" id="uploadform" mode="smart" />
<div id="spell-menu" class="popupmenu" data-popup-init="spellmenu"></div>
<div id="headers-menu" class="popupmenu" data-popup-init="headersmenu">
<h3 id="aria-label-headersmenu" class="voice"><roundcube:label name="arialabelheadersmenu" /></h3>
<ul class="toolbarmenu listing" role="menu" aria-labelledby="aria-label-headersmenu">
<li role="menuitem"><a data-target="cc" href="#" role="button" tabindex="-1" class="recipient"><roundcube:label name="cc" /></a></li>
<li role="menuitem"><a data-target="bcc" href="#" role="button" tabindex="-1" class="recipient"><roundcube:label name="bcc" /></a></li>
<li role="menuitem"><a data-target="replyto" href="#" role="button" tabindex="-1" class="recipient"><roundcube:label name="replyto" /></a></li>
<li role="menuitem"><a data-target="followupto" href="#" role="button" tabindex="-1" class="recipient"><roundcube:label name="followupto" /></a></li>
</ul>
</div>
<div id="responses-menu" class="popupmenu">
<h3 id="aria-label-responsesmenu" class="voice"><roundcube:label name="arialabelresponsesmenu" /></h3>
<ul class="toolbarmenu listing" role="menu" aria-labelledby="aria-label-responsesmenu">
<li role="separator" class="separator"><label><roundcube:label name="insertresponse" /></label></li>
<roundcube:object name="responseslist" id="responseslist" tagname="ul" itemclass="active" />
<li role="separator" class="separator"><label><roundcube:label name="manageresponses" /></label></li>
<roundcube:button command="save-response" type="link-menuitem" label="newresponse" class="create responses" classAct="create responses active" unselectable="on" />
<roundcube:button command="responses" type="link-menuitem" label="editresponses" class="edit responses" classAct="edit responses active" />
</ul>
</div>
<div id="attachmentmenu" class="popupmenu">
<h3 id="aria-label-attachmentmenu" class="voice"><roundcube:label name="arialabelattachmentmenu" /></h3>
<ul class="toolbarmenu listing" role="menu" aria-labelledby="aria-label-attachmentmenu">
<roundcube:button command="open-attachment" id="attachmenuopen" type="link-menuitem" label="open" class="extwin" classAct="extwin active" />
<roundcube:button command="download-attachment" id="attachmenudownload" type="link-menuitem" label="download" class="download" classAct="download active" />
<roundcube:button command="rename-attachment" id="attachmenurename" type="link-menuitem" label="rename" class="rename" classAct="rename active" />
<roundcube:container name="attachmentmenu" id="attachmentoptionsmenu" />
</ul>
</div>
<div id="encryption-menu" class="popupmenu">
<ul class="toolbarmenu listing" role="menu">
<roundcube:button command="compose-encrypted" type="link-menuitem" label="encryptmessage" class="encrypt" classAct="encrypt active" />
<roundcube:button command="compose-encrypted-signed" type="link-menuitem" label="encryptandsign" class="encrypt sign" classAct="encrypt sign active" />
</ul>
</div>
<roundcube:include file="includes/footer.html" />
diff --git a/skins/elastic/templates/folders.html b/skins/elastic/templates/folders.html
index 9a8d47c6e..8a72402a9 100644
--- a/skins/elastic/templates/folders.html
+++ b/skins/elastic/templates/folders.html
@@ -1,59 +1,64 @@
<roundcube:include file="includes/layout.html" />
<roundcube:include file="includes/menu.html" />
<roundcube:include file="includes/settings-menu.html" />
<h1 class="voice"><roundcube:label name="folders" /></h1>
<!-- folders list -->
<div class="list listbox selected" aria-labelledby="aria-label-folderslist">
<div class="header">
<a class="button icon back-sidebar-button" href="#sidebar"><span class="inner"><roundcube:label name="settings" /></span></a>
<span id="aria-label-folderslist" class="header-title"><roundcube:label name="folders" /></span>
- <roundcube:object name="searchform" id="foldersearch" wrapper="searchbar toolbar"
- label="foldersearchform" buttontitle="findfolders" options="search-filter" ariatag="h2" />
<a class="button icon toolbar-menu-button" href="#list-menu"><span class="inner"><roundcube:label name="menu" /></span></a>
</div>
+ <roundcube:object name="searchform" id="foldersearch" wrapper="searchbar toolbar" ariatag="h2"
+ label="foldersearchform" buttontitle="findfolders" options="foldersearchmenu" />
+ <div id="foldersearchmenu" class="hidden searchoptions scroller propform formcontainer" aria-labelledby="aria-label-search-menu" aria-controls="subscription-table">
+ <h3 id="aria-label-search-menu" class="voice"><roundcube:label name="searchmod" /></h3>
+ <div class="formcontent">
+ <roundcube:object name="folderfilter" id="folderlist-filter" noheader="true" />
+ </div>
+ <div class="formbuttons">
+ <button class="btn btn-primary icon search" href="#" onclick="return rcmail.command('search')"><roundcube:label name="search" /></button>
+ </div>
+ </div>
<div class="scroller" tabindex="-1">
<roundcube:object name="foldersubscription" id="subscription-table"
class="treelist listing folderlist iconized" role="listbox" data-list="subscription_list"
data-label-msg="listempty" data-label-ext="listusebutton" data-create-command="folder-create" />
</div>
- <div class="footer">
+ <div class="footer small">
<roundcube:if condition="env:quota" />
<div id="quotadisplay" class="quota-widget">
<span class="voice"><roundcube:label name="quota"></span>
<roundcube:object name="quotaDisplay" class="count" display="text" />
</div>
<roundcube:endif />
</div>
</div>
<!-- folder info frame -->
<div class="content" role="main">
<h2 id="aria-label-toolbar" class="voice"><roundcube:label name="arialabeltoolbar" /></h2>
<div class="header" role="toolbar" aria-labelledby="aria-label-toolbar">
<a class="button icon back-list-button" href="#back"><span class="inner"><roundcube:label name="back" /></span></a>
<span class="header-title"></span>
<!-- toolbar -->
<div id="folderstoolbar" class="toolbar">
<roundcube:button command="create-folder" type="link"
class="button create disabled" classAct="button create"
label="create" title="createfolder" innerClass="inner" data-fab="true" />
<roundcube:button command="delete-folder" type="link"
class="button delete disabled" classAct="button delete"
label="delete" title="delete" innerclass="inner" />
<roundcube:button command="purge" type="link"
class="button purge disabled" classAct="button purge"
label="empty" title="empty" innerclass="inner" />
</div>
</div>
<div class="iframe-wrapper">
<roundcube:object name="folderframe" id="preferences-frame" src="/watermark.html" />
</div>
</div>
-<div id="search-filter" class="popupmenu form nolist toolbarmenu" data-editable="true">
- <roundcube:object name="folderfilter" id="folderlist-filter" noheader="true" />
-</div>
-
<roundcube:include file="includes/footer.html" />
diff --git a/skins/elastic/templates/includes/pagenav.html b/skins/elastic/templates/includes/pagenav.html
index b1d477078..382293393 100644
--- a/skins/elastic/templates/includes/pagenav.html
+++ b/skins/elastic/templates/includes/pagenav.html
@@ -1,24 +1,24 @@
-<div class="pagenav toolbar" role="toolbar">
+<div class="pagenav toolbar footer small" role="toolbar">
<roundcube:button command="firstpage" type="link"
class="button firstpage disabled" classAct="button firstpage"
title="firstpage" label="first" innerclass="inner" />
<roundcube:button command="previouspage" type="link"
class="button prevpage disabled" classAct="button prevpage"
title="previouspage" label="previous" innerclass="inner" />
<roundcube:if condition="template:name == 'mail'" />
<roundcube:object name="messageCountDisplay" class="pagenav-text" aria-live="polite" aria-relevant="text" />
<input class="form-control" type="text" size="3" disabled title="<roundcube:label name="currpage" />" />
<roundcube:elseif condition="template:name == 'addressbook'" />
<span class="pagenav-text" aria-live="polite" aria-relevant="text">
<roundcube:object name="recordsCountDisplay" label="fromtoshort" />
</span>
<roundcube:else />
<span class="pagenav-text" aria-live="polite" aria-relevant="text">&nbsp;</span>
<roundcube:endif />
<roundcube:button command="nextpage" type="link"
class="button nextpage disabled" classAct="button nextpage"
title="nextpage" label="next" innerclass="inner" />
<roundcube:button command="lastpage" type="link"
class="button lastpage disabled" classAct="button lastpage"
title="lastpage" label="last" innerclass="inner" />
</div>
diff --git a/skins/elastic/templates/mail.html b/skins/elastic/templates/mail.html
index 9618cc14e..e5bdc5b23 100644
--- a/skins/elastic/templates/mail.html
+++ b/skins/elastic/templates/mail.html
@@ -1,189 +1,209 @@
<roundcube:include file="includes/layout.html" />
<roundcube:include file="includes/menu.html" />
<h1 class="voice"><roundcube:label name="mail" /></h1>
<!-- folders list -->
<div class="sidebar listbox" role="navigation" aria-labelledby="aria-label-folderlist">
<div class="header">
<a class="button icon back-list-button" href="#back"><span class="inner"><roundcube:label name="back" /></span></a>
<span class="header-title username"><roundcube:object name="username" /></span>
</div>
<h2 id="aria-label-folderlist" class="voice"><roundcube:label name="arialabelfolderlist" /></h2>
<div id="folderlist-content" class="scroller">
<roundcube:object name="mailboxlist" id="mailboxlist" class="treelist listing folderlist" folder_filter="mail" unreadwrap="%s" />
</div>
<div id="folderlist-footer" class="footer toolbar">
<roundcube:button name="folderactions" type="link" title="folderactions" label="actions"
class="button actions" innerclass="inner" data-popup="mailboxoptions-menu" />
+ </div>
+ <div class="footer toolbar small">
<roundcube:if condition="env:quota" />
<div id="quotadisplay" class="quota-widget">
<span class="voice"><roundcube:label name="quota"></span>
<roundcube:object name="quotaDisplay" class="count" display="text" />
</div>
<roundcube:endif />
</div>
</div>
<!-- messages list -->
<div class="list listbox selected">
<div id="messagelist-header" class="header">
<a class="button icon menu-button" href="#menu"><span class="inner"><roundcube:label name="menu" /></span></a>
<a class="button icon back-sidebar-button folders" href="#sidebar"><span class="inner"><roundcube:label name="mailboxlist" /></span></a>
<span class="header-title"></span>
- <roundcube:object name="searchfilter" id="mailsearchfilter" class="searchfilterbar hidden" aria-controls="messagelist" />
- <roundcube:add_label name="filter" />
- <roundcube:object name="searchform" id="mailsearchform" wrapper="searchbar toolbar"
- label="mailquicksearchbox" buttontitle="findmail" options="search-menu" ariatag="h2" />
+ <div class="toolbar" role="toolbar">
+<!--
+ <a href="#select" class="button select" data-popup="listselect-menu" title="<roundcube:label name="select" />"><span class="inner"><roundcube:label name="select" /></span></a>
+-->
+ <roundcube:if condition="env:threads" />
+ <a href="#threads" class="button threads" data-popup="threadselect-menu" title="<roundcube:label name="threads" />"><span class="inner"><roundcube:label name="threads" /></span></a>
+ <roundcube:endif />
+ <roundcube:object name="listmenulink" class="button settings" label="options" innerclass="inner" />
+ <roundcube:button command="checkmail" type="link" class="button refresh" label="refresh" title="checkmail" innerclass="inner" />
+ <roundcube:container name="listcontrols" id="listcontrols" />
+ </div>
<a class="button icon toolbar-menu-button" href="#list-menu"><span class="inner"><roundcube:label name="menu" /></span></a>
</div>
- <roundcube:include file="includes/pagenav.html" />
+ <roundcube:object name="searchform" id="mailsearchform" wrapper="searchbar toolbar"
+ label="mailquicksearchbox" buttontitle="findmail" options="searchmenu" ariatag="h2" />
+ <div id="searchmenu" class="hidden searchoptions scroller propform formcontainer" aria-labelledby="aria-label-search-menu" aria-controls="messagelist">
+ <h3 id="aria-label-search-menu" class="voice"><roundcube:label name="searchmod" /></h3>
+ <div class="formcontent">
+ <ul class="proplist">
+ <li><label><input type="checkbox" name="s_mods[]" value="subject" /><roundcube:label name="subject" /></label></li>
+ <li><label><input type="checkbox" name="s_mods[]" value="from" /><roundcube:label name="from" /></label></li>
+ <li><label><input type="checkbox" name="s_mods[]" value="to" /><roundcube:label name="to" /></label></li>
+ <li><label><input type="checkbox" name="s_mods[]" value="cc" /><roundcube:label name="cc" /></label></li>
+ <li><label><input type="checkbox" name="s_mods[]" value="bcc" /><roundcube:label name="bcc" /></label></li>
+ <li><label><input type="checkbox" name="s_mods[]" value="body" /><roundcube:label name="body" /></label></li>
+ <li><label><input type="checkbox" name="s_mods[]" value="text" /><roundcube:label name="msgtext" /></label></li>
+ </ul>
+ <div class="input-group">
+ <div class="input-group-prepend">
+ <label for="searchfilter" class="input-group-text"><roundcube:label name="type" /></label>
+ </div>
+ <roundcube:object name="searchfilter" id="searchfilter" />
+ </div>
+ <div class="input-group">
+ <div class="input-group-prepend">
+ <label for="s_interval" class="input-group-text"><roundcube:label name="date" /></label>
+ </div>
+ <roundcube:object name="searchinterval" id="s_interval" onchange="rcmail.set_searchinterval($(this).val())" />
+ </div>
+ <div class="input-group">
+ <div class="input-group-prepend">
+ <label for="s_scope" class="input-group-text"><roundcube:label name="searchscope" /></label>
+ </div>
+ <select name="s_scope" id="s_scope">
+ <option value="base"><roundcube:label name="currentfolder" /></option>
+ <option value="sub"><roundcube:label name="subfolders" /></option>
+ <option value="all"><roundcube:label name="allfolders" /></option>
+ </select>
+ </div>
+ </div>
+ <div class="formbuttons">
+ <button class="btn btn-primary icon search" href="#" onclick="return rcmail.command('search')"><roundcube:label name="search" /></button>
+ </div>
+ </div>
<div id="messagelist-content" class="scroller" tabindex="-1">
<h2 id="aria-label-messagelist" class="voice"><roundcube:label name="arialabelmessagelist" /></h2>
<roundcube:object name="messages" id="messagelist" optionsmenuIcon="true"
class="listing messagelist sortheader fixedheader"
aria-labelledby="aria-label-messagelist"
data-list="message_list" data-label-msg="listempty"
/>
</div>
- <div id="messagelist-footer" class="footer toolbar" role="toolbar">
- <div id="listcontrols" class="listselectors">
- <a href="#select" class="button select" data-popup="listselect-menu" title="<roundcube:label name="select" />"><span class="inner"><roundcube:label name="select" /></span></a>
- <roundcube:if condition="env:threads" />
- <a href="#threads" class="button threads" data-popup="threadselect-menu" title="<roundcube:label name="threads" />"><span class="inner"><roundcube:label name="threads" /></span></a>
- <roundcube:endif />
- <roundcube:object name="listmenulink" class="button settings" label="options" innerclass="inner" />
- <roundcube:button command="checkmail" type="link" class="button refresh" label="refresh" title="checkmail" innerclass="inner" />
- <roundcube:container name="listcontrols" id="listcontrols" />
- </div>
- </div>
+ <roundcube:include file="includes/pagenav.html" />
</div>
<!-- message preview -->
<div class="content">
<h2 id="aria-label-toolbar" class="voice"><roundcube:label name="arialabeltoolbar" /></h2>
<div class="header" role="toolbar" aria-labelledby="aria-label-toolbar">
<a class="button icon back-list-button" href="#back"><span class="inner"><roundcube:label name="back" /></span></a>
<span class="header-title"></span>
<roundcube:include file="includes/mail-menu.html" />
</div>
<h2 id="aria-label-mailpreviewframe" class="voice"><roundcube:label name="arialabelmailpreviewframe" /></h2>
<div class="iframe-wrapper">
<roundcube:object name="messagecontentframe"
id="messagecontframe"
aria-labelledby="aria-label-mailpreviewframe"
src="/watermark.html"
title="arialabelmailpreviewframe"
/>
</div>
</div>
<!-- popup menus -->
-<div id="search-menu" class="popupmenu form" data-editable="true" data-popup-init="searchmenu">
- <h3 id="aria-label-search-menu" class="voice"><roundcube:label name="searchmod" /></h3>
- <ul class="toolbarmenu" role="menu" aria-labelledby="aria-label-search-menu">
- <li role="menuitem" class="checkbox"><label><input type="checkbox" name="s_mods[]" value="subject" id="s_mod_subject" /><roundcube:label name="subject" /></label></li>
- <li role="menuitem" class="checkbox"><label><input type="checkbox" name="s_mods[]" value="from" id="s_mod_from" /><roundcube:label name="from" /></label></li>
- <li role="menuitem" class="checkbox"><label><input type="checkbox" name="s_mods[]" value="to" id="s_mod_to" /><roundcube:label name="to" /></label></li>
- <li role="menuitem" class="checkbox"><label><input type="checkbox" name="s_mods[]" value="cc" id="s_mod_cc" /><roundcube:label name="cc" /></label></li>
- <li role="menuitem" class="checkbox"><label><input type="checkbox" name="s_mods[]" value="bcc" id="s_mod_bcc" /><roundcube:label name="bcc" /></label></li>
- <li role="menuitem" class="checkbox"><label><input type="checkbox" name="s_mods[]" value="body" id="s_mod_body" /><roundcube:label name="body" /></label></li>
- <li role="menuitem" class="checkbox"><label><input type="checkbox" name="s_mods[]" value="text" id="s_mod_text" /><roundcube:label name="msgtext" /></label></li>
- <li role="separator" class="separator"><label><roundcube:label name="date" /></label></li>
- <li role="menuitem"><roundcube:object name="searchinterval" id="s_interval" onchange="rcmail.set_searchinterval($(this).val())" /></li>
- <li role="separator" class="separator"><label><roundcube:label name="searchscope" /></label></li>
- <li role="menuitem"><label><input type="radio" name="s_scope" value="base" id="s_scope_base" /> <span><roundcube:label name="currentfolder" /></span></label></li>
- <li role="menuitem"><label><input type="radio" name="s_scope" value="sub" id="s_scope_sub" /> <span><roundcube:label name="subfolders" /></span></label></li>
- <li role="menuitem"><label><input type="radio" name="s_scope" value="all" id="s_scope_all" /> <span><roundcube:label name="allfolders" /></span></label></li>
- </ul>
- <div class="buttons"><button class="btn btn-primary icon search" href="#" onclick="if (rcmail.command('search')) UI.menu_hide('search-menu')"><roundcube:label name="search" /></button></div>
-</div>
<div id="dragmessage-menu" class="popupmenu">
<h3 id="aria-label-dragmessage-menu" class="voice"><roundcube:label name="arialabeldropactionmenu" /></h3>
<ul class="toolbarmenu" role="menu" aria-labelledby="aria-label-dragmessage-menu">
<roundcube:button command="move" type="link-menuitem" onclick="return rcmail.drag_menu_action('move')" label="move" classAct="active" />
<roundcube:button command="copy" type="link-menuitem" onclick="return rcmail.drag_menu_action('copy')" label="copy" classAct="active" />
</ul>
</div>
<div id="mailboxoptions-menu" class="popupmenu">
<h3 id="aria-label-mailboxoptions-menu" class="voice"><roundcube:label name="arialabelmailboxmenu" /></h3>
<ul class="toolbarmenu listing" role="menu" aria-labelledby="aria-label-mailboxoptions-menu">
<roundcube:button command="expunge" type="link-menuitem" label="compact" class="expunge" classAct="expunge active" />
<roundcube:button command="purge" type="link-menuitem" label="empty" class="purge" classAct="purge active" />
<roundcube:button command="mark-all-read" type="link-menuitem" label="markallread" class="read" classAct="read active" />
<roundcube:button command="folders" task="settings" type="link-menuitem" label="managefolders" class="folders" classAct="folders active" />
<roundcube:container name="mailboxoptions" id="mailboxoptionsmenu" />
</ul>
</div>
<div id="listselect-menu" class="popupmenu">
<h3 id="aria-label-listselect-menu" class="voice"><roundcube:label name="arialabellistselectmenu" /></h3>
<ul class="toolbarmenu listing" role="menu" aria-labelledby="aria-label-listselect-menu">
<roundcube:button command="select-all" type="link-menuitem" label="all" class="select all" classAct="select all active" />
<roundcube:button command="select-all" type="link-menuitem" prop="page" label="currpage" class="select page" classAct="select page active" />
<roundcube:button command="select-all" type="link-menuitem" prop="unread" label="unread" class="select unread" classAct="select unread active" />
<roundcube:button command="select-all" type="link-menuitem" prop="flagged" label="flagged" class="select flagged" classAct="select flagged active" />
<roundcube:button command="select-all" type="link-menuitem" prop="invert" label="invert" class="select invert" classAct="select invert active" />
<roundcube:button command="select-none" type="link-menuitem" label="none" class="select none" classAct="select none active" />
</ul>
</div>
<div id="threadselect-menu" class="popupmenu">
<h3 id="aria-label-threadselectmenu" class="voice"><roundcube:label name="arialabelthreadselectmenu" /></h3>
<ul class="toolbarmenu listing" role="menu" aria-labelledby="aria-label-threadselectmenu">
<roundcube:button command="expand-all" type="link-menuitem" label="expand-all" class="expand all" classAct="expand all active" />
<roundcube:button command="expand-unread" type="link-menuitem" label="expand-unread" class="expand unread" classAct="expand unread active" />
<roundcube:button command="collapse-all" type="link-menuitem" label="collapse-all" class="expand none" classAct="expand none active" />
</ul>
</div>
<div id="listoptions-menu" class="popupmenu propform" role="dialog" aria-labelledby="aria-label-listoptions">
<h3 id="aria-label-listoptions" class="voice"><roundcube:label name="arialabelmessagelistoptions" /></h3>
<roundcube:if condition="!in_array('message_sort_col', (array)config:dont_override)" />
<div class="form-group row">
<label for="listoptions-sortcol" class="col-form-label col-sm-4"><roundcube:label name="listsorting" /></label>
<div class="col-sm-8">
<select id="listoptions-sortcol" name="sort_col" class="form-control">
<option value=""><roundcube:label name="nonesort" /></option>
<option value="arrival"><roundcube:label name="arrival" /></option>
<option value="date"><roundcube:label name="sentdate" /></option>
<option value="subject"><roundcube:label name="subject" /></option>
<option value="fromto"><roundcube:label name="fromto" /></option>
<option value="from"><roundcube:label name="from" /></option>
<option value="to"><roundcube:label name="to" /></option>
<option value="cc"><roundcube:label name="cc" /></option>
<option value="size"><roundcube:label name="size" /></option>
</select>
</div>
</div>
<roundcube:endif />
<roundcube:if condition="!in_array('message_sort_order', (array)config:dont_override)" />
<div class="form-group row">
<label for="listoptions-sortord" class="col-form-label col-sm-4"><roundcube:label name="listorder" /></label>
<div class="col-sm-8">
<select id="listoptions-sortord" name="sort_ord" class="form-control">
<option value="ASC"><roundcube:label name="asc" /></option>
<option value="DESC"><roundcube:label name="desc" /></option>
</select>
</div>
</div>
<roundcube:endif />
<roundcube:if condition="env:threads" />
<div class="form-group row">
<label for="listoptions-threads" class="col-form-label col-sm-4"><roundcube:label name="lmode" /></label>
<div class="col-sm-8">
<select id="listoptions-threads" name="mode" class="form-control">
<option value="list"><roundcube:label name="list" /></option>
<option value="threads"><roundcube:label name="threads" /></option>
</select>
</div>
</div>
<roundcube:endif />
<roundcube:add_label name="listoptionstitle" />
</div>
<roundcube:object name="messageimportform" id="uploadform" mode="smart" />
<roundcube:include file="includes/footer.html" />
diff --git a/skins/elastic/ui.js b/skins/elastic/ui.js
index f5bd49934..29ab11465 100644
--- a/skins/elastic/ui.js
+++ b/skins/elastic/ui.js
@@ -1,3206 +1,3177 @@
/**
* Roundcube webmail functions for the Elastic skin
*
* Copyright (c) 2017-2018, The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*
* @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0
*/
"use strict";
function rcube_elastic_ui()
{
var ref = this,
mode = 'normal', // one of: large, normal, small, phone
touch = false,
ios = false,
is_framed = rcmail.is_framed(),
env = {
config: {
standard_windows: rcmail.env.standard_windows,
message_extwin: rcmail.env.message_extwin,
compose_extwin: rcmail.env.compose_extwin,
help_open_extwin: rcmail.env.help_open_extwin
},
small_screen_config: {
standard_windows: true,
message_extwin: false,
compose_extwin: false,
help_open_extwin: false
}
},
menus = {},
content_buttons = [],
frame_buttons = [],
layout = {
menu: $('#layout > .menu'),
sidebar: $('#layout > .sidebar'),
list: $('#layout > .list'),
content: $('#layout > .content'),
},
buttons = {
menu: $('a.menu-button'),
back_sidebar: $('a.back-sidebar-button'),
back_list: $('a.back-list-button'),
back_content: $('a.back-content-button'),
};
// Public methods
this.register_content_buttons = register_content_buttons;
this.menu_hide = menu_hide;
this.menu_toggle = menu_toggle;
this.menu_destroy = menu_destroy;
this.popup_init = popup_init;
this.about_dialog = about_dialog;
this.headers_dialog = headers_dialog;
this.headers_show = headers_show;
this.spellmenu = spellmenu;
this.searchmenu = searchmenu;
this.headersmenu = headersmenu;
this.attachmentmenu = attachmentmenu;
this.mailtomenu = mailtomenu;
this.show_list = show_list;
this.show_sidebar = show_sidebar;
this.smart_field_init = smart_field_init;
this.smart_field_reset = smart_field_reset;
this.form_errors = form_errors;
this.switch_nav_list = switch_nav_list;
this.searchbar_init = searchbar_init;
this.pretty_checkbox = pretty_checkbox;
// Initialize layout
layout_init();
// Convert some elements to Bootstrap style
bootstrap_style();
// Initialize responsive toolbars (have to be before popups init)
toolbar_init();
// Initialize content frame and list handlers
content_frame_init();
// Initialize menu dropdowns
dropdowns_init();
// Setup various UI elements
setup();
// Update layout after initialization
resize();
/**
* Setup procedure
*/
function setup()
{
var title, form, content_buttons = [];
- // Initialize search forms (in list headers)
- $('.header > .searchbar').each(function() { searchbar_init(this); });
- $('.header > .searchfilterbar').each(function() { searchfilterbar_init(this); });
-
// Intercept jQuery-UI dialogs to re-style them
if ($.ui) {
$.widget('ui.dialog', $.ui.dialog, {
open: function() {
this._super();
dialog_open(this);
return this;
}
});
}
// menu/sidebar/list button
buttons.menu.on('click', function() { app_menu(true); return false; });
buttons.back_sidebar.on('click', function() { show_sidebar(); return false; });
buttons.back_list.on('click', function() { show_list(); return false; });
buttons.back_content.on('click', function() { show_content(true); return false; });
+ // Initialize search forms
+ $('.searchbar').each(function() { searchbar_init(this); });
+
// Set content frame title in parent window (exclude ext-windows and dialog frames)
if (is_framed && !rcmail.env.extwin && !parent.$('.ui-dialog:visible').length) {
if (title = $('h1.voice:first').text()) {
parent.$('#layout > .content > .header > .header-title:not(.constant)').text(title);
}
}
else if (!is_framed) {
title = $('.boxtitle:first', layout.content).detach().text();
if (!title) {
title = $('h1.voice:first').text();
}
if (title) {
$('.header > .header-title', layout.content).text(title);
}
}
// Add content frame toolbar in the footer, for content buttons and navigation
if (!is_framed && layout.content.length && !$(layout.content).is('.no-navbar')
&& !$(layout.content).children('.frame-content').length
) {
env.frame_nav = $('<div class="footer toolbar content-frame-navigation hide-nav-buttons">')
.append($('<a class="button prev">')
.append($('<span class="inner"></span>').text(rcmail.gettext('previous'))))
.append($('<span class="buttons">'))
.append($('<a class="button next">')
.append($('<span class="inner"></span>').text(rcmail.gettext('next'))))
.appendTo(layout.content);
}
// Move some buttons to the frame footer toolbar
$('a[data-content-button]').each(function() {
content_buttons.push(create_cloned_button($(this)));
});
// Move form buttons from the content frame into the frame footer (on parent window)
$('.formbuttons').children().each(function() {
var target = $(this);
// skip non-content buttons
if (!is_framed && !target.parents('.content').length) {
return;
}
if (target.is('.cancel')) {
target.addClass('hidden');
return;
}
content_buttons.push(create_cloned_button(target));
});
(is_framed ? parent.UI : ref).register_content_buttons(content_buttons);
// Mail compose features
if (form = rcmail.gui_objects.messageform) {
form = $('form[name="' + form + '"]');
// Show input elements with non-empty value
// These event handlers need to be registered before rcmail 'init' event
$('#_cc, #_bcc, #_replyto, #_followupto', $('.compose-headers')).each(function() {
$(this).on('change', function() {
$('#compose' + $(this).attr('id'))[this.value ? 'removeClass' : 'addClass']('hidden');
});
});
// We put compose options outside of the main form
// Because IE/Edge (<16) does not support 'form' attribute we'll copy
// inputs into the main form as hidden fields
// TODO: Consider doing this for IE/Edge only, just set the 'form' attribute on others
$('#compose-options').find('textarea,input,select').each(function() {
var hidden = $('<input>')
.attr({type: 'hidden', name: $(this).attr('name')})
.appendTo(form);
$(this).attr('tabindex', 2)
.on('change', function() {
hidden.val(this.type != 'checkbox' || this.checked ? $(this).val() : '');
})
.change();
});
}
// Use smart recipient inputs
// This have to be after mail compose feature above
$('[data-recipient-input]').each(function() { recipient_input(this); });
// Image upload widget
$('.image-upload').each(function() { image_upload_input(this); });
// Add HTML/Plain tabs (switch) on top of textarea with TinyMCE editor
$('textarea[data-html-editor]').each(function() { html_editor_init(this); });
$('#dragmessage-menu,#dragcontact-menu').each(function() {
rcmail.gui_object('dragmenu', this.id);
});
// Taskmenu items added by plugins do not use elastic classes (e.g help plugin)
// it's for larry skin compat. We'll assign 'button', 'selected' and icon-specific class.
$('#taskmenu > a').each(function() {
if (/button-([a-z]+)/.test(this.className)) {
var data, name = RegExp.$1,
button = find_button(this.id);
if (button && (data = button.data)) {
if (data.sel) {
data.sel += ' button ' + name;
data.sel = data.sel.replace('button-selected', 'selected');
}
if (data.act) {
data.act += ' button ' + name;
}
rcmail.buttons[button.command][button.index] = data;
rcmail.init_button(button.command, data);
}
$(this).addClass('button ' + name);
$('.button-inner', this).addClass('inner');
}
});
// Some plugins use 'listbubtton' class, we'll replace it with 'button'
$('.listbutton').each(function() {
var button = find_button(this.id);
$(this).addClass('button').removeClass('listbutton');
if (button.data.sel) {
button.data.sel = button.data.sel.replace('listbutton', 'button');
}
if (button.data.act) {
button.data.act = button.data.act.replace('listbutton', 'button');
}
rcmail.buttons[button.command][button.index] = button.data;
rcmail.init_button(button.command, button.data);
});
// buttons that should be hidden on small screen devices
$('[data-hidden]').each(function() {
var m, v = $(this).data('hidden'),
parent = $(this).parent('li'),
re = /(large|big|small|phone)/g;
while (m = re.exec(v)) {
$(parent.length ? parent : this).addClass('hidden-' + m[1]);
}
});
// Modify normal checkboxes on lists so they are different
// than those used for row selection, i.e. use icons
$('[data-list]').each(function() {
$('input[type=checkbox]', this).each(function() { pretty_checkbox(this); });
});
// Assign .formcontainer class to the iframe body, when it
// contains .formcontent and .formbuttons.
if (is_framed) {
$('.formcontent').each(function() {
if ($(this).next('.formbuttons').length) {
$(this).parent().addClass('formcontainer');
}
});
}
// move "Download all attachments" button into a better location
$('#attachment-list + a.zipdownload').appendTo('.header-links');
if (ios = $('html').is('.ipad,.iphone')) {
$('.iframe-wrapper, .scroller').addClass('ios-scroll');
}
if ($('html').filter('.ipad,.iphone,.webkit.mobile,.webkit.tablet').addClass('webkit-scroller').length) {
$(layout.menu).addClass('webkit-scroller');
}
// Set .notree class on treelist widget update
$('.treelist').each(function() {
var list = this, callback = function() {
$(list)[$('.treetoggle', list).length > 0 ? 'removeClass' : 'addClass']('notree');
};
(new MutationObserver(callback)).observe(list, {childList: true, subtree: true});
callback();
});
};
/**
* Moves form buttons into the content frame actions toolbar (for mobile)
*/
function register_content_buttons(buttons)
{
// we need these buttons really only in phone mode
if (/*mode == 'phone' && */ env.frame_nav && buttons && buttons.length) {
var toolbar = env.frame_nav.children('.buttons');
content_buttons = [];
$.each(buttons, function() {
if (this.data('target')) {
content_buttons.push(this.data('target'));
}
});
toolbar.html('').append(buttons);
}
};
/**
* Registers cloned button
*/
function register_cloned_button(old_id, new_id, active_class)
{
var button = find_button(old_id);
if (button) {
rcmail.register_button(button.command, new_id, button.data.type, active_class, button.data.sel);
}
};
/**
* Create a button clone for use in toolbar
*/
function create_cloned_button(target)
{
var button = $('<a>'),
target_id = target.attr('id'),
button_id = target_id + '-clone',
btn_class = target[0].className;
btn_class = $.trim(btn_class.replace('btn-primary', 'primary').replace(/(btn[a-z-]*|button|disabled)/g, ''))
btn_class += ' button disabled';
button.attr({'onclick': '', id: button_id, href: '#', 'class': btn_class})
.append($('<span class="inner">').text(target.text()))
.on('click', function(e) { target.click(); });
if (is_framed) {
button.data('target', target);
frame_buttons.push($.extend({button_id: button_id}, find_button(target[0].id)));
}
else {
// Register the button to get active state updates
register_cloned_button(target_id, button_id, btn_class.replace(' disabled', ''));
}
return button;
};
/**
* Finds an rcmail button
*/
function find_button(id)
{
var i, button, command;
for (command in rcmail.buttons) {
for (i = 0; i < rcmail.buttons[command].length; i++) {
button = rcmail.buttons[command][i];
if (button.id == id) {
return {
command: command,
index: i,
data: button
};
}
}
}
};
/**
* Setup environment
*/
function layout_init()
{
// Select current layout element
env.last_selected = $('#layout > div.selected')[0];
if (!env.last_selected && layout.content.length) {
$.each(['sidebar', 'list', 'content'], function() {
if (layout[this].length) {
env.last_selected = layout[this][0];
layout[this].addClass('selected');
return false;
}
});
}
// Register resize handler
$(window).on('resize', function() {
clearTimeout(env.resize_timeout);
env.resize_timeout = setTimeout(function() { resize(); }, 25);
});
// Enable rcmail.open_window intercepting
env.open_window = rcmail.open_window;
rcmail.open_window = window_open;
rcmail
.addEventListener('message', message_displayed)
.addEventListener('menu-open', menu_toggle)
.addEventListener('menu-close', menu_toggle)
.addEventListener('editor-init', tinymce_init)
.addEventListener('autocomplete_create', rcmail_popup_init)
.addEventListener('googiespell_create', rcmail_popup_init)
.addEventListener('setquota', update_quota)
.addEventListener('enable-command', enable_command_handler)
.addEventListener('init', init);
};
/**
* rcmail 'init' event handler
*/
function init()
{
// Additional functionality on list widgets
$('table[data-list]').each(function() {
var button,
table = $(this),
list = table.data('list');
if (rcmail[list] && rcmail[list].multiselect) {
+ var parent = table.parents('.sidebar,.list,.content'),
+ toolbar = parent.find('.pagenav');
+
+ if (!toolbar) {
+ toolbar = $('<div class="pagenav toolbar footer small">').appendTo(parent);
+ }
+
// Enable checkbox selection on list widgets
rcmail[list].checkbox_selection = true;
// Add Select button to the list navigation bar
button = $('<a>').attr({'class': 'button icon toggleselect disabled', role: 'button'})
.on('click', function() { if ($(this).is('.active')) table.toggleClass('withselection'); })
.append($('<span class="inner">').text(rcmail.gettext('select')))
- .insertBefore(table.parents('.sidebar,.list,.content').find('.header-title'));
+ .prependTo(toolbar);
// Update Select button state on list update
rcmail.addEventListener('listupdate', function(prop) {
if (prop.list && prop.list == rcmail[list]) {
if (prop.rowcount) {
button.addClass('active').removeClass('disabled').attr('tabindex', 0);
}
else {
button.removeClass('active').addClass('disabled').attr('tabindex', -1);
}
}
});
}
// https://github.com/roundcube/elastic/issues/45
// Draggable blocks scrolling on touch devices, we'll disable it there
if (touch && rcmail[list]) {
if (typeof rcmail[list].draggable == 'function') {
rcmail[list].draggable('destroy');
}
else if (typeof rcmail[list].draggable == 'boolean') {
rcmail[list].draggable = false;
}
}
});
// Display "List is empty..." on the list
if (window.MutationObserver) {
$('[data-label-msg]').filter('ul,table').each(function() {
var fn, observer, callback,
info = $('<div class="listing-info hidden">').insertAfter(this),
table = $(this),
fn = function() {
var ext, command,
msg = table.data('label-msg'),
list = table.is('ul') ? table : table.children('tbody');
if (!rcmail.env.search_request && !rcmail.env.qsearch
&& msg && !list.children(':visible').length
) {
ext = table.data('label-ext');
command = table.data('create-command');
if (ext && (!command || rcmail.commands[command])) {
msg += ' ' + ext;
}
info.text(msg).removeClass('hidden');
return;
}
info.addClass('hidden');
};
callback = function() {
// wait until the UI stops loading and the list is visible
if (rcmail.busy || !table.is(':visible')) {
return setTimeout(callback, 250);
}
clearTimeout(env.list_timer);
env.list_timer = setTimeout(fn, 50);
};
// show/hide the message when something changes on the list
observer = new MutationObserver(callback);
observer.observe(table[0], {childList: true, subtree: true, attributes: true, attributeFilter: ['style']});
// initialize the message
callback();
});
}
// Create floating action button(s)
if ((layout.list.length || layout.content.length) && is_mobile()) {
var fabuttons = [];
$('[data-fab]').each(function() {
var button = $(this),
task = button.data('fab-task') || '*',
action = button.data('fab-action') || '*';
if ((task == '*' || task == rcmail.task)
&& (action == '*' || action == rcmail.env.action || (action == 'none' && !rcmail.env.action))
) {
fabuttons.push(create_cloned_button(button));
}
});
if (fabuttons.length) {
$('<div class="floating-action-buttons">').append(fabuttons)
.appendTo(layout.list.length ? layout.list : layout.content);
}
}
// Add menu link for each attachment
if (rcmail.env.action != 'print') {
$('#attachment-list > li').each(function() {
attachmentmenu_append(this);
});
}
var phone_confirmation = function(label) {
if (mode == 'phone') {
rcmail.display_message(rcmail.gettext(label), 'confirmation');
}
};
rcmail.addEventListener('fileappended', function(e) {
if (e.attachment.complete) {
attachmentmenu_append(e.item);
if (e.attachment.mimetype == 'text/vcard' && rcmail.commands['attach-vcard']) {
phone_confirmation('vcard_attachments.vcardattached');
}
}
})
.addEventListener('managesieve.insertrow', function(o) { bootstrap_style(o.obj); })
.addEventListener('add-recipient', function() { phone_confirmation('recipientsadded'); });
rcmail.init_pagejumper('.pagenav > input');
if (rcmail.task == 'mail') {
if (rcmail.env.action == 'compose') {
// In compose window we do not provide "Back' button, instead
// we modify the Mail button in the task menu to act like it (i.e. calls 'list' command)
if (!rcmail.env.extwin) {
$('a.button.mail', layout.menu).attr('onclick', "return rcmail.command('list','',this,event)");
}
rcmail.addEventListener('compose-encrypted', function(e) {
$("a.mode-html, button.attach").prop('disabled', e.active);
$('a.button.attach, a.button.responses')[e.active ? 'addClass' : 'removeClass']('disabled');
});
$('.sidebar > .footer > a.button').click(function() {
if ($(this).is('.disabled')) {
rcmail.display_message(rcmail.gettext('nocontactselected'), 'warning');
}
});
}
// Append contact menu to all mailto: links
if (rcmail.env.action == 'preview' || rcmail.env.action == 'show') {
$('a').filter('[href^="mailto:"]').each(function() {
mailtomenu_append(this);
});
}
}
else if (rcmail.task == 'settings') {
rcmail.addEventListener('identity-encryption-show', function(p) {
bootstrap_style(p.container);
});
rcmail.addEventListener('identity-encryption-update', function(p) {
bootstrap_style(p.container);
});
}
rcmail.env.thread_padding = '1.5rem';
// Update layout after initialization (again)
// In devel mode we have to wait until all styles are applied by less
if (rcmail.env.devel_mode && window.less) {
less.pageLoadFinished.then(function() {
resize();
});
}
else {
resize();
}
// Add date format placeholder to datepicker inputs
var func, format;
if (format = rcmail.env.date_format_localized) {
func = function(input) { $(input).filter('.datepicker').attr('placeholder', format); };
$('input.datepicker').each(function() { func(this); });
rcmail.addEventListener('insert-edit-field', func);
}
};
/**
* Apply bootstrap classes to html elements
*/
function bootstrap_style(context)
{
if (!context) {
context = document;
}
$('input.button,button', context).not('.btn').addClass('btn').not('.btn-primary,.primary,.mainaction').addClass('btn-secondary');
$('input.button.mainaction,button.primary,button.mainaction', context).addClass('btn-primary');
$('button.btn.delete,button.btn.discard', context).addClass('btn-danger');
$.each(['warning', 'error', 'information', 'confirmation'], function() {
var type = this;
$('.box' + type + ':not(.ui.alert)', context).each(function() {
alert_style(this, type, true);
});
});
// Convert structure of single dialogs (one input or just an image),
// e.g. group create, attachment rename where we use <label>Label<input></label>
if (context != document && $('.popup', context).children().length == 1) {
var content = $('.popup', context).children().first();
if (content.is('img')) {
$('.popup', context).addClass('justified');
}
else if (content.is('label')) {
var input = content.find('input').detach(),
label = content.detach(),
id = input.attr('id');
if (!id) {
input.attr('id', id = 'dialog-input-elastic');
}
$('.popup', context).addClass('formcontent').append(
$('<div class="form-group row">')
.append(label.attr('for', id).addClass('col-sm-2 col-form-label'))
.append($('<div class="col-sm-10">').append(input))
);
input.focus();
}
}
// Forms
var supported_controls = 'input:not(.button,[type=button],[type=file],[type=radio],[type=checkbox]),select,textarea';
$(supported_controls, $('.propform', context)).addClass('form-control');
$('[type=checkbox]', $('.propform', context)).addClass('form-check-input');
$('table.propform', context).each(function() {
var text_rows = 0, form_rows = 0;
$(this).find('> tbody > tr').each(function() {
var first, last, row = $(this),
row_classes = ['form-group', 'row'],
cells = row.children('td');
if (cells.length == 2) {
first = cells.first();
last = cells.last();
$('label', first).addClass('col-form-label');
first.addClass('col-sm-4');
last.addClass('col-sm-8');
if (last.find('[type=checkbox]').length == 1 && !last.find('.proplist').length) {
row_classes.push('form-check');
if (last.find('a').length) {
row_classes.push('with-link');
}
form_rows++;
}
else if (!last.find('input:not([type=hidden]),textarea,radio,select').length) {
last.addClass('form-control-plaintext');
text_rows++;
}
else {
form_rows++;
}
// style some multi-input fields
if (last.children('.datepicker') && last.children('input').length == 2) {
last.addClass('datetime');
}
}
else if (cells.length == 1) {
cells.css('width', '100%');
}
row.addClass(row_classes.join(' '));
});
if (text_rows > form_rows) {
$(this).addClass('text-only');
}
});
// Special input + anything entry
$('td.input-group', context).each(function() {
$(this).children(':not(:first)').addClass('input-group-append');
});
// Other forms, e.g. Contact advanced search
$('fieldset.propform:not(.groupped) div.row', context).each(function() {
var has_input = $('input:not([type=hidden]),select,textarea', this).length > 0;
if (has_input) {
$(supported_controls, this).addClass('form-control');
}
$(this).children().last().addClass('col-sm-8' + (!has_input ? ' form-control-plaintext' : ''));
$(this).children().first().addClass('col-sm-4 col-form-label');
$(this).addClass('form-group');
});
// Contact info/edit form
$('fieldset.propform.groupped fieldset', context).each(function() {
$('.row', this).each(function() {
var label, first,
has_input = $('input,select,textarea', this).length > 0,
items = $(this).children();
if (has_input) {
$(supported_controls, this).addClass('form-control');
}
if (items.length < 2) {
return;
}
first = items.first();
if (first.is('select')) {
first.addClass('input-group-prepend');
}
else {
first.wrap('<span class="input-group-prepend">').addClass('input-group-text');
}
if (!has_input) {
items.last().addClass('form-control-plaintext');
}
$('.content', this).addClass('input-group-prepend input-group-append input-group-text');
$('a.deletebutton', this).addClass('input-group-text icon delete').wrap('<span class="input-group-append">');
$(this).addClass('input-group');
});
});
// Other forms, e.g. Insert response
$('.propform > .prop.block:not(.row)', context).each(function() {
$(this).addClass('form-group row').each(function() {
$('label', this).addClass('col-form-label').wrap($('<div class="col-sm-4">'));
$('input,select,textarea', this).wrap($('<div class="col-sm-8">'));
$(supported_controls, this).addClass('form-control');
});
});
$('td.rowbuttons > a', context).addClass('btn');
// Testing Bootstrap Tabs on contact info/edit page
// Tabs do not scale nicely on very small screen, so can be used
// only with small number of tabs with short text labels
$('form.tabbed,div.tabbed', context).each(function(idx, item) {
var tabs = [], nav = $('<ul>').attr({'class': 'nav nav-tabs', role: 'tablist'});
$(this).addClass('tab-content').children('fieldset').each(function(i, fieldset) {
var tab, id = fieldset.id || ('tab' + idx + '-' + i),
tab_class = $(fieldset).data('navlink-class');
$(fieldset).addClass('tab-pane').attr({id: id, role: 'tabpanel'});
tab = $('<li>').addClass('nav-item').append(
$('<a>').addClass('nav-link' + (tab_class ? ' ' + tab_class : ''))
.attr({role: 'tab', 'href': '#' + id})
.text($('legend:first', fieldset).text())
.click(function() {
$(this).tab('show');
// Returning false here prevents from strange scrolling issue
// when the form is in an iframe, e.g. contact edit form
return false;
})
);
$('legend:first', fieldset).hide();
tabs.push(tab);
});
// create the navigation bar
nav.append(tabs).insertBefore(item);
// activate the first tab
$('a.nav-link:first', nav).click();
});
// Make tables pretier
$('table:not(.table,.propform,.listing,.ui-datepicker-calendar)', context)
.filter(function() {
// exclude direct propform children and external content
return !$(this).parent().is('.propform')
&& !$(this).parents('.message-htmlpart,.message-partheaders,.boxinformation,.raw-tables').length;
})
.each(function() {
// TODO: Consider implementing automatic setting of table-responsive on window resize
var table = $(this).addClass('table');
table.parent().addClass('table-responsive-sm');
table.find('thead').addClass('thead-default');
});
$('.toolbarmenu select', context).addClass('form-control');
if (context != document) {
$(supported_controls, context).addClass('form-control');
}
// The same for some other checkboxes
// We do this here, not in setup() because we want to cover dialogs
$('input.pretty-checkbox, .propform input[type=checkbox], .form-check > input, .popupmenu.form input[type=checkbox], .toolbarmenu input[type=checkbox]', context)
.each(function() { pretty_checkbox(this); });
// Also when we add action-row of the form, e.g. Managesieve plugin adds them after the page is ready
if ($(context).is('.actionrow')) {
$('input[type=checkbox]', context).each(function() { pretty_checkbox(this); });
}
// Make message-objects alerts pretty (the same as UI alerts)
$('#message-objects', context).children(':not(.ui.alert)').each(function() {
// message objects with notice class are really warnings
var cl = $(this).removeClass('notice').attr('class').split(/\s/)[0] || 'warning';
alert_style(this, cl);
$(this).addClass('box' + cl);
$('a', this).addClass('btn btn-primary');
});
// Style calendar widget (we use setTimeout() because there's no widget event we could bind to)
$('input.datepicker', context).focus(function() {
setTimeout(function() { bootstrap_style($('.ui-datepicker')); }, 5);
});
// Form validation errors (managesieve plugin)
$('.error', context).addClass('is-invalid');
// Make logon form prettier
if (rcmail.env.task == 'login' && context == document) {
$('#login-form table tr').each(function() {
var input = $('input,select', this),
label = $('label', this),
icon_name = input.data('icon'),
icon = $('<i>').attr('class', 'input-group-text icon ' + input.attr('name').replace('_', ''));
if (icon_name) {
icon.addClass(icon_name);
}
$(this).addClass('form-group row');
label.parent().css('display', 'none');
input.addClass('form-control')
.attr('placeholder', label.text())
.before($('<span class="input-group-prepend">').append(icon))
.parent().addClass('input-group');
});
}
};
/**
* Initializes popup menus
*/
function dropdowns_init()
{
$('[data-popup]').each(function() { popup_init(this); });
$(document).on('click', popups_close);
rcube_webmail.set_iframe_events({mousedown: popups_close, touchstart: popups_close});
};
/**
* Init content frame
*/
function content_frame_init()
{
var last_selected = env.last_selected,
title_reset = function(title) {
if (typeof title !== 'string' || !title.length) {
title = $('h1.voice').text() || $('title').text() || '';
}
$('.header > .header-title', layout.content).text(title);
};
// display or reset the content frame
var common_content_handler = function(e, href, show, title)
{
content_frame_navigation(href, e);
if (show && !layout.content.is(':visible')) {
env.last_selected = layout.content[0];
screen_resize();
if (title) {
title_reset(title);
}
}
else if (!show) {
if (env.last_selected != last_selected && !env.content_lock) {
env.last_selected = last_selected;
screen_resize();
}
title_reset();
}
env.content_lock = false;
};
// display the list widget after 'list' and 'listgroup' commands
var common_list_handler = function(e) {
if (mode != 'large' && !env.content_lock && e.list) {
show_list();
}
env.content_lock = false;
// display current folder name in list header
if (e.title) {
$('.header > .header-title', layout.list).text(e.title);
}
};
var list_handler = function(e) {
var args = {};
if (rcmail.env.task == 'addressbook' || (rcmail.env.task == 'mail' && !rcmail.env.action)) {
args.list = true;
}
// display current folder name in list header
if (rcmail.env.task == 'mail' && !rcmail.env.action) {
var name = $.type(e) == 'string' ? e : rcmail.env.mailbox,
folder = rcmail.env.mailboxes[name];
args.title = folder ? folder.name : '';
}
common_list_handler(args);
};
// when loading content-frame in small-screen mode display it
layout.content.find('iframe').on('load', function(e) {
var href = '', show = true;
// Reset the scroll position of the iframe-wrapper
$(this).parent('.iframe-wrapper').scrollTop(0);
try {
href = e.target.contentWindow.location.href;
show = !href.endsWith(rcmail.env.blankpage);
// Reset title back to the default
$(e.target.contentWindow).on('unload', title_reset);
}
catch(e) { /* ignore */ }
common_content_handler(e, href, show);
});
rcmail
.addEventListener('afterlist', list_handler)
.addEventListener('afterlistgroup', list_handler)
.addEventListener('afterlistsearch', list_handler)
// plugins
.addEventListener('show-list', function(e) {
e.list = true;
common_list_handler(e);
})
.addEventListener('show-content', function(e) {
if (!$(e.obj).is('iframe')) {
$(e.scrollElement || e.obj).scrollTop(0);
if (is_mobile()) {
iframe_loader(e.obj);
}
}
common_content_handler(e.event || new Event, '_action=' + (e.mode || 'edit'), true, e.title);
});
};
/**
* Content frame navigation
*/
function content_frame_navigation(href, event)
{
// Don't display navigation for create/add action frames
if (href.match(/_action=(create|add)/) || href.match(/_nav=hide/)) {
$(env.frame_nav).addClass('hide-nav-buttons');
return;
}
var node, uid, list, _list = $('[data-list]', layout.list).data('list');
if (!_list || !(list = rcmail[_list])) {
// hide navbar if there are no visible buttons, e.g. Help plugin UI
if ($(env.frame_nav).is('.hide-nav-buttons') && !$('.buttons', env.frame_nav).children().length) {
$(env.frame_nav).addClass('hidden');
}
return;
}
$(env.frame_nav).removeClass('hide-nav-buttons hidden');
// expand collapsed row so we do not skip the whole thread
// TODO: Unified interface for list and treelist widgets
if (uid = list.get_single_selection()) {
if (list.rows && list.rows[uid] && !list.rows[uid].expanded) {
list.expand_row(event, uid);
}
else if (list.get_node && (node = list.get_node(uid)) && node.collapsed) {
list.expand(uid);
}
}
var prev, next,
frame = $('#' + rcmail.env.contentframe),
next_button = $('a.button.next', env.frame_nav).off('click').addClass('disabled'),
prev_button = $('a.button.prev', env.frame_nav).off('click').addClass('disabled');
if ((next = list.get_next()) || rcmail.env.current_page < rcmail.env.pagecount) {
next_button.removeClass('disabled').on('click', function() {
env.content_lock = true;
iframe_loader(frame);
if (next) {
list.select(next);
}
else {
rcmail.env.list_uid = 'FIRST';
rcmail.command('nextpage');
}
});
}
if (((prev = list.get_prev()) && (prev != '*' || _list != 'subscription_list')) || rcmail.env.current_page > 1) {
prev_button.removeClass('disabled').on('click', function() {
env.content_lock = true;
iframe_loader(frame);
if (prev) {
list.select(prev);
}
else {
rcmail.env.list_uid = 'LAST';
rcmail.command('previouspage');
}
});
}
};
/**
* Handler for editor-init event
*/
function tinymce_init(o)
{
// Enable autoresize plugin
o.config.plugins += ' autoresize';
if (is_touch()) {
// Make the toolbar icons bigger
o.config.toolbar_items_size = null;
// Use minimalistic toolbar
o.config.toolbar = 'undo redo | insert | styleselect';
if (o.config.plugins.match(/emoticons/)) {
o.config.toolbar += ' emoticons';
}
}
};
/**
* Handler for some Roundcube core popups
*/
function rcmail_popup_init(o)
{
// Add some common styling to the autocomplete/googiespell popups
$('table,ul', o.obj).addClass('toolbarmenu listing iconized');
$(o.obj).addClass('popupmenu popover');
bootstrap_style(o.obj);
// for googiespell list
$('input', o.obj).addClass('form-control');
// Modify the googiespell menu on mobile
if (is_mobile() && $(o.obj).is('.googie_window')) {
// Set popup Close title
var title = rcmail.gettext('close'),
class_name = 'button icon cancel',
close_link = $('<a>').attr('class', class_name).text(title)
.click(function(e) {
e.stopPropagation();
$('.popover-overlay').remove();
$(o.obj).hide();
});
$('<h3 class="popover-header">').append(close_link).prependTo(o.obj);
// add overlay element for phone layout
if (!$('.popover-overlay').length) {
$('<div>').attr('class', 'popover-overlay')
.appendTo('body')
.click(function() { $(this).remove(); });
}
$('table,button', o.obj).click(function(e) {
if (!$(e.target).is('input')) {
$('.popover-overlay').remove();
}
});
}
};
/**
* Handler for 'enable-command' event
*/
function enable_command_handler(args)
{
if (is_framed) {
$.each(frame_buttons, function(i, button) {
if (args.command == button.command) {
parent.$('#' + button.button_id)[args.status ? 'removeClass' : 'addClass']('disabled');
}
});
}
if (rcmail.task == 'mail') {
switch (args.command) {
case 'reply-list':
if (rcmail.env.reply_all_mode == 1) {
var label = rcmail.gettext(args.status ? 'replylist' : 'replyall');
$('a.button.reply-all').attr('title', label).find('.inner').text(label);
}
break;
case 'compose-encrypted':
// show the toolbar button for Mailvelope
if (args.status) {
$('a.button.encrypt').parent().show();
}
break;
case 'compose-encrypted-signed':
// enable selector for encrypt and sign
$('#encryption-menu-button').show();
break;
case 'mark':
// show the toolbar button for Mailvelope
$('a.button.markmessage')[args.status ? 'removeClass' : 'addClass']('disabled');
break;
}
}
};
/**
* Window resize handler
* Does layout reflows e.g. on screen orientation change
*/
function resize()
{
var size, mobile, width = $(window).width();
if (width <= 480)
size = 'phone';
else if (width > 1200)
size = 'large';
else if (width > 768)
size = 'normal';
else
size = 'small';
touch = width <= 1024;
mode = size;
screen_resize();
screen_resize_html();
// disable ext-windows and other features
if (mobile = is_mobile()) {
rcmail.set_env(env.small_screen_config);
rcmail.enable_command('extwin', false);
}
else {
rcmail.set_env(env.config);
rcmail.enable_command('extwin', true);
}
// Hide content frame buttons on small devices (with frame toolbar in parent window)
$.each(content_buttons, function() { $(this)[mobile ? 'hide' : 'show'](); });
};
function screen_resize()
{
if (is_framed && !layout.sidebar.length && !layout.list.length) {
return;
}
switch (mode) {
case 'phone': screen_resize_phone(); break;
case 'small': screen_resize_small(); break;
case 'normal': screen_resize_normal(); break;
case 'large': screen_resize_large(); break;
}
screen_resize_headers();
// On iOS and Android the content frame height is never correct, fix it
if (bw.webkit) {
$('.iframe-wrapper').each(function() {
var h = $(this).height();
if (h) {
$(this).children('iframe').height(h);
}
});
}
};
/**
* Assigns layout-* and touch-mode class to the 'html' element
*
* If we're inside an iframe that is small we have to
* check if the parent window is also small (mobile).
* We use that e.g. to still display desktop-like popovers in dialogs
*/
function screen_resize_html()
{
var meta = layout_metadata(),
html = $(document.documentElement);
if (html[0].className.match(/layout-([a-z]+)/)) {
if (RegExp.$1 != meta.mode) {
html.removeClass('layout-' + RegExp.$1)
.addClass('layout-' + meta.mode);
}
}
else {
html.addClass('layout-' + meta.mode);
}
if (meta.touch && !html.is('.touch')) {
html.addClass('touch');
}
else if (!meta.touch && html.is('.touch')) {
html.removeClass('touch');
}
};
/**
* Sets left and right margin to the header title element to make it
* properly centered depending on the number of buttons on both sides
*/
function screen_resize_headers()
{
$('#layout > div > .header').each(function() {
var title, right = 0, left = 0, padding = 0,
sizes = {left: 0, right: 0};
$(this).children(':visible').each(function() {
if (!title && $(this).is('.header-title')) {
title = $(this);
return;
}
- if ($(this).is('.searchbar')) {
- padding += this.offsetWidth;
- }
- else {
- sizes[title ? 'right' : 'left'] += this.offsetWidth;
- }
+ sizes[title ? 'right' : 'left'] += this.offsetWidth;
});
if (padding + sizes.right >= sizes.left) {
right = 0;
left = sizes.right + padding - sizes.left;
}
else {
left = 0;
right = sizes.left - (padding + sizes.right);
}
$(title).css({
'margin-right': right + 'px',
'margin-left': left + 'px',
'padding-right': padding + 'px'
});
});
};
function screen_resize_phone()
{
screen_resize_small_all();
app_menu(false);
};
function screen_resize_small()
{
screen_resize_small_all();
app_menu(true);
};
function screen_resize_normal()
{
var show;
if (layout.list.length) {
show = layout.list.is(env.last_selected) || (!layout.sidebar.is(env.last_selected) && !layout.sidebar.is('.layout-sticky'));
layout.list[show ? 'removeClass' : 'addClass']('hidden');
}
if (layout.sidebar.length) {
show = !layout.list.length || layout.sidebar.is(env.last_selected) || layout.sidebar.is('.layout-sticky');
layout.sidebar[show ? 'removeClass' : 'addClass']('hidden');
}
layout.content.removeClass('hidden');
app_menu(true);
screen_resize_small_none();
+ $('.header > ul.toolbar', layout.list).addClass('popupmenu');
};
function screen_resize_large()
{
$.each(layout, function(name, item) { item.removeClass('hidden'); });
screen_resize_small_none();
};
function screen_resize_small_all()
{
var show, got_content = false;
if (layout.content.length) {
show = got_content = layout.content.is(env.last_selected);
layout.content[show ? 'removeClass' : 'addClass']('hidden');
}
if (layout.list.length) {
show = !got_content && layout.list.is(env.last_selected);
layout.list[show ? 'removeClass' : 'addClass']('hidden');
}
if (layout.sidebar.length) {
show = !got_content && (layout.sidebar.is(env.last_selected) || !layout.list.length);
layout.sidebar[show ? 'removeClass' : 'addClass']('hidden');
}
if (got_content) {
buttons.back_list.show();
}
$('.header > ul.toolbar', layout.content).addClass('popupmenu');
+ $('.header > ul.toolbar', layout.list).addClass('popupmenu');
};
function screen_resize_small_none()
{
buttons.back_list.filter(function() { return $(this).parents('.sidebar').length == 0; }).hide();
$('ul.toolbar.popupmenu').removeClass('popupmenu');
};
function show_content(unsticky)
{
// show sidebar and hide list
layout.list.addClass('hidden');
layout.sidebar.addClass('hidden');
layout.content.removeClass('hidden');
if (unsticky) {
layout.sidebar.removeClass('layout-sticky');
}
screen_resize_headers();
env.last_selected = layout.content[0];
};
function show_sidebar(sticky)
{
// show sidebar and hide list
layout.list.addClass('hidden');
layout.sidebar.removeClass('hidden');
if (sticky) {
layout.sidebar.addClass('layout-sticky');
}
if (mode == 'small' || mode == 'phone') {
layout.content.addClass('hidden');
}
screen_resize_headers();
env.last_selected = layout.sidebar[0];
};
function show_list(scroll)
{
if (!layout.list.length && !layout.sidebar.length) {
history.back();
}
else {
// show list and hide sidebar and content
layout.sidebar.addClass('hidden').removeClass('layout-sticky');
layout.list.removeClass('hidden');
if (mode == 'small' || mode == 'phone') {
hide_content();
}
if (scroll) {
layout.list.children('.scroller').scrollTop(0);
}
env.last_selected = layout.list[0];
}
screen_resize_headers();
};
function hide_content()
{
// show sidebar or list, hide content frame
env.last_selected = layout.list[0] || layout.sidebar[0];
screen_resize();
// reset content frame, so we can load it again
rcmail.show_contentframe(false);
// now we have to unselect selected row on the list
$('[data-list]', layout.list).each(function() {
var list = $(this).data('list');
if (rcmail[list]) {
if (rcmail[list].clear_selection) {
rcmail[list].clear_selection(); // list widget
}
else if (rcmail[list].select) {
rcmail[list].select(); // treelist widget
}
}
});
};
// show menu widget
function app_menu(show)
{
if (show) {
if (mode == 'phone') {
$('<div id="menu-overlay" class="popover-overlay">').appendTo('body');
if (!env.menu_initialized) {
env.menu_initialized = true;
$('a', layout.menu).on('click', function(e) { if (mode == 'phone') app_menu(); });
}
layout.menu.addClass('popover');
}
layout.menu.removeClass('hidden');
}
else {
$('#menu-overlay').remove();
layout.menu.addClass('hidden').removeClass('popover');
}
};
/**
* Triggered when a UI message is displayed
*/
function message_displayed(p)
{
if (p.type == 'loading' && $('.iframe-loader:visible').length) {
// hide original message object, we don't need two "loaders"
rcmail.hide_message(p.object);
return;
}
alert_style(p.object, p.type, true);
$(p.object).attr('role', 'alert');
/*
$('a', p.object).addClass('alert-link');
// show a popup dialog on errors
if (p.type == 'error' && rcmail.env.task != 'login') {
// hide original message object, we don't want both
rcmail.hide_message(p.object);
}
*/
};
/**
* Applies some styling and icon to an alert object
*/
function alert_style(object, type, wrap)
{
var tmp, classes = 'ui alert',
addicon = !$(object).is('.noicon'),
map = {
information: 'alert-info',
notice: 'alert-info',
confirmation: 'alert-success',
warning: 'alert-warning',
error: 'alert-danger',
loading: 'alert-info loading',
vcardattachment: 'alert-info' /* vcard_attachments plugin */
};
if (wrap && addicon && !$(object).is('.aligned-buttons')) {
// we need the content to be non-text node for best alignment
tmp = $(object).html();
$(object).html($('<span>').html(tmp));
}
if (tmp = map[type]) {
classes += ' ' + tmp;
if (addicon) {
$('<i>').attr('class', 'icon').prependTo(object);
}
}
$(object).addClass(classes);
};
/**
* Set UI dialogs size/style depending on screen size
*/
function dialog_open(dialog)
{
var me = $(dialog.uiDialog),
width = me.width(),
height = me.height(),
maxWidth = $(window).width(),
maxHeight = $(window).height();
if (maxWidth <= 480) {
me.css({width: '100%', height: '100%'});
}
else {
if (height > maxHeight) {
me.css('height', '100%');
}
if (width > maxWidth) {
me.css('width', '100%');
}
}
// Display loader when the dialog has an iframe
iframe_loader($('div.popup > iframe', me));
// TODO: style buttons/forms
bootstrap_style(dialog.uiDialog);
};
/**
* Initializes searchbar widget
*/
function searchbar_init(bar)
{
- var parent_class = 'with-search',
- input = $('input:not([type=hidden])', bar).addClass('form-control'),
- button = $('a.button.search', bar),
+ var options_button = $('a.button.options', bar),
+ input = $('input:not([type=hidden])', bar),
form = $('form', bar),
is_search_pending = function() {
- // TODO: This have to be improved to detect real searching state
- // There are cases when search is active but the input is empty
- return input.val();
- },
- hide_func = function(event, focus) {
- if (button.is(':visible')) {
- return;
+ if (input.val()) {
+ return true;
}
- $(bar).removeClass('open')[is_search_pending() ? 'addClass' : 'removeClass']('active');
+ if (rcmail.gui_objects.search_filter && $(rcmail.gui_objects.search_filter).val() != 'ALL') {
+ return true;
+ }
- if (focus && rcube_event.is_keyboard(event)) {
- button.focus();
+ if (rcmail.gui_objects.foldersfilter && $(rcmail.gui_objects.foldersfilter).val() != '---') {
+ return true;
}
+ },
+ update_func = function() {
+ $(bar)[is_search_pending() ? 'addClass' : 'removeClass']('active');
};
- if (!$(bar).next().length) {
- parent_class += ' no-toolbar';
- }
-
- $(bar).parent().addClass(parent_class);
+ options_button.on('click', function(e) {
+ var id = $(this).data('target'),
+ options = $('#' + id),
+ open = options.is(':visible');
- if (is_search_pending()) {
- $(bar).addClass('active');
- }
+ if (options.length) {
+ if (!open) {
+ if (ref[id]) {
+ ref[id](options.get(0), this, e);
+ }
+ else if (typeof window[id] == 'function') {
+ window[id](options.get(0), this, e);
+ }
+ }
- // make the input pretty
- form.addClass('input-group')
- .prepend($('<span class="input-group-prepend">').append('<i class="input-group-text icon search">'))
- .append($('<span class="input-group-append">')
- .append($('a.options', bar).detach().removeClass('button').addClass('icon input-group-text'))
- .append($('a.reset', bar).detach().removeClass('button').addClass('icon input-group-text'))
- .append($('<a class="icon cancel input-group-text" href="#">')));
+ options.next()[open ? 'show' : 'hide']();
+ options.toggleClass('hidden');
+ $('.floating-action-buttons').toggleClass('hidden');
+ $(bar).toggleClass('open');
- // Display search form
- button.on('click', function() {
- $(bar).addClass('open');
- input.focus();
+ $('button.search', options).off('click.search').on('click.search', function() {
+ options_button.trigger('click');
+ });
+ }
});
+ input.on('input change', update_func);
+
// Search reset action
$('a.reset', bar).on('click', function(e) {
// for treelist widget's search setting val and keyup.treelist is needed
// in normal search form reset-search command will do the trick
- // TODO: This calls for some generalization, what about two searchboxes on a page?
input.val('').change().trigger('keyup.treelist', {keyCode: 27});
- // we have to de-activate filter
- // TODO: Probably that should not reset filter, but that's current Roundcube bahavior
- $(bar).prev('.searchfilterbar').removeClass('active');
-
- hide_func(e, true);
- });
-
- $('a.cancel', bar).attr('title', rcmail.gettext('close')).on('click', function(e) { hide_func(e, true); });
-
- // These will hide the form, but not reset it
- rcube_webmail.set_iframe_events({mousedown: hide_func});
- $('body').on('mousedown', function(e) {
- // close searchbar on mousedown anywhere, but not inside the searchbar or dialogs
- if (!$(e.target).parents('.popover,.searchbar').length) {
- hide_func(e);
+ // Reset filter
+ if (rcmail.gui_objects.search_filter) {
+ $(rcmail.gui_objects.search_filter).val('ALL');
}
- });
-
- rcmail.addEventListener('init', function() { if (input.val()) $(bar).addClass('active'); });
- };
-
- /**
- * Initializes searchfilterbar widget
- */
- function searchfilterbar_init(bar)
- {
- bar = $('<div class="searchfilterbar searchbar toolbar">')
- .insertAfter(bar)
- .append($(bar).detach())
- .append($('<a class="button icon filter">').attr({title: rcmail.gettext('filter'), tabindex: 0}));
-
- $('select', bar).wrap($('<div class="input-group">'))
- .parent().prepend($('<span class="input-group-prepend">').append('<i class="input-group-text icon filter">'))
- .append($('<span class="input-group-append">').append($('<a class="icon cancel input-group-text">')
- .attr({title: rcmail.gettext('close'), href: '#'})));
-
- var select = $('select', bar),
- button = $('a.button.filter', bar),
- form = $('.input-group', bar),
- is_filter_enabled = function() {
- var value = select.val();
- return value && value != 'ALL';
- },
- hide_func = function(event, focus) {
- bar[is_filter_enabled() ? 'addClass' : 'removeClass']('active');
-
- if (button.is(':visible')) {
- return;
- }
-
- bar.removeClass('open');
-
- if (focus && rcube_event.is_keyboard(event)) {
- button.focus();
- }
- };
- bar.parent().addClass('with-filter');
-
- if (is_filter_enabled()) {
- bar.addClass('active');
- }
-
- select.removeClass('hidden searchfilterbar').addClass('form-control')
- .on('change', function(e) { hide_func(e, true); });
+ if (rcmail.gui_objects.foldersfilter) {
+ $(rcmail.gui_objects.foldersfilter).val('---');
+ }
- // Display filter selection (with animation effect)
- button.on('click', function() {
- bar.addClass('open');
- select.focus();
+ update_func();
});
- // Filter close button
- $('a.cancel', bar).on('click', function(e) { hide_func(e, true); });
+ rcmail.addEventListener('init', function() {
+ update_func();
- // These will hide the form, but not reset it
- rcube_webmail.set_iframe_events({mousedown: hide_func});
- $('body').on('mousedown', function(e) {
- // close searchbar on mousedown anywhere, but not inside the searchbar or dialogs
- if (!$(e.target).parents('.searchfilterbar').length) {
- hide_func(e);
+ if (rcmail.gui_objects.search_filter) {
+ $(rcmail.gui_objects.search_filter).on('change', update_func);
+ }
+
+ if (rcmail.gui_objects.foldersfilter) {
+ $(rcmail.gui_objects.foldersfilter).on('change', update_func);
}
});
};
/**
* Converts toolbar menu into popup-menu for small screens
*/
function toolbar_init()
{
if (env.got_smart_toolbar) {
return;
}
env.got_smart_toolbar = true;
- var items = [];
-
- // convert toolbar to a popup list
- $('.header > .toolbar:not(.searchbar)', layout.content).each(function() {
- var toolbar = $(this);
-
- toolbar.children().each(function() {
+ var items = [],
+ list_items = [],
+ button_func = function(button, items) {
var item = $('<li role="menuitem">'),
- button = $(this).detach();
+ button = $(button).detach();
// Remove empty text nodes that break alignment of text of the menu item
button.contents().filter(function() { if (this.nodeType == 3 && !$.trim(this.nodeValue).length) $(this).remove(); });
if (button.is('.spacer')) {
item.addClass('spacer');
}
else {
item.append(button);
}
items.push(item);
+ };
+
+ // convert toolbar to a popup list
+ if (layout.list) {
+ $('.header > .toolbar', layout.list).each(function() {
+ var toolbar = $(this);
+
+ toolbar.children().each(function() { button_func(this, list_items); });
+ toolbar.remove();
});
+ }
- toolbar.remove();
- });
+ // convert toolbar to a popup list
+ if (layout.content) {
+ $('.header > .toolbar', layout.content).each(function() {
+ var toolbar = $(this);
+
+ toolbar.children().each(function() { button_func(this, items); });
+ toolbar.remove();
+ });
+ }
// special elements to clone and add to the toolbar (mobile only)
$('ul[data-menu="toolbar-small"] > li > a').each(function() {
var button = $(this).clone();
button.attr('id', this.id + '_clone');
// TODO: rcmail.register_button()
items.push($('<li role="menuitem">').addClass('hidden-big').append(button));
});
+ // append the new list toolbar and menu button
+ if (list_items.length) {
+ var container = layout.list.children('.header'),
+ menu_attrs = {'class': 'toolbar popupmenu listing iconized', id: 'toolbar-list-menu'},
+ menu_button = $('<a class="button icon toolbar-list-button" href="#list-menu">')
+ .attr({'data-popup': 'toolbar-list-menu'});
+
+ container
+ // TODO: copy original toolbar attributes (class, role, aria-*)
+ .append($('<ul>').attr(menu_attrs).data('popup-parent', container).append(list_items))
+ .append(menu_button);
+ }
+
// append the new toolbar and menu button
if (items.length) {
var container = layout.content.children('.header'),
menu_attrs = {'class': 'toolbar popupmenu listing iconized', id: 'toolbar-menu'},
menu_button = $('<a class="button icon toolbar-menu-button" href="#menu">')
.attr({'data-popup': 'toolbar-menu'});
container
// TODO: copy original toolbar attributes (class, role, aria-*)
.append($('<ul>').attr(menu_attrs).data('popup-parent', container).append(items))
.append(menu_button);
if (layout.list.length) {
// bind toolbar menu with the menu button in the list header
$('a.toolbar-menu-button', layout.list).click(function(e) {
e.stopPropagation();
menu_button.click();
});
}
}
};
/**
* Initialize a popup for specified button element
*/
function popup_init(item, win)
{
// On mobile we display the menu from the frame in the parent window
if (is_framed && is_mobile()) {
return parent.UI.popup_init(item, win || window);
}
if (!win) win = window;
var level,
popup_id = $(item).data('popup'),
popup = $(win.$('#' + popup_id).get(0)), // a "hack" to support elements in frames
popup_orig = popup,
title = $(item).attr('title'),
content_element = function() {
// On mobile we display a menu from the frame in the parent window
// To make menu actions working we have to clone the menu
// and pass click events to it...
if (win != window) {
popup = popup_orig.clone(true, true);
popup.attr('id', popup_id + '-clone')
.appendTo(document.body)
.find('li > a, li.checkbox > label').attr('onclick', '').off('click').on('click', function(e) {
if (!$(this).is('.disabled')) {
$(item).popover('hide');
win.$('#' + $(this).attr('id')).click();
}
return false;
});
}
return popup.get(0);
};
$(item).attr({
'aria-haspopup': 'true',
'aria-expanded': 'false',
'aria-owns': popup_id,
})
.popover({
content: content_element,
trigger: $(item).data('popup-trigger') || 'click',
placement: $(item).data('popup-pos') || 'bottom',
animation: true,
boundary: 'window', // fix for https://github.com/twbs/bootstrap/issues/25428
html: true
})
.on('show.bs.popover', function(event) {
var init_func = popup.data('popup-init');
if (popup_id && menus[popup_id]) {
menus[popup_id].transitioning = true;
}
if (init_func && ref[init_func]) {
ref[init_func](popup.get(0), item, event);
}
else if (init_func && win[init_func]) {
win[init_func](popup.get(0), item, event);
}
level = $('div.popover:visible').length + 1;
popup.removeClass('hidden').attr('aria-hidden', false)
// Stop propagation on menu items that have popups
// to make a click on them not hide their parent menu(s)
.find('[aria-haspopup="true"]')
.data('level', level + 1)
.off('click.popup')
.on('click.popup', function(e) { e.stopPropagation(); });
if (!is_mobile()) {
// Set popup height so it is less than the window height
popup.css('max-height', Math.min(36 * 15 - 1, $(window).height() - 30));
}
})
.on('shown.bs.popover', function(event) {
var mobile = is_mobile(),
popover = $('#' + $(item).attr('aria-describedby'));
level = $(item).data('level') || 1;
// Set popup Back/Close title
if (mobile) {
var label = level > 1 ? 'back' : 'close',
title = rcmail.gettext(label),
class_name = 'button icon ' + (label == 'back' ? 'back' : 'cancel');
$('.popover-header', popover).empty()
.append($('<a>').attr('class', class_name).text(title)
.on('click', function(e) {
$(item).popover('hide');
if (level > 1) {
e.stopPropagation();
}
})
.on('mousedown', function(e) {
// stop propagation to i.e. do not close jQuery-UI dialogs below
e.stopPropagation();
})
);
}
// Hide other menus on the same level
$.each(menus, function(id, prop) {
if ($(prop.target).data('level') == level && id != popup_id) {
menu_hide(id);
}
});
if (popup_id && menus[popup_id]) {
menus[popup_id].transitioning = false;
}
// add overlay element for phone layout
if (mobile && !$('.popover-overlay').length) {
$('<div>').attr('class', 'popover-overlay')
.appendTo('body')
.click(function() { $(this).remove(); });
}
$('.popover-body', popover).addClass('webkit-scroller');
})
.on('hide.bs.popover', function() {
if (level == 1) {
$('.popover-overlay').remove();
}
if (popup_id && menus[popup_id] && popup.is(':visible')) {
menus[popup_id].transitioning = true;
}
})
.on('hidden.bs.popover', function() {
if (/-clone$/.test(popup.attr('id'))) {
popup.remove();
}
else {
popup.attr('aria-hidden', true)
// Some menus aren't being hidden, force that
.addClass('hidden')
// Bootstrap will detach the popup element from
// the DOM (https://github.com/twbs/bootstrap/issues/20219)
// making our menus to not update buttons state.
// Work around this by attaching it back to the DOM tree.
popup.appendTo(popup.data('popup-parent') || document.body);
}
// close orphaned popovers, for some reason there are sometimes such dummy elements left
$('.popover-body:empty').each(function() { $(this).parent().remove(); });
if (popup_id && menus[popup_id]) {
delete menus[popup_id];
}
})
.on('keypress', function(event) {
// Close the popup on ESC key
if (event.originalEvent.keyCode == 27) {
$(item).popover('hide');
}
});
// re-add title attribute removed by bootstrap popover
if (title) {
$(item).attr('title', title);
}
popup.attr('aria-hidden', 'true').data('button', item);
// stop propagation to e.g. do not hide the popup when
// clicking inside on form elements
if (popup.data('editable')) {
popup.on('click mousedown', function(e) { e.stopPropagation(); });
}
};
/**
* Closes all popups (for use as event handler)
*/
function popups_close(e)
{
$('.popover.show').each(function() {
var popup = $('.popover-body', this),
button = popup.children().first().data('button');
if (button && e.target != button && !$(button).find(e.target).length && typeof button !== 'string') {
$(button).popover('hide');
}
if (!button) {
$(this).remove();
}
});
};
/**
* Handler for menu-open and menu-close events
*/
function menu_toggle(p)
{
if (!p || !p.name || (p.props && p.props.skinable === false)) {
return;
}
if (is_framed && is_mobile()) {
if (!p.win) {
p.win = window;
}
return parent.UI.menu_toggle(p);
}
if (p.name == 'messagelistmenu') {
menu_messagelist(p);
}
else if (p.event == 'menu-open') {
var fn, pos,
content = $('ul:first', p.obj),
target = p.props && p.props.link ? p.props.link : p.originalEvent.target;
if ($(target).is('span')) {
target = $(target).parents('a,li')[0];
}
if (p.name.match(/^drag/)) {
// create a fake element to position drag menu on the cursor position
pos = rcube_event.get_mouse_pos(p.originalEvent);
target = $('<a>').css({
position: 'absolute',
left: pos.x,
top: pos.y,
height: '1px',
width: '1px',
visibility: 'hidden'
})
.appendTo(document.body).get(0);
}
pos = $(target).data('popup-pos') || 'right';
if (p.name == 'folder-selector') {
content.addClass('listing folderlist');
}
else if (p.name == 'addressbook-selector' || p.name == 'contactgroup-selector') {
content.addClass('listing contactlist');
}
else if (content.hasClass('toolbarmenu')) {
content.addClass('listing');
}
if (p.name == 'pagejump-selector') {
content.addClass('simplelist');
p.obj.addClass('simplelist');
pos = 'top';
}
// There can be only one menu of the same type
if (menus[p.name]) {
menu_hide(p.name, p.originalEvent);
}
// Popover menus use animation. Sometimes the same menu is
// immediately hidden and shown (e.g. folder-selector for copy and move action)
// we have to wait until the previous menu hides before we can open it again
fn = function() {
if (menus[p.name] && menus[p.name].transitioning) {
return setTimeout(fn, 50);
}
if (!$(target).data('popup')) {
$(target).data({
popup: p.name,
'popup-pos': pos,
'popup-trigger': 'manual'
});
popup_init(target, p.win);
}
menus[p.name] = {target: target};
$(target).popover('show');
}
fn();
}
else {
menu_hide(p.name, p.originalEvent);
}
// Stop propagation so multi-level menus work properly
p.originalEvent.stopPropagation();
};
/**
* Close menu by name
*/
function menu_hide(name, event)
{
var target = menu_target(name);
if (name.match(/^drag/)) {
$(target).popover('dispose').remove();
}
else {
$(target).popover('hide');
// In phone mode close all menus when forwardmenu is requested to be closed
// FIXME: This is a hack, we need some generic solution.
if (name == 'forwardmenu') {
popups_close(event);
}
}
};
/**
* Destroys menu by name
*
* This is required when you replace the menu content element
*/
function menu_destroy(name)
{
$('[aria-owns=' + name + ']').popover('dispose').data('popup', null);
};
/**
* Get menu target by name
*/
function menu_target(name)
{
var target;
if (menus[name]) {
target = menus[name].target;
}
else {
target = $('#' + name).data('button');
if (!target) {
// catch cases as 'forwardmenu' where menu suffix has no hyphen
// or try with -menu suffix if it's not in the menu name already
if (name.match(/(?!-)menu$/)) {
name = name.substr(0, name.length - 4);
}
target = $('#' + name + '-menu').data('button');
}
}
return target;
};
/**
* Messages list options dialog
*/
function menu_messagelist(p)
{
var content = $('#listoptions-menu'),
width = content.width() + 25,
dialog = content.clone();
// set form values
$('select[name="sort_col"]', dialog).val(rcmail.env.sort_col || '');
$('select[name="sort_ord"]', dialog).val(rcmail.env.sort_order || 'ASC');
$('select[name="mode"]', dialog).val(rcmail.env.threading ? 'threads' : 'list');
// Fix id/for attributes
$('select', dialog).each(function() { this.id = this.id + '-clone'; });
$('label', dialog).each(function() { $(this).attr('for', $(this).attr('for') + '-clone'); });
var save_func = function(e) {
if (rcube_event.is_keyboard(e.originalEvent)) {
$('#listmenulink').focus();
}
var col = $('select[name="sort_col"]', dialog).val(),
ord = $('select[name="sort_ord"]', dialog).val(),
mode = $('select[name="mode"]', dialog).val();
rcmail.set_list_options([], col, ord, mode == 'threads' ? 1 : 0);
return true;
};
dialog = rcmail.simple_dialog(dialog, rcmail.gettext('listoptionstitle'), save_func, {
closeOnEscape: true,
minWidth: 400,
width: width
});
};
/**
* About dialog
*/
function about_dialog(elem)
{
var support_url, support_func, support_button = false,
dialog = $('<iframe>').attr({id: 'aboutframe', src: rcmail.url('settings/about', {_framed: 1})}),
support_link = $('#supportlink');
if (support_link.length && (support_url = support_link.attr('href'))) {
support_button = support_link.text();
support_func = function(e) { support_url.indexOf('mailto:') < 0 ? window.open(support_url) : location.href = support_url; };
}
rcmail.simple_dialog(dialog, $(elem).text(), support_func, {
button: support_button,
button_class: 'help',
cancel_button: 'close',
width: 600,
height: 400
});
};
/**
* Show/hide more mail headers (envelope)
*/
function headers_show(button)
{
var headers = $(button).parent().prev();
headers[headers.is('.hidden') ? 'removeClass' : 'addClass']('hidden');
};
/**
* Mail headers dialog
*/
function headers_dialog()
{
var props = {_uid: rcmail.env.uid, _mbox: rcmail.env.mailbox, _framed: 1},
dialog = $('<iframe>').attr({id: 'headersframe', src: rcmail.url('headers', props)});
rcmail.simple_dialog(dialog, rcmail.gettext('arialabelmessageheaders'), null, {
cancel_button: 'close',
width: 600,
height: 400
});
};
/**
* Search options menu popup
*/
function searchmenu(obj)
{
var n, all,
list = $('input[name="s_mods[]"]', obj),
- scope_list = $('input[name="s_scope"]', obj),
+ scope_select = $('select[name=s_scope]', obj),
mbox = rcmail.env.mailbox,
mods = rcmail.env.search_mods,
scope = rcmail.env.search_scope || 'base';
if (!$(obj).data('initialized')) {
list.on('click', function() { set_searchmod(this, obj); });
- scope_list.on('click', function() { rcmail.set_searchscope(this.value); });
+ scope_select.on('click', function() { rcmail.set_searchscope($(this).val()); });
$(obj).data('initialized', true);
}
if (rcmail.env.search_mods) {
if (rcmail.env.task == 'mail') {
if (scope == 'all') {
mbox = '*';
}
mods = mods[mbox] ? mods[mbox] : mods['*'];
all = 'text';
- scope_list.prop('checked', false).filter('#s_scope_' + scope).prop('checked', true);
+ scope_select.val(scope);
}
else {
all = '*';
}
if (mods[all]) {
list.map(function() {
this.checked = true;
this.disabled = this.value != all;
});
}
else {
list.prop('disabled', false).prop('checked', false);
for (n in mods) {
- $('#s_mod_' + n, obj).prop('checked', true);
+ list.filter('[value="' + n + '"]').prop('checked', true);
}
}
}
};
function set_searchmod(elem, menu)
{
var all, m, task = rcmail.env.task,
mods = rcmail.env.search_mods,
mbox = rcmail.env.mailbox,
- scope = $('input[name="s_scope"]:checked', menu).val();
+ scope = $('select[name=s_scope]', menu).val();
if (scope == 'all') {
mbox = '*';
}
if (!mods) {
mods = {};
}
if (task == 'mail') {
if (!mods[mbox]) {
mods[mbox] = rcube_clone_object(mods['*']);
}
m = mods[mbox];
all = 'text';
}
else { //addressbook
m = mods;
all = '*';
}
if (!elem.checked) {
delete(m[elem.value]);
}
else {
m[elem.value] = 1;
}
// mark all fields
if (elem.value == all) {
$('input[name="s_mods[]"]', menu).map(function() {
if (this == elem) {
return;
}
this.checked = true;
if (elem.checked) {
this.disabled = true;
delete m[this.value];
}
else {
this.disabled = false;
m[this.value] = 1;
}
});
}
rcmail.set_searchmods(m);
};
/**
* Spellcheck languages list
*/
function spellmenu(obj)
{
var i, link, li, list = [],
lang = rcmail.spellcheck_lang(),
ul = $('ul', obj);
if (!ul.length) {
ul = $('<ul class="toolbarmenu selectable listing iconized" role="menu">');
for (i in rcmail.env.spell_langs) {
li = $('<li role="menuitem">');
link = $('<a href="#'+ i +'" tabindex="0"></a>')
.text(rcmail.env.spell_langs[i])
.addClass('active').data('lang', i)
.on('click keypress', function(e) {
if (e.type != 'keypress' || rcube_event.get_keycode(e) == 13) {
rcmail.spellcheck_lang_set($(this).data('lang'));
rcmail.hide_menu('spell-menu', e);
return false;
}
});
link.appendTo(li);
list.push(li);
}
ul.append(list).appendTo(obj);
}
// select current language
$('li', ul).each(function() {
var el = $('a', this);
if (el.data('lang') == lang) {
el.addClass('selected').attr('aria-selected', 'true');
}
else if (el.hasClass('selected')) {
el.removeClass('selected').removeAttr('aria-selected');
}
});
};
/**
* Attachment menu
*/
function attachmentmenu(obj, button, event)
{
var id = $(button).parent().attr('id').replace(/^attach/, '');
$.each(['open', 'download', 'rename'], function() {
var action = this;
$('#attachmenu' + action, obj).off('click').attr('onclick', '').click(function(e) {
rcmail.command(action + '-attachment', id, this, e.originalEvent);
});
});
// call menu-open so core can set state of menu commands
rcmail.command('menu-open', {menu: 'attachmentmenu', id: id}, obj, event);
};
/**
* Appends drop-icon to attachments list item (to invoke attachment menu)
*/
function attachmentmenu_append(item)
{
item = $(item);
if (!item.is('.no-menu') && !item.children('.drop').length) {
var label = rcmail.gettext('options');
var button = $('<a>')
.attr({
href: '#',
tabindex: 0,
title: label,
'class': 'button icon dropdown skip-content'
})
.on('click', function(e) {
attachmentmenu($('#attachmentmenu'), button, e);
})
.append($('<span>').attr('class', 'inner').text(label))
.appendTo(item);
}
};
/**
* Mailto menu
*/
function mailtomenu(obj, button, event)
{
var mailto = $(button).attr('href').replace(/^mailto:/, '');
if (mailto.indexOf('@') < 0) {
return true; // let the browser handle this
}
if (rcmail.env.has_writeable_addressbook) {
$('.addressbook', obj).addClass('active')
.off('click').on('click', function(e) {
var i, contact = mailto,
txt = $(button).filter('.rcmContactAddress').text();
contact = contact.split('?')[0].split(',')[0].replace(/(^<|>$)/g, '');
if (txt) {
txt = txt.replace('<' + contact + '>', '');
contact = '"' + $.trim(txt) + '" <' + contact + '>';
}
rcmail.command('add-contact', contact, this, e.originalEvent);
});
}
$('.compose', obj).off('click').on('click', function(e) {
rcmail.command('compose', mailto, this, e.originalEvent);
});
return rcmail.command('menu-open', {menu: 'mailto-menu', link: button}, button, event);
};
/**
* Appends popup menu to mailto links
*/
function mailtomenu_append(item)
{
$(item).attr('onclick', '').on('click', function(e) {
return mailtomenu($('#mailto-menu'), item, e);
});
};
/**
* Headers menu in mail compose
*/
function headersmenu(obj, button, event)
{
$('li > a', obj).each(function() {
var target = '#compose_' + $(this).data('target');
$(this)[$(target).is(':visible') ? 'removeClass' : 'addClass']('active')
.off().on('click', function() {
$(target).removeClass('hidden').find('.recipient-input > input').focus();
});
});
};
/**
* Create/Update quota widget (setquota event handler)
*/
function update_quota(p)
{
var element = $('#quotadisplay'),
bar = element.find('.bar'),
value = p.total ? p.percent : 0;
if (!bar.length) {
bar = $('<span class="bar"><span class="value"></span></span>').appendTo(element);
}
if (value > 0 && value < 10) {
value = 10; // smaller values look not so nice
}
bar.find('.value').css('width', value + '%')[value >= 90 ? 'addClass' : 'removeClass']('warning');
element.attr('title', element.find('.count').attr('title'));
if (p.table) {
element.css('cursor', 'pointer').data('popup-pos', 'top')
.off('click').on('click', function(e) {
rcmail.simple_dialog(p.table, 'quota', null, {cancel_button: 'close'});
});
}
};
/**
* Replaces recipient input with content-editable element that uses "recipient boxes"
*/
function recipient_input(obj)
{
var list, input, ac_props,
input_len_update = function() {
input.css('width', input.val().length * 10 + 15);
},
apply_func = function() {
// update the original input
$(obj).val(list.text() + input.val());
},
focus_func = function() {
list.addClass('focus');
// move cursor to the end of input text, use setTimeout for Firefox
setTimeout(function() { rcmail.set_caret_pos(input.get(0), input.val().length); }, 1);
},
insert_recipient = function(name, email, replace) {
var recipient = $('<li class="recipient">'),
name_element = $('<span class="name">').html(recipient_input_name(name || email))
.on('dblclick', function(e) { recipient_input_edit_dialog(e, insert_recipient); }),
email_element = $('<span class="email">'),
// TODO: should the 'close' link have tabindex?
link = $('<a>').attr({'class': 'button icon remove'})
.click(function() {
recipient.remove();
apply_func();
input.focus();
return false;
});
if (name) {
email = ' <' + email + '>';
}
email_element.text((name ? email : '') + ',');
recipient.attr('title', name ? (name + email) : null)
.append([name_element, email_element, link])
if (replace)
replace.replaceWith(recipient);
else
recipient.insertBefore(input.parent());
},
update_func = function() {
var text = input.val().replace(/[,;\s]+$/, ''),
result = recipient_input_parser(text);
$.each(result.recipients, function() {
insert_recipient(this.name, this.email);
});
input.val(result.text);
apply_func();
input_len_update();
if (result.recipients.length) {
return true;
}
},
parse_func = function(e) {
// Note it can be also executed when autocomplete inserts a recipient
update_func();
if (e.type == 'blur') {
list.removeClass('focus');
}
},
keydown_func = function(e) {
// On Backspace remove the last recipient
if (e.keyCode == 8 && !input.val().length) {
list.children('li.recipient:last').remove();
apply_func();
return false;
}
// Here we add a recipient box when the separator character (,;) was pressed
else if (e.key == ',' || e.key == ';') {
if (update_func()) {
return false;
}
}
input_len_update();
};
// Create the input elemennt and "editable" area
input = $('<input>').attr({type: 'text', tabindex: $(obj).attr('tabindex')})
.on('paste change blur', parse_func)
.on('keydown', keydown_func)
.on('focus mousedown', focus_func);
list = $('<ul>').addClass('form-control recipient-input')
.append($('<li>').append(input))
.on('click', function() { input.focus(); });
// "Replace" the original input/textarea with the content-editable div
// Note: we do not remove the original element, and we do not use
// display: none, because we want to handle onfocus event
// Note: tabindex:-1 to make Shift+TAB working on these widgets
$(obj).css({position: 'absolute', opacity: 0, left: '-5000px', width: '10px'})
.attr('tabindex', -1)
.after(list)
// some core code sometimes focuses or changes the original node
// in such cases we wan't to parse it's value and apply changes
// to the widget element
.on('focus', function(e) { input.focus(); })
.on('change', function(e) {
$('li.recipient', list).remove();
input.val(this.value).change();
})
// copy and parse the value already set
.change();
// this one line is here to fix border of Bootstrap's input-group,
// input-group should not contain any hidden elements
$(obj).detach().insertBefore(list.parent());
if (rcmail.env.autocomplete_threads > 0) {
ac_props = {
threads: rcmail.env.autocomplete_threads,
sources: rcmail.env.autocomplete_sources
};
}
// Init autocompletion
rcmail.init_address_input_events(input, ac_props);
};
/**
* Parses recipient address input and extracts recipients from it
*/
function recipient_input_parser(text)
{
var recipients = [],
address_rx_part = '(\\S+|("[^"]+"))@\\S+',
recipient_rx1 = new RegExp('(<' + address_rx_part + '>)'),
recipient_rx2 = new RegExp('(' + address_rx_part + ')'),
global_rx = /(?=\S)[^",;]*(?:"[^\\"]*(?:\\[,;\S][^\\"]*)*"[^",;]*)*/g,
matches = text.match(global_rx);
$.each(matches || [], function() {
if (this.length && (recipient_rx1.test(this) || recipient_rx2.test(this))) {
var email = RegExp.$1,
name = $.trim(this.replace(email, ''));
recipients.push({
name: name,
email: email.replace(/(^<|>$)/g, ''),
text: this
});
text = text.replace(this, '');
}
});
text = text.replace(/[,;]+/, ',').replace(/^[,;]/, '');
return {recipients: recipients, text: text};
};
/**
* Generates HTML for a text adding <span class="hidden">
* for quote/backslash characters, so they are hidden from the user,
* but still in place to make copying simpler
*
* Note: Selection works in Chrome, but not in Firefox?
*/
function recipient_input_name(text)
{
var i, char, result = '', len = text.length;
if (text.charAt(0) != '"' && text.indexOf('"') > -1) {
text = '"' + text.replace('\\', '\\\\').replace('"', '\\"') + '"';
}
for (i=0; i<len; i++) {
char = text.charAt(i);
switch (char) {
case '"':
if (i > 0 && i < len - 1) {
result += '"';
break;
}
result += '<span class="quotes">' + char + '</span>';
break;
case '\\':
result += '<span class="quotes">' + char + '</span>';
if (text.charAt(i+1) == '\\') {
result += char;
i++;
}
break;
case '<':
result += '&lt;';
break;
case '>':
result += '&gt;';
break;
default:
result += char;
}
}
return result;
};
/**
* Displays dialog to edit a recipient entry
*/
function recipient_input_edit_dialog(e, callback)
{
var element = $(e.target).parents('.recipient'),
recipient = element.text().replace(/,+$/, ''),
input = $('<input>').attr({type: 'text', size: 50}).val(recipient),
content = $('<label>').text(rcmail.gettext('recipient')).append(input);
rcmail.simple_dialog(content, 'recipientedit', function() {
var result, value = input.val();
if (value) {
if (value != recipient) {
result = recipient_input_parser(value);
if (result.recipients.length != 1) {
return false;
}
callback(result.recipients[0].name, result.recipients[0].email, element);
}
return true;
}
});
};
/**
* Adds logic to the contact photo widget
*/
function image_upload_input(obj)
{
var reset_button = $('<a>')
.attr({'class': 'icon button delete', href: '#', })
.click(function(e) { rcmail.command('delete-photo', '', this, e); return false; });
$(obj).append(reset_button).click(function() { rcmail.upload_input('upload-form'); });
$('img', obj).on('load', function() {
// FIXME: will that work in IE?
var state = (this.currentSrc || this.src).indexOf(rcmail.env.photo_placeholder) != -1;
$(obj)[state ? 'removeClass' : 'addClass']('changed');
});
};
/**
* Displays loading... overlay for iframes
*/
function iframe_loader(frame)
{
frame = $(frame);
if (frame.length) {
var loader = $('<div>').attr('class', 'iframe-loader')
.append($('<div>').attr('class', 'spinner').text(rcmail.gettext('loading')));
// custom 'loaded' event is expected to be triggered by plugins
// when using the loader not on an iframe
frame.on('load error loaded', function() {
// wait some time to make sure the iframe stopped loading
setTimeout(function() { loader.remove(); }, 500);
})
.parent().append(loader);
// fix scrolling in iOS
if (ios) {
frame.parent().addClass('ios-scroll');
}
}
};
/**
* Checkbox wrapper
*/
function pretty_checkbox(checkbox)
{
var checkbox = $(checkbox),
id = checkbox.attr('id');
if (checkbox.is('.icon-checkbox')) {
return;
}
if (!id) {
if (!env.icon_checkbox) env.icon_checkbox = 0;
id = 'icochk' + (++env.icon_checkbox);
checkbox.attr('id', id);
}
checkbox.addClass('icon-checkbox form-check-input').after(
$('<label>').attr({'for': id, title: checkbox.attr('title') || ''})
.on('click', function(e) { e.stopPropagation(); })
);
};
/**
* HTML editor textarea wrapper with nice looking tabs-like switch
*/
function html_editor_init(obj)
{
// Here we support two structures
// 1. <div><textarea></textarea><select name="editorSelector"></div>
// 2. <tr><td><td><td><textarea></textarea></td></tr>
// <tr><td><td><td><input type="checkbox"></td></tr>
var sw, is_table = false,
editor = $(obj),
parent = editor.parent(),
mode = function() {
if (is_table) {
return sw.is(':checked') ? 'html' : 'plain';
}
return sw.val();
},
tabs = $('<ul class="nav nav-tabs">')
.append($('<li class="nav-item">')
.append($('<a class="nav-link mode-html" href="#">')
.text(rcmail.gettext('htmltoggle'))))
.append($('<li class="nav-item">')
.append($('<a class="nav-link mode-plain" href="#">')
.text(rcmail.gettext('plaintoggle'))));
if (parent.is('td')) {
sw = $('input[type="checkbox"]', parent.parent().next());
is_table = true;
}
else {
sw = $('[name="editorSelector"]', obj.form);
}
// sanity check
if (sw.length != 1) {
return;
}
parent.addClass('html-editor');
editor.before(tabs);
$('a', tabs).attr('tabindex', editor.attr('tabindex'))
.on('click', function(e) {
var id = editor.attr('id'), is_html = $(this).is('.mode-html');
e.preventDefault();
if (rcmail.command('toggle-editor', {id: id, html: is_html}, '', e.originalEvent)) {
$(this).tab('show');
if (is_table) {
sw.prop('checked', is_html);
}
}
})
.filter('.mode-' + mode()).tab('show');
if (is_table) {
// Hide unwanted table cells
sw.parent().parent().hide();
parent.prev().hide();
// Modify the textarea cell to use 100% width
parent.addClass('col-sm-12');
}
// make the textarea autoresizeable
textarea_autoresize_init(editor);
};
/**
* Make the textarea autoresizeable depending on it's content length.
* The way there's no vertical scrollbar.
*/
function textarea_autoresize_init(textarea)
{
var resize = function(e) {
clearTimeout(env.textarea_timer);
env.textarea_timer = setTimeout(function() {
var area = $(e.target),
initial_height = area.data('initial-height'),
scroll_height = area[0].scrollHeight;
// do nothing when the area is hidden
if (!scroll_height) {
return;
}
if (!initial_height) {
area.data('initial-height', initial_height = scroll_height);
}
// strange effect in Chrome/Firefox when you delete a line in the textarea
// the scrollHeight is not decreased by the line height, but by 2px
// so jumps up many times in small steps, we'd rather use one big step
if (area.outerHeight() - scroll_height == 2) {
scroll_height -= 19; // 21px is the assumed line height
}
area.outerHeight(Math.max(initial_height, scroll_height));
}, 10);
};
$(textarea).css('overflow-y', 'hidden').on('input', resize).trigger('input');
// Make sure the height is up-to-date also in time intervals
setInterval(function() { $(textarea).trigger('input'); }, 1000);
};
// Inititalizes smart list input
function smart_field_init(field)
{
var tip, id = field.id + '_list',
area = $('<div class="multi-input"><div class="content"></div><div class="invalid-feedback"></div></div>'),
list = field.value ? field.value.split("\n") : [''];
if ($('#' + id).length) {
return;
}
// add input rows
$.each(list, function(i, v) {
smart_field_row_add($('.content', area), v, field.name, i, $(field).data('size'));
});
area.attr('id', id);
field = $(field);
if (field.attr('disabled')) {
area.hide();
}
// disable the original field anyway, we don't want it in POST
else {
field.prop('disabled', true);
}
if (field.data('hidden')) {
area.hide();
}
field.after(area);
if (field.hasClass('is-invalid')) {
area.addClass('is-invalid');
$('.invalid-feedback', area).text(field.data('error-msg'));
}
};
function smart_field_row_add(area, value, name, idx, size, after)
{
// build row element content
var input, elem = $('<div class="input-group">'
+ '<input type="text" class="form-control">'
+ '<span class="input-group-append"><a class="icon reset input-group-text" href="#"></a></span>'
+ '</div>'),
attrs = {value: value, name: name + '[]'};
if (size) {
attrs.size = size;
}
input = $('input', elem).attr(attrs)
.keydown(function(e) {
var input = $(this);
// element creation event (on Enter)
if (e.which == 13) {
var name = input.attr('name').replace(/\[\]$/, ''),
dt = (new Date()).getTime(),
elem = smart_field_row_add(area, '', name, dt, size, input.parent());
$('input', elem).focus();
}
// backspace or delete: remove input, focus previous one
else if ((e.which == 8 || e.which == 46) && input.val() == '') {
var parent = input.parent(),
siblings = area.children();
if (siblings.length > 1) {
if (parent.prev().length) {
parent.prev().children('input').focus();
}
else {
parent.next().children('input').focus();
}
parent.remove();
return false;
}
}
});
// element deletion event
$('a.reset', elem).click(function() {
var record = $(this.parentNode.parentNode);
if (area.children().length > 1) {
$('input', record.next().length ? record.next() : record.prev()).focus();
record.remove();
}
else {
$('input', record).val('').focus();
}
});
$(elem).find('input,a')
.on('focus', function() { area.addClass('focused'); })
.on('blur', function() { area.removeClass('focused'); });
if (after) {
after.after(elem);
}
else {
elem.appendTo(area);
}
return elem;
};
// Reset and fill the smart list input with new data
function smart_field_reset(field, data)
{
var id = field.id + '_list',
list = data.length ? data : [''],
area = $('#' + id).children('.content');
area.empty();
// add input rows
$.each(list, function(i, v) {
smart_field_row_add(area, v, field.name, i, $(field).data('size'));
});
};
/**
* Register form errors, mark fields as invalid, dsplay the error below the input
*/
function form_errors(tips)
{
$.each(tips, function() {
var input = $('#' + this[0]).addClass('is-invalid');
if (input.data('type') == 'list') {
input.data('error-msg', this[2]);
return;
}
input.after($('<span class="invalid-feedback">').text(this[2]));
});
};
/**
* Show/hide the navigation list
*/
function switch_nav_list(obj)
{
var records, height, speed = 250,
button = $('a.button', obj),
navlist = $(obj).next();
if (!navlist.height()) {
records = $('tr,li', navlist).filter(function() { return this.style.display != 'none'; });
height = $(records[0]).height() || 50;
navlist.animate({height: (Math.min(5, records.length) * height + 1) + 'px'}, speed);
button.addClass('collapse').removeClass('expand');
$(obj).addClass('expanded');
}
else {
navlist.animate({height: '0'}, speed);
button.addClass('expand').removeClass('collapse');
$(obj).removeClass('expanded');
}
};
/**
* Wrapper for rcmail.open_window to intercept window opening
* and display a dialog with an iframe instead of a real window.
*/
function window_open(url)
{
if (!is_mobile()) {
return env.open_window.apply(rcmail, arguments);
}
// _extwin=1, _framed=1 are required to display attachment preview
// layout properly and make mobile menus working
url = rcmail.add_url(url, '_framed', 1);
url = rcmail.add_url(url, '_extwin', 1);
var label, title = '',
props = {cancel_button: 'close', width: 768, height: 768},
frame = $('<iframe>').attr({id: 'windowframe', src: url});
if (/_action=([a-z_]+)/.test(url) && (label = rcmail.labels[RegExp.$1])) {
title = label;
}
if (/_frame=1/.test(url)) {
props.dialogClass = 'no-titlebar';
}
rcmail.simple_dialog(frame, title, null, props);
return true;
};
/**
* Get layout modes. In frame mode returns the parent layout modes.
*/
function layout_metadata()
{
if (is_framed) {
var doc = $(parent.document.documentElement);
return {
mode: doc[0].className.match(/layout-([a-z]+)/) ? RegExp.$1 : mode,
touch: doc.is('.touch'),
};
}
return {mode: mode, touch: touch};
};
/**
* Returns true if the layout is in 'small' or 'phone' mode
*/
function is_mobile()
{
var meta = layout_metadata();
return meta.mode == 'phone' || meta.mode == 'small';
};
/**
* Returns true if the layout is in 'touch' mode
*/
function is_touch()
{
var meta = layout_metadata();
return meta.touch;
};
}
if (window.rcmail) {
/**
* Elastic version of show_menu as we don't need e.g. menu positioning from core
* TODO: keyboard navigation in menus
*/
rcmail.show_menu = function(prop, show, event)
{
var name = typeof prop == 'object' ? prop.menu : prop,
obj = $('#' + name);
if (typeof prop == 'string') {
prop = {menu: name};
}
// just delegate the action to rcube_elastic_ui
return rcmail.triggerEvent(show === false ? 'menu-close' : 'menu-open', {name: name, obj: obj, props: prop, originalEvent: event});
}
/**
* Elastic version of hide_menu as we don't need e.g. menus stack handling
*/
rcmail.hide_menu = function(name, event)
{
// delegate to rcube_elastic_ui
return rcmail.triggerEvent('menu-close', {name: name, props: {menu: name}, originalEvent: event});
}
}
else {
// rcmail does not exists e.g. on the error template inside a frame
// we fake the engine a little
var rcmail = parent.rcmail;
var rcube_webmail = parent.rcube_webmail;
var bw = {};
}
var UI = new rcube_elastic_ui();

File Metadata

Mime Type
text/x-diff
Expires
Sat, Apr 18, 8:26 AM (56 m, 22 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
435553
Default Alt Text
(320 KB)

Event Timeline