summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/exe/css.php23
-rw-r--r--lib/plugins/styling/.travis.yml13
-rw-r--r--lib/plugins/styling/README27
-rw-r--r--lib/plugins/styling/_test/general.test.php33
-rw-r--r--lib/plugins/styling/action.php108
-rw-r--r--lib/plugins/styling/admin.php216
-rw-r--r--lib/plugins/styling/iris.js1488
-rw-r--r--lib/plugins/styling/lang/en/intro.txt2
-rw-r--r--lib/plugins/styling/lang/en/lang.php32
-rw-r--r--lib/plugins/styling/plugin.info.txt7
-rw-r--r--lib/plugins/styling/script.js91
-rw-r--r--lib/tpl/dokuwiki/lang/en/lang.php12
-rw-r--r--lib/tpl/dokuwiki/lang/en/style.txt4
13 files changed, 2053 insertions, 3 deletions
diff --git a/lib/exe/css.php b/lib/exe/css.php
index 701cebaed..dc4d7d75c 100644
--- a/lib/exe/css.php
+++ b/lib/exe/css.php
@@ -37,7 +37,7 @@ function css_out(){
if(!$tpl) $tpl = $conf['template'];
// load style.ini
- $styleini = css_styleini($tpl);
+ $styleini = css_styleini($tpl, $INPUT->bool('preview'));
// find mediatypes
if ($INPUT->str('s') == 'feed') {
@@ -49,7 +49,7 @@ function css_out(){
}
// The generated script depends on some dynamic options
- $cache = new cache('styles'.$_SERVER['HTTP_HOST'].$_SERVER['SERVER_PORT'].DOKU_BASE.$tpl.$type,'.css');
+ $cache = new cache('styles'.$_SERVER['HTTP_HOST'].$_SERVER['SERVER_PORT'].$INPUT->int('preview').DOKU_BASE.$tpl.$type,'.css');
// if old 'default' userstyle setting exists, make it 'screen' userstyle for backwards compatibility
if (isset($config_cascade['userstyle']['default'])) {
@@ -63,6 +63,7 @@ function css_out(){
$cache_files[] = $tplinc.'style.local.ini'; // @deprecated
$cache_files[] = DOKU_CONF."tpl/$tpl/style.ini";
$cache_files[] = __FILE__;
+ if($INPUT->bool('preview')) $cache_files[] = $conf['cachedir'].'/preview.ini';
// Array of needed files and their web locations, the latter ones
// are needed to fix relative paths in the stylesheets
@@ -262,9 +263,12 @@ function css_applystyle($css, $replacements) {
* @author Andreas Gohr <andi@splitbrain.org>
*
* @param string $tpl the used template
+ * @param bool $preview load preview replacements
* @return array with keys 'stylesheets' and 'replacements'
*/
-function css_styleini($tpl) {
+function css_styleini($tpl, $preview=false) {
+ global $conf;
+
$stylesheets = array(); // mode, file => base
$replacements = array(); // placeholder => value
@@ -321,6 +325,19 @@ function css_styleini($tpl) {
}
}
+ // allow replacement overwrites in preview mode
+ if($preview) {
+ $webbase = DOKU_BASE;
+ $ini = $conf['cachedir'].'/preview.ini';
+ if(file_exists($ini)) {
+ $data = parse_ini_file($ini, true);
+ // replacements
+ if(is_array($data['replacements'])) {
+ $replacements = array_merge($replacements, css_fixreplacementurls($data['replacements'], $webbase));
+ }
+ }
+ }
+
return array(
'stylesheets' => $stylesheets,
'replacements' => $replacements
diff --git a/lib/plugins/styling/.travis.yml b/lib/plugins/styling/.travis.yml
new file mode 100644
index 000000000..75ee0b152
--- /dev/null
+++ b/lib/plugins/styling/.travis.yml
@@ -0,0 +1,13 @@
+# Config file for travis-ci.org
+
+language: php
+php:
+ - "5.5"
+ - "5.4"
+ - "5.3"
+env:
+ - DOKUWIKI=master
+ - DOKUWIKI=stable
+before_install: wget https://raw.github.com/splitbrain/dokuwiki-travis/master/travis.sh
+install: sh travis.sh
+script: cd _test && phpunit --stderr --group plugin_styling
diff --git a/lib/plugins/styling/README b/lib/plugins/styling/README
new file mode 100644
index 000000000..a1a5e890c
--- /dev/null
+++ b/lib/plugins/styling/README
@@ -0,0 +1,27 @@
+styling Plugin for DokuWiki
+
+Allows to edit style.ini replacements
+
+All documentation for this plugin can be found at
+https://www.dokuwiki.org/plugin:styling
+
+If you install this plugin manually, make sure it is installed in
+lib/plugins/styling/ - if the folder is called different it
+will not work!
+
+Please refer to http://www.dokuwiki.org/plugins for additional info
+on how to install plugins in DokuWiki.
+
+----
+Copyright (C) Andreas Gohr <andi@splitbrain.org>
+
+This program 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; version 2 of the License
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+See the COPYING file in your DokuWiki folder for details
diff --git a/lib/plugins/styling/_test/general.test.php b/lib/plugins/styling/_test/general.test.php
new file mode 100644
index 000000000..1337f6f75
--- /dev/null
+++ b/lib/plugins/styling/_test/general.test.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * General tests for the styling plugin
+ *
+ * @group plugin_styling
+ * @group plugins
+ */
+class general_plugin_styling_test extends DokuWikiTest {
+
+ /**
+ * Simple test to make sure the plugin.info.txt is in correct format
+ */
+ public function test_plugininfo() {
+ $file = __DIR__.'/../plugin.info.txt';
+ $this->assertFileExists($file);
+
+ $info = confToHash($file);
+
+ $this->assertArrayHasKey('base', $info);
+ $this->assertArrayHasKey('author', $info);
+ $this->assertArrayHasKey('email', $info);
+ $this->assertArrayHasKey('date', $info);
+ $this->assertArrayHasKey('name', $info);
+ $this->assertArrayHasKey('desc', $info);
+ $this->assertArrayHasKey('url', $info);
+
+ $this->assertEquals('styling', $info['base']);
+ $this->assertRegExp('/^https?:\/\//', $info['url']);
+ $this->assertTrue(mail_isvalid($info['email']));
+ $this->assertRegExp('/^\d\d\d\d-\d\d-\d\d$/', $info['date']);
+ $this->assertTrue(false !== strtotime($info['date']));
+ }
+}
diff --git a/lib/plugins/styling/action.php b/lib/plugins/styling/action.php
new file mode 100644
index 000000000..622c634d6
--- /dev/null
+++ b/lib/plugins/styling/action.php
@@ -0,0 +1,108 @@
+<?php
+/**
+ * DokuWiki Plugin styling (Action Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+/**
+ * Class action_plugin_styling
+ *
+ * This handles all the save actions and loading the interface
+ *
+ * All this usually would be done within an admin plugin, but we want to have this available outside
+ * the admin interface using our floating dialog.
+ */
+class action_plugin_styling extends DokuWiki_Action_Plugin {
+
+ /**
+ * Registers a callback functions
+ *
+ * @param Doku_Event_Handler $controller DokuWiki's event controller object
+ * @return void
+ */
+ public function register(Doku_Event_Handler $controller) {
+ $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax');
+ $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_action');
+ $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'handle_header');
+ }
+
+ /**
+ * Adds the preview parameter to the stylesheet loading in non-js mode
+ *
+ * @param Doku_Event $event event object by reference
+ * @param mixed $param [the parameters passed as fifth argument to register_hook() when this
+ * handler was registered]
+ * @return void
+ */
+ public function handle_header(Doku_Event &$event, $param) {
+ global $ACT;
+ global $INPUT;
+ if($ACT != 'admin' || $INPUT->str('page') != 'styling') return;
+ if(!auth_isadmin()) return;
+
+ // set preview
+ $len = count($event->data['link']);
+ for($i = 0; $i < $len; $i++) {
+ if(
+ $event->data['link'][$i]['rel'] == 'stylesheet' &&
+ strpos($event->data['link'][$i]['href'], 'lib/exe/css.php') !== false
+ ) {
+ $event->data['link'][$i]['href'] .= '&preview=1&tseed='.time();
+ }
+ }
+ }
+
+ /**
+ * Updates the style.ini settings by passing it on to handle() of the admin component
+ *
+ * @param Doku_Event $event event object by reference
+ * @param mixed $param [the parameters passed as fifth argument to register_hook() when this
+ * handler was registered]
+ * @return void
+ */
+ public function handle_action(Doku_Event &$event, $param) {
+ if($event->data != 'styling_plugin') return;
+ if(!auth_isadmin()) return;
+ $event->data = 'show';
+
+ /** @var admin_plugin_styling $hlp */
+ $hlp = plugin_load('admin', 'styling');
+ $hlp->handle();
+ }
+
+ /**
+ * Create the style form in the floating Dialog
+ *
+ * @param Doku_Event $event event object by reference
+ * @param mixed $param [the parameters passed as fifth argument to register_hook() when this
+ * handler was registered]
+ * @return void
+ */
+
+ public function handle_ajax(Doku_Event &$event, $param) {
+ if($event->data != 'plugin_styling') return;
+ if(!auth_isadmin()) return;
+ $event->preventDefault();
+ $event->stopPropagation();
+
+ global $ID;
+ global $INPUT;
+ $ID = getID();
+
+ /** @var admin_plugin_styling $hlp */
+ $hlp = plugin_load('admin', 'styling');
+ if($INPUT->str('run') == 'preview') {
+ $hlp->run_preview();
+ } else {
+ $hlp->form(true);
+ }
+ }
+
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/lib/plugins/styling/admin.php b/lib/plugins/styling/admin.php
new file mode 100644
index 000000000..3c2ab577c
--- /dev/null
+++ b/lib/plugins/styling/admin.php
@@ -0,0 +1,216 @@
+<?php
+/**
+ * DokuWiki Plugin styling (Admin Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+class admin_plugin_styling extends DokuWiki_Admin_Plugin {
+
+ /**
+ * @return int sort number in admin menu
+ */
+ public function getMenuSort() {
+ return 1000;
+ }
+
+ /**
+ * @return bool true if only access for superuser, false is for superusers and moderators
+ */
+ public function forAdminOnly() {
+ return true;
+ }
+
+ /**
+ * @param string $language
+ * @return string
+ */
+ public function getMenuText($language) {
+ $js = $this->getLang('js');
+ return $js['menu'];
+ }
+
+ /**
+ * handle the different actions (also called from ajax)
+ */
+ public function handle() {
+ global $INPUT;
+ $run = $INPUT->extract('run')->str('run');
+ if(!$run) return;
+ $run = "run_$run";
+ $this->$run();
+ }
+
+ /**
+ * Render HTML output, e.g. helpful text and a form
+ */
+ public function html() {
+ echo '<div id="plugin__styling">';
+ ptln('<h1>'.$this->getMenuText('').'</h1>');
+ $this->form(false);
+ echo '</div>';
+ }
+
+ /**
+ * Create the actual editing form
+ * @param boolean $isajax
+ */
+ public function form($isajax) {
+ global $conf;
+ global $ID;
+ define('SIMPLE_TEST', 1); // hack, ideally certain functions should be moved out of css.php
+ require_once(DOKU_INC.'lib/exe/css.php');
+ $styleini = css_styleini($conf['template'], true);
+ $replacements = $styleini['replacements'];
+
+ if($isajax) {
+ $target = wl($ID, array('do' => 'styling_plugin'));
+ } else {
+ $target = wl($ID, array('do' => 'admin', 'page' => 'styling'));
+ }
+
+ if(empty($replacements)) {
+ echo '<p class="error">'.$this->getLang('error').'</p>';
+ } else {
+ echo $this->locale_xhtml('intro');
+
+ echo '<form class="styling" method="post" action="'.$target.'">';
+
+ echo '<table>';
+ foreach($replacements as $key => $value) {
+ $name = tpl_getLang($key);
+ if(empty($name)) $name = $this->getLang($key);
+ if(empty($name)) $name = $key;
+
+ echo '<tr>';
+ echo '<td>'.$name.'</td>';
+ echo '<td><input type="text" name="tpl['.hsc($key).']" value="'.hsc($value).'" '.$this->colorClass($key).' />';
+ echo '</tr>';
+ }
+ echo '</table>';
+
+ echo '<p>';
+ echo '<input type="submit" name="run[preview]" class="btn_preview" value="'.$this->getLang('btn_preview').'">';
+ echo '<input type="submit" name="run[reset]" value="'.$this->getLang('btn_reset').'">'; #FIXME only if preview.ini exists
+ echo '</p>';
+
+ echo '<p>';
+ echo '<input type="submit" name="run[save]" value="'.$this->getLang('btn_save').'">';
+ echo '</p>';
+
+ echo '<p>';
+ echo '<input type="submit" name="run[revert]" value="'.$this->getLang('btn_revert').'">'; #FIXME only if local.ini exists
+ echo '</p>';
+
+ echo '</form>';
+
+ echo tpl_locale_xhtml('style');
+
+ }
+ }
+
+ /**
+ * set the color class attribute
+ */
+ protected function colorClass($key) {
+ static $colors = array(
+ 'text',
+ 'background',
+ 'text_alt',
+ 'background_alt',
+ 'text_neu',
+ 'background_neu',
+ 'border',
+ 'highlight',
+ 'background_site',
+ 'link',
+ 'existing',
+ 'missing',
+ );
+
+ if(preg_match('/colou?r/', $key) || in_array(trim($key,'_'), $colors)) {
+ return 'class="color"';
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * saves the preview.ini (alos called from ajax directly)
+ */
+ public function run_preview() {
+ global $conf;
+ $ini = $conf['cachedir'].'/preview.ini';
+ io_saveFile($ini, $this->makeini());
+ }
+
+ /**
+ * deletes the preview.ini
+ */
+ protected function run_reset() {
+ global $conf;
+ $ini = $conf['cachedir'].'/preview.ini';
+ io_saveFile($ini, '');
+ }
+
+ /**
+ * deletes the local style.ini replacements
+ */
+ protected function run_revert() {
+ $this->replaceini('');
+ $this->run_reset();
+ }
+
+ /**
+ * save the local style.ini replacements
+ */
+ protected function run_save() {
+ $this->replaceini($this->makeini());
+ $this->run_reset();
+ }
+
+ /**
+ * create the replacement part of a style.ini from submitted data
+ *
+ * @return string
+ */
+ protected function makeini() {
+ global $INPUT;
+
+ $ini = "[replacements]\n";
+ $ini .= ";These overwrites have been generated from the Template styling Admin interface\n";
+ $ini .= ";Any values in this section will be overwritten by that tool again\n";
+ foreach($INPUT->arr('tpl') as $key => $val) {
+ $ini .= $key.' = "'.addslashes($val).'"'."\n";
+ }
+
+ return $ini;
+ }
+
+ /**
+ * replaces the replacement parts in the local ini
+ *
+ * @param string $new the new ini contents
+ */
+ protected function replaceini($new) {
+ global $conf;
+ $ini = DOKU_CONF."tpl/".$conf['template']."/style.ini";
+ if(file_exists($ini)) {
+ $old = io_readFile($ini);
+ $old = preg_replace('/\[replacements\]\n.*?(\n\[.*]|$)/s', '\\1', $old);
+ $old = trim($old);
+ } else {
+ $old = '';
+ }
+
+ io_makeFileDir($ini);
+ io_saveFile($ini, "$old\n\n$new");
+ }
+
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/lib/plugins/styling/iris.js b/lib/plugins/styling/iris.js
new file mode 100644
index 000000000..4eda5022e
--- /dev/null
+++ b/lib/plugins/styling/iris.js
@@ -0,0 +1,1488 @@
+/*! Iris Color Picker - v1.0.7 - 2014-11-28
+* https://github.com/Automattic/Iris
+* Copyright (c) 2014 Matt Wiebe; Licensed GPLv2 */
+(function( $, undef ){
+ var _html, nonGradientIE, gradientType, vendorPrefixes, _css, Iris, UA, isIE, IEVersion;
+
+ _html = '<div class="iris-picker"><div class="iris-picker-inner"><div class="iris-square"><a class="iris-square-value" href="#"><span class="iris-square-handle ui-slider-handle"></span></a><div class="iris-square-inner iris-square-horiz"></div><div class="iris-square-inner iris-square-vert"></div></div><div class="iris-slider iris-strip"><div class="iris-slider-offset"></div></div></div></div>';
+ _css = '.iris-picker{display:block;position:relative}.iris-picker,.iris-picker *{-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input+.iris-picker{margin-top:4px}.iris-error{background-color:#ffafaf}.iris-border{border-radius:3px;border:1px solid #aaa;width:200px;background-color:#fff}.iris-picker-inner{position:absolute;top:0;right:0;left:0;bottom:0}.iris-border .iris-picker-inner{top:10px;right:10px;left:10px;bottom:10px}.iris-picker .iris-square-inner{position:absolute;left:0;right:0;top:0;bottom:0}.iris-picker .iris-square,.iris-picker .iris-slider,.iris-picker .iris-square-inner,.iris-picker .iris-palette{border-radius:3px;box-shadow:inset 0 0 5px rgba(0,0,0,.4);height:100%;width:12.5%;float:left;margin-right:5%}.iris-picker .iris-square{width:76%;margin-right:10%;position:relative}.iris-picker .iris-square-inner{width:auto;margin:0}.iris-ie-9 .iris-square,.iris-ie-9 .iris-slider,.iris-ie-9 .iris-square-inner,.iris-ie-9 .iris-palette{box-shadow:none;border-radius:0}.iris-ie-9 .iris-square,.iris-ie-9 .iris-slider,.iris-ie-9 .iris-palette{outline:1px solid rgba(0,0,0,.1)}.iris-ie-lt9 .iris-square,.iris-ie-lt9 .iris-slider,.iris-ie-lt9 .iris-square-inner,.iris-ie-lt9 .iris-palette{outline:1px solid #aaa}.iris-ie-lt9 .iris-square .ui-slider-handle{outline:1px solid #aaa;background-color:#fff;-ms-filter:"alpha(Opacity=30)"}.iris-ie-lt9 .iris-square .iris-square-handle{background:0;border:3px solid #fff;-ms-filter:"alpha(Opacity=50)"}.iris-picker .iris-strip{margin-right:0;position:relative}.iris-picker .iris-strip .ui-slider-handle{position:absolute;background:0;margin:0;right:-3px;left:-3px;border:4px solid #aaa;border-width:4px 3px;width:auto;height:6px;border-radius:4px;box-shadow:0 1px 2px rgba(0,0,0,.2);opacity:.9;z-index:5;cursor:ns-resize}.iris-strip .ui-slider-handle:before{content:" ";position:absolute;left:-2px;right:-2px;top:-3px;bottom:-3px;border:2px solid #fff;border-radius:3px}.iris-picker .iris-slider-offset{position:absolute;top:11px;left:0;right:0;bottom:-3px;width:auto;height:auto;background:transparent;border:0;border-radius:0}.iris-picker .iris-square-handle{background:transparent;border:5px solid #aaa;border-radius:50%;border-color:rgba(128,128,128,.5);box-shadow:none;width:12px;height:12px;position:absolute;left:-10px;top:-10px;cursor:move;opacity:1;z-index:10}.iris-picker .ui-state-focus .iris-square-handle{opacity:.8}.iris-picker .iris-square-handle:hover{border-color:#999}.iris-picker .iris-square-value:focus .iris-square-handle{box-shadow:0 0 2px rgba(0,0,0,.75);opacity:.8}.iris-picker .iris-square-handle:hover::after{border-color:#fff}.iris-picker .iris-square-handle::after{position:absolute;bottom:-4px;right:-4px;left:-4px;top:-4px;border:3px solid #f9f9f9;border-color:rgba(255,255,255,.8);border-radius:50%;content:" "}.iris-picker .iris-square-value{width:8px;height:8px;position:absolute}.iris-ie-lt9 .iris-square-value,.iris-mozilla .iris-square-value{width:1px;height:1px}.iris-palette-container{position:absolute;bottom:0;left:0;margin:0;padding:0}.iris-border .iris-palette-container{left:10px;bottom:10px}.iris-picker .iris-palette{margin:0;cursor:pointer}.iris-square-handle,.ui-slider-handle{border:0;outline:0}';
+
+ // Even IE9 dosen't support gradients. Elaborate sigh.
+ UA = navigator.userAgent.toLowerCase();
+ isIE = navigator.appName === 'Microsoft Internet Explorer';
+ IEVersion = isIE ? parseFloat( UA.match( /msie ([0-9]{1,}[\.0-9]{0,})/ )[1] ) : 0;
+ nonGradientIE = ( isIE && IEVersion < 10 );
+ gradientType = false;
+
+ // we don't bother with an unprefixed version, as it has a different syntax
+ vendorPrefixes = [ '-moz-', '-webkit-', '-o-', '-ms-' ];
+
+ // Bail for IE <= 7
+ if ( nonGradientIE && IEVersion <= 7 ) {
+ $.fn.iris = $.noop;
+ $.support.iris = false;
+ return;
+ }
+
+ $.support.iris = true;
+
+ function testGradientType() {
+ var el, base,
+ bgImageString = 'backgroundImage';
+
+ if ( nonGradientIE ) {
+ gradientType = 'filter';
+ }
+ else {
+ el = $( '<div id="iris-gradtest" />' );
+ base = 'linear-gradient(top,#fff,#000)';
+ $.each( vendorPrefixes, function( i, val ){
+ el.css( bgImageString, val + base );
+ if ( el.css( bgImageString ).match( 'gradient' ) ) {
+ gradientType = i;
+ return false;
+ }
+ });
+ // check for legacy webkit gradient syntax
+ if ( gradientType === false ) {
+ el.css( 'background', '-webkit-gradient(linear,0% 0%,0% 100%,from(#fff),to(#000))' );
+ if ( el.css( bgImageString ).match( 'gradient' ) ) {
+ gradientType = 'webkit';
+ }
+ }
+ el.remove();
+ }
+
+ }
+
+ /**
+ * Only for CSS3 gradients. oldIE will use a separate function.
+ *
+ * Accepts as many color stops as necessary from 2nd arg on, or 2nd
+ * arg can be an array of color stops
+ *
+ * @param {string} origin Gradient origin - top or left, defaults to left.
+ * @return {string} Appropriate CSS3 gradient string for use in
+ */
+ function createGradient( origin, stops ) {
+ origin = ( origin === 'top' ) ? 'top' : 'left';
+ stops = $.isArray( stops ) ? stops : Array.prototype.slice.call( arguments, 1 );
+ if ( gradientType === 'webkit' ) {
+ return legacyWebkitGradient( origin, stops );
+ } else {
+ return vendorPrefixes[ gradientType ] + 'linear-gradient(' + origin + ', ' + stops.join(', ') + ')';
+ }
+ }
+
+ /**
+ * Stupid gradients for a stupid browser.
+ */
+ function stupidIEGradient( origin, stops ) {
+ var type, self, lastIndex, filter, startPosProp, endPosProp, dimensionProp, template, html;
+
+ origin = ( origin === 'top' ) ? 'top' : 'left';
+ stops = $.isArray( stops ) ? stops : Array.prototype.slice.call( arguments, 1 );
+ // 8 hex: AARRGGBB
+ // GradientType: 0 vertical, 1 horizontal
+ type = ( origin === 'top' ) ? 0 : 1;
+ self = $( this );
+ lastIndex = stops.length - 1;
+ filter = 'filter';
+ startPosProp = ( type === 1 ) ? 'left' : 'top';
+ endPosProp = ( type === 1 ) ? 'right' : 'bottom';
+ dimensionProp = ( type === 1 ) ? 'height' : 'width';
+ template = '<div class="iris-ie-gradient-shim" style="position:absolute;' + dimensionProp + ':100%;' + startPosProp + ':%start%;' + endPosProp + ':%end%;' + filter + ':%filter%;" data-color:"%color%"></div>';
+ html = '';
+ // need a positioning context
+ if ( self.css('position') === 'static' ) {
+ self.css( {position: 'relative' } );
+ }
+
+ stops = fillColorStops( stops );
+ $.each(stops, function( i, startColor ) {
+ var endColor, endStop, filterVal;
+
+ // we want two at a time. if we're on the last pair, bail.
+ if ( i === lastIndex ) {
+ return false;
+ }
+
+ endColor = stops[ i + 1 ];
+ //if our pairs are at the same color stop, moving along.
+ if ( startColor.stop === endColor.stop ) {
+ return;
+ }
+
+ endStop = 100 - parseFloat( endColor.stop ) + '%';
+ startColor.octoHex = new Color( startColor.color ).toIEOctoHex();
+ endColor.octoHex = new Color( endColor.color ).toIEOctoHex();
+
+ filterVal = 'progid:DXImageTransform.Microsoft.Gradient(GradientType=' + type + ', StartColorStr=\'' + startColor.octoHex + '\', EndColorStr=\'' + endColor.octoHex + '\')';
+ html += template.replace( '%start%', startColor.stop ).replace( '%end%', endStop ).replace( '%filter%', filterVal );
+ });
+ self.find( '.iris-ie-gradient-shim' ).remove();
+ $( html ).prependTo( self );
+ }
+
+ function legacyWebkitGradient( origin, colorList ) {
+ var stops = [];
+ origin = ( origin === 'top' ) ? '0% 0%,0% 100%,' : '0% 100%,100% 100%,';
+ colorList = fillColorStops( colorList );
+ $.each( colorList, function( i, val ){
+ stops.push( 'color-stop(' + ( parseFloat( val.stop ) / 100 ) + ', ' + val.color + ')' );
+ });
+ return '-webkit-gradient(linear,' + origin + stops.join(',') + ')';
+ }
+
+ function fillColorStops( colorList ) {
+ var colors = [],
+ percs = [],
+ newColorList = [],
+ lastIndex = colorList.length - 1;
+
+ $.each( colorList, function( index, val ) {
+ var color = val,
+ perc = false,
+ match = val.match( /1?[0-9]{1,2}%$/ );
+
+ if ( match ) {
+ color = val.replace( /\s?1?[0-9]{1,2}%$/, '' );
+ perc = match.shift();
+ }
+ colors.push( color );
+ percs.push( perc );
+ });
+
+ // back fill first and last
+ if ( percs[0] === false ) {
+ percs[0] = '0%';
+ }
+
+ if ( percs[lastIndex] === false ) {
+ percs[lastIndex] = '100%';
+ }
+
+ percs = backFillColorStops( percs );
+
+ $.each( percs, function( i ){
+ newColorList[i] = { color: colors[i], stop: percs[i] };
+ });
+ return newColorList;
+ }
+
+ function backFillColorStops( stops ) {
+ var first = 0,
+ last = stops.length - 1,
+ i = 0,
+ foundFirst = false,
+ incr,
+ steps,
+ step,
+ firstVal;
+
+ if ( stops.length <= 2 || $.inArray( false, stops ) < 0 ) {
+ return stops;
+ }
+ while ( i < stops.length - 1 ) {
+ if ( ! foundFirst && stops[i] === false ) {
+ first = i - 1;
+ foundFirst = true;
+ } else if ( foundFirst && stops[i] !== false ) {
+ last = i;
+ i = stops.length;
+ }
+ i++;
+ }
+ steps = last - first;
+ firstVal = parseInt( stops[first].replace('%'), 10 );
+ incr = ( parseFloat( stops[last].replace('%') ) - firstVal ) / steps;
+ i = first + 1;
+ step = 1;
+ while ( i < last ) {
+ stops[i] = ( firstVal + ( step * incr ) ) + '%';
+ step++;
+ i++;
+ }
+ return backFillColorStops( stops );
+ }
+
+ $.fn.gradient = function() {
+ var args = arguments;
+ return this.each( function() {
+ // this'll be oldishIE
+ if ( nonGradientIE ) {
+ stupidIEGradient.apply( this, args );
+ } else {
+ // new hotness
+ $( this ).css( 'backgroundImage', createGradient.apply( this, args ) );
+ }
+ });
+ };
+
+ $.fn.raninbowGradient = function( origin, args ) {
+ var opts, template, i, steps;
+
+ origin = origin || 'top';
+ opts = $.extend( {}, { s: 100, l: 50 }, args );
+ template = 'hsl(%h%,' + opts.s + '%,' + opts.l + '%)';
+ i = 0;
+ steps = [];
+ while ( i <= 360 ) {
+ steps.push( template.replace('%h%', i) );
+ i += 30;
+ }
+ return this.each(function() {
+ $(this).gradient( origin, steps );
+ });
+ };
+
+ // the colorpicker widget def.
+ Iris = {
+ options: {
+ color: false,
+ mode: 'hsl',
+ controls: {
+ horiz: 's', // horizontal defaults to saturation
+ vert: 'l', // vertical defaults to lightness
+ strip: 'h' // right strip defaults to hue
+ },
+ hide: true, // hide the color picker by default
+ border: true, // draw a border around the collection of UI elements
+ target: false, // a DOM element / jQuery selector that the element will be appended within. Only used when called on an input.
+ width: 200, // the width of the collection of UI elements
+ palettes: false // show a palette of basic colors beneath the square.
+ },
+ _color: '',
+ _palettes: [ '#000', '#fff', '#d33', '#d93', '#ee2', '#81d742', '#1e73be', '#8224e3' ],
+ _inited: false,
+ _defaultHSLControls: {
+ horiz: 's',
+ vert: 'l',
+ strip: 'h'
+ },
+ _defaultHSVControls: {
+ horiz: 'h',
+ vert: 'v',
+ strip: 's'
+ },
+ _scale: {
+ h: 360,
+ s: 100,
+ l: 100,
+ v: 100
+ },
+ _create: function() {
+ var self = this,
+ el = self.element,
+ color = self.options.color || el.val();
+
+ if ( gradientType === false ) {
+ testGradientType();
+ }
+
+ if ( el.is( 'input' ) ) {
+ if ( self.options.target ) {
+ self.picker = $( _html ).appendTo( self.options.target );
+ } else {
+ self.picker = $( _html ).insertAfter( el );
+ }
+
+ self._addInputListeners( el );
+ } else {
+ el.append( _html );
+ self.picker = el.find( '.iris-picker' );
+ }
+
+ // Browsers / Versions
+ // Feature detection doesn't work for these, and $.browser is deprecated
+ if ( isIE ) {
+ if ( IEVersion === 9 ) {
+ self.picker.addClass( 'iris-ie-9' );
+ } else if ( IEVersion <= 8 ) {
+ self.picker.addClass( 'iris-ie-lt9' );
+ }
+ } else if ( UA.indexOf('compatible') < 0 && UA.indexOf('khtml') < 0 && UA.match( /mozilla/ ) ) {
+ self.picker.addClass( 'iris-mozilla' );
+ }
+
+ if ( self.options.palettes ) {
+ self._addPalettes();
+ }
+
+ self._color = new Color( color ).setHSpace( self.options.mode );
+ self.options.color = self._color.toString();
+
+ // prep 'em for re-use
+ self.controls = {
+ square: self.picker.find( '.iris-square' ),
+ squareDrag: self.picker.find( '.iris-square-value' ),
+ horiz: self.picker.find( '.iris-square-horiz' ),
+ vert: self.picker.find( '.iris-square-vert' ),
+ strip: self.picker.find( '.iris-strip' ),
+ stripSlider: self.picker.find( '.iris-strip .iris-slider-offset' )
+ };
+
+ // small sanity check - if we chose hsv, change default controls away from hsl
+ if ( self.options.mode === 'hsv' && self._has('l', self.options.controls) ) {
+ self.options.controls = self._defaultHSVControls;
+ } else if ( self.options.mode === 'hsl' && self._has('v', self.options.controls) ) {
+ self.options.controls = self._defaultHSLControls;
+ }
+
+ // store it. HSL gets squirrely
+ self.hue = self._color.h();
+
+ if ( self.options.hide ) {
+ self.picker.hide();
+ }
+
+ if ( self.options.border ) {
+ self.picker.addClass( 'iris-border' );
+ }
+
+ self._initControls();
+ self.active = 'external';
+ self._dimensions();
+ self._change();
+ },
+ _has: function(needle, haystack) {
+ var ret = false;
+ $.each(haystack, function(i,v){
+ if ( needle === v ) {
+ ret = true;
+ // exit the loop
+ return false;
+ }
+ });
+ return ret;
+ },
+ _addPalettes: function () {
+ var container = $( '<div class="iris-palette-container" />' ),
+ palette = $( '<a class="iris-palette" tabindex="0" />' ),
+ colors = $.isArray( this.options.palettes ) ? this.options.palettes : this._palettes;
+
+ // do we have an existing container? Empty and reuse it.
+ if ( this.picker.find( '.iris-palette-container' ).length ) {
+ container = this.picker.find( '.iris-palette-container' ).detach().html( '' );
+ }
+
+ $.each(colors, function(index, val) {
+ palette.clone().data( 'color', val )
+ .css( 'backgroundColor', val ).appendTo( container )
+ .height( 10 ).width( 10 );
+ });
+
+ this.picker.append(container);
+ },
+ _paint: function() {
+ var self = this;
+ self._paintDimension( 'top', 'strip' );
+ self._paintDimension( 'top', 'vert' );
+ self._paintDimension( 'left', 'horiz' );
+ },
+ _paintDimension: function( origin, control ) {
+ var self = this,
+ c = self._color,
+ mode = self.options.mode,
+ color = self._getHSpaceColor(),
+ target = self.controls[ control ],
+ controlOpts = self.options.controls,
+ stops;
+
+ // don't paint the active control
+ if ( control === self.active || ( self.active === 'square' && control !== 'strip' ) ) {
+ return;
+ }
+
+ switch ( controlOpts[ control ] ) {
+ case 'h':
+ if ( mode === 'hsv' ) {
+ color = c.clone();
+ switch ( control ) {
+ case 'horiz':
+ color[controlOpts.vert](100);
+ break;
+ case 'vert':
+ color[controlOpts.horiz](100);
+ break;
+ case 'strip':
+ color.setHSpace('hsl');
+ break;
+ }
+ stops = color.toHsl();
+ } else {
+ if ( control === 'strip' ) {
+ stops = { s: color.s, l: color.l };
+ } else {
+ stops = { s: 100, l: color.l };
+ }
+ }
+
+ target.raninbowGradient( origin, stops );
+ break;
+ case 's':
+ if ( mode === 'hsv' ) {
+ if ( control === 'vert' ) {
+ stops = [ c.clone().a(0).s(0).toCSS('rgba'), c.clone().a(1).s(0).toCSS('rgba') ];
+ } else if ( control === 'strip' ) {
+ stops = [ c.clone().s(100).toCSS('hsl'), c.clone().s(0).toCSS('hsl') ];
+ } else if ( control === 'horiz' ) {
+ stops = [ '#fff', 'hsl(' + color.h + ',100%,50%)' ];
+ }
+ } else { // implicit mode === 'hsl'
+ if ( control === 'vert' && self.options.controls.horiz === 'h' ) {
+ stops = ['hsla(0, 0%, ' + color.l + '%, 0)', 'hsla(0, 0%, ' + color.l + '%, 1)'];
+ } else {
+ stops = ['hsl('+ color.h +',0%,50%)', 'hsl(' + color.h + ',100%,50%)'];
+ }
+ }
+
+
+ target.gradient( origin, stops );
+ break;
+ case 'l':
+ if ( control === 'strip' ) {
+ stops = ['hsl(' + color.h + ',100%,100%)', 'hsl(' + color.h + ', ' + color.s + '%,50%)', 'hsl('+ color.h +',100%,0%)'];
+ } else {
+ stops = ['#fff', 'rgba(255,255,255,0) 50%', 'rgba(0,0,0,0) 50%', 'rgba(0,0,0,1)'];
+ }
+ target.gradient( origin, stops );
+ break;
+ case 'v':
+ if ( control === 'strip' ) {
+ stops = [ c.clone().v(100).toCSS(), c.clone().v(0).toCSS() ];
+ } else {
+ stops = ['rgba(0,0,0,0)', '#000'];
+ }
+ target.gradient( origin, stops );
+ break;
+ default:
+ break;
+ }
+ },
+
+ _getHSpaceColor: function() {
+ return ( this.options.mode === 'hsv' ) ? this._color.toHsv() : this._color.toHsl();
+ },
+
+ _dimensions: function( reset ) {
+ // whatever size
+ var self = this,
+ opts = self.options,
+ controls = self.controls,
+ square = controls.square,
+ strip = self.picker.find( '.iris-strip' ),
+ squareWidth = '77.5%',
+ stripWidth = '12%',
+ totalPadding = 20,
+ innerWidth = opts.border ? opts.width - totalPadding : opts.width,
+ controlsHeight,
+ paletteCount = $.isArray( opts.palettes ) ? opts.palettes.length : self._palettes.length,
+ paletteMargin, paletteWidth, paletteContainerWidth;
+
+ if ( reset ) {
+ square.css( 'width', '' );
+ strip.css( 'width', '' );
+ self.picker.css( {width: '', height: ''} );
+ }
+
+ squareWidth = innerWidth * ( parseFloat( squareWidth ) / 100 );
+ stripWidth = innerWidth * ( parseFloat( stripWidth ) / 100 );
+ controlsHeight = opts.border ? squareWidth + totalPadding : squareWidth;
+
+ square.width( squareWidth ).height( squareWidth );
+ strip.height( squareWidth ).width( stripWidth );
+ self.picker.css( { width: opts.width, height: controlsHeight } );
+
+ if ( ! opts.palettes ) {
+ return self.picker.css( 'paddingBottom', '' );
+ }
+
+ // single margin at 2%
+ paletteMargin = squareWidth * 2 / 100;
+ paletteContainerWidth = squareWidth - ( ( paletteCount - 1 ) * paletteMargin );
+ paletteWidth = paletteContainerWidth / paletteCount;
+ self.picker.find('.iris-palette').each( function( i ) {
+ var margin = i === 0 ? 0 : paletteMargin;
+ $( this ).css({
+ width: paletteWidth,
+ height: paletteWidth,
+ marginLeft: margin
+ });
+ });
+ self.picker.css( 'paddingBottom', paletteWidth + paletteMargin );
+ strip.height( paletteWidth + paletteMargin + squareWidth );
+ },
+
+ _addInputListeners: function( input ) {
+ var self = this,
+ debounceTimeout = 100,
+ callback = function( event ){
+ var color = new Color( input.val() ),
+ val = input.val().replace( /^#/, '' );
+
+ input.removeClass( 'iris-error' );
+ // we gave a bad color
+ if ( color.error ) {
+ // don't error on an empty input - we want those allowed
+ if ( val !== '' ) {
+ input.addClass( 'iris-error' );
+ }
+ } else {
+ if ( color.toString() !== self._color.toString() ) {
+ // let's not do this on keyup for hex shortcodes
+ if ( ! ( event.type === 'keyup' && val.match( /^[0-9a-fA-F]{3}$/ ) ) ) {
+ self._setOption( 'color', color.toString() );
+ }
+ }
+ }
+ };
+
+ input.on( 'change', callback ).on( 'keyup', self._debounce( callback, debounceTimeout ) );
+
+ // If we initialized hidden, show on first focus. The rest is up to you.
+ if ( self.options.hide ) {
+ input.one( 'focus', function() {
+ self.show();
+ });
+ }
+ },
+
+ _initControls: function() {
+ var self = this,
+ controls = self.controls,
+ square = controls.square,
+ controlOpts = self.options.controls,
+ stripScale = self._scale[controlOpts.strip];
+
+ controls.stripSlider.slider({
+ orientation: 'vertical',
+ max: stripScale,
+ slide: function( event, ui ) {
+ self.active = 'strip';
+ // "reverse" for hue.
+ if ( controlOpts.strip === 'h' ) {
+ ui.value = stripScale - ui.value;
+ }
+
+ self._color[controlOpts.strip]( ui.value );
+ self._change.apply( self, arguments );
+ }
+ });
+
+ controls.squareDrag.draggable({
+ containment: controls.square.find( '.iris-square-inner' ),
+ zIndex: 1000,
+ cursor: 'move',
+ drag: function( event, ui ) {
+ self._squareDrag( event, ui );
+ },
+ start: function() {
+ square.addClass( 'iris-dragging' );
+ $(this).addClass( 'ui-state-focus' );
+ },
+ stop: function() {
+ square.removeClass( 'iris-dragging' );
+ $(this).removeClass( 'ui-state-focus' );
+ }
+ }).on( 'mousedown mouseup', function( event ) {
+ var focusClass = 'ui-state-focus';
+ event.preventDefault();
+ if (event.type === 'mousedown' ) {
+ self.picker.find( '.' + focusClass ).removeClass( focusClass ).blur();
+ $(this).addClass( focusClass ).focus();
+ } else {
+ $(this).removeClass( focusClass );
+ }
+ }).on( 'keydown', function( event ) {
+ var container = controls.square,
+ draggable = controls.squareDrag,
+ position = draggable.position(),
+ distance = self.options.width / 100; // Distance in pixels the draggable should be moved: 1 "stop"
+
+ // make alt key go "10"
+ if ( event.altKey ) {
+ distance *= 10;
+ }
+
+ // Reposition if one of the directional keys is pressed
+ switch ( event.keyCode ) {
+ case 37: position.left -= distance; break; // Left
+ case 38: position.top -= distance; break; // Up
+ case 39: position.left += distance; break; // Right
+ case 40: position.top += distance; break; // Down
+ default: return true; // Exit and bubble
+ }
+
+ // Keep draggable within container
+ position.left = Math.max( 0, Math.min( position.left, container.width() ) );
+ position.top = Math.max( 0, Math.min( position.top, container.height() ) );
+
+ draggable.css(position);
+ self._squareDrag( event, { position: position });
+ event.preventDefault();
+ });
+
+ // allow clicking on the square to move there and keep dragging
+ square.mousedown( function( event ) {
+ var squareOffset, pos;
+ // only left click
+ if ( event.which !== 1 ) {
+ return;
+ }
+
+ // prevent bubbling from the handle: no infinite loops
+ if ( ! $( event.target ).is( 'div' ) ) {
+ return;
+ }
+
+ squareOffset = self.controls.square.offset();
+ pos = {
+ top: event.pageY - squareOffset.top,
+ left: event.pageX - squareOffset.left
+ };
+ event.preventDefault();
+ self._squareDrag( event, { position: pos } );
+ event.target = self.controls.squareDrag.get(0);
+ self.controls.squareDrag.css( pos ).trigger( event );
+ });
+
+ // palettes
+ if ( self.options.palettes ) {
+ self._paletteListeners();
+ }
+ },
+
+ _paletteListeners: function() {
+ var self = this;
+ self.picker.find('.iris-palette-container').on('click.palette', '.iris-palette', function() {
+ self._color.fromCSS( $(this).data('color') );
+ self.active = 'external';
+ self._change();
+ }).on( 'keydown.palette', '.iris-palette', function( event ) {
+ if ( ! ( event.keyCode === 13 || event.keyCode === 32 ) ) {
+ return true;
+ }
+ event.stopPropagation();
+ $( this ).click();
+ });
+ },
+
+ _squareDrag: function( event, ui ) {
+ var self = this,
+ controlOpts = self.options.controls,
+ dimensions = self._squareDimensions(),
+ vertVal = Math.round( ( dimensions.h - ui.position.top ) / dimensions.h * self._scale[controlOpts.vert] ),
+ horizVal = self._scale[controlOpts.horiz] - Math.round( ( dimensions.w - ui.position.left ) / dimensions.w * self._scale[controlOpts.horiz] );
+
+ self._color[controlOpts.horiz]( horizVal )[controlOpts.vert]( vertVal );
+
+ self.active = 'square';
+ self._change.apply( self, arguments );
+ },
+
+ _setOption: function( key, value ) {
+ var self = this,
+ oldValue = self.options[key],
+ doDimensions = false,
+ hexLessColor,
+ newColor,
+ method;
+
+ // ensure the new value is set. We can reset to oldValue if some check wasn't met.
+ self.options[key] = value;
+
+ switch(key) {
+ case 'color':
+ // cast to string in case we have a number
+ value = '' + value;
+ hexLessColor = value.replace( /^#/, '' );
+ newColor = new Color( value ).setHSpace( self.options.mode );
+ if ( newColor.error ) {
+ self.options[key] = oldValue;
+ } else {
+ self._color = newColor;
+ self.options.color = self.options[key] = self._color.toString();
+ self.active = 'external';
+ self._change();
+ }
+ break;
+ case 'palettes':
+ doDimensions = true;
+
+ if ( value ) {
+ self._addPalettes();
+ } else {
+ self.picker.find('.iris-palette-container').remove();
+ }
+
+ // do we need to add events?
+ if ( ! oldValue ) {
+ self._paletteListeners();
+ }
+ break;
+ case 'width':
+ doDimensions = true;
+ break;
+ case 'border':
+ doDimensions = true;
+ method = value ? 'addClass' : 'removeClass';
+ self.picker[method]('iris-border');
+ break;
+ case 'mode':
+ case 'controls':
+ // if nothing's changed, let's bail, since this causes re-rendering the whole widget
+ if ( oldValue === value ) {
+ return;
+ }
+
+ // we're using these poorly named variables because they're already scoped.
+ // method is the element that Iris was called on. oldValue will be the options
+ method = self.element;
+ oldValue = self.options;
+ oldValue.hide = ! self.picker.is( ':visible' );
+ self.destroy();
+ self.picker.remove();
+ return $(self.element).iris(oldValue);
+ }
+
+ // Do we need to recalc dimensions?
+ if ( doDimensions ) {
+ self._dimensions(true);
+ }
+ },
+
+ _squareDimensions: function( forceRefresh ) {
+ var square = this.controls.square,
+ dimensions,
+ control;
+
+ if ( forceRefresh !== undef && square.data('dimensions') ) {
+ return square.data('dimensions');
+ }
+
+ control = this.controls.squareDrag;
+ dimensions = {
+ w: square.width(),
+ h: square.height()
+ };
+ square.data( 'dimensions', dimensions );
+ return dimensions;
+ },
+
+ _isNonHueControl: function( active, type ) {
+ if ( active === 'square' && this.options.controls.strip === 'h' ) {
+ return true;
+ } else if ( type === 'external' || ( type === 'h' && active === 'strip' ) ) {
+ return false;
+ }
+
+ return true;
+ },
+
+ _change: function() {
+ var self = this,
+ controls = self.controls,
+ color = self._getHSpaceColor(),
+ actions = [ 'square', 'strip' ],
+ controlOpts = self.options.controls,
+ type = controlOpts[self.active] || 'external',
+ oldHue = self.hue;
+
+ if ( self.active === 'strip' ) {
+ // take no action on any of the square sliders if we adjusted the strip
+ actions = [];
+ } else if ( self.active !== 'external' ) {
+ // for non-strip, non-external, strip should never change
+ actions.pop(); // conveniently the last item
+ }
+
+ $.each( actions, function(index, item) {
+ var value, dimensions, cssObj;
+ if ( item !== self.active ) {
+ switch ( item ) {
+ case 'strip':
+ // reverse for hue
+ value = ( controlOpts.strip === 'h' ) ? self._scale[controlOpts.strip] - color[controlOpts.strip] : color[controlOpts.strip];
+ controls.stripSlider.slider( 'value', value );
+ break;
+ case 'square':
+ dimensions = self._squareDimensions();
+ cssObj = {
+ left: color[controlOpts.horiz] / self._scale[controlOpts.horiz] * dimensions.w,
+ top: dimensions.h - ( color[controlOpts.vert] / self._scale[controlOpts.vert] * dimensions.h )
+ };
+
+ self.controls.squareDrag.css( cssObj );
+ break;
+ }
+ }
+ });
+
+ // Ensure that we don't change hue if we triggered a hue reset
+ if ( color.h !== oldHue && self._isNonHueControl( self.active, type ) ) {
+ self._color.h(oldHue);
+ }
+
+ // store hue for repeating above check next time
+ self.hue = self._color.h();
+
+ self.options.color = self._color.toString();
+
+ // only run after the first time
+ if ( self._inited ) {
+ self._trigger( 'change', { type: self.active }, { color: self._color } );
+ }
+
+ if ( self.element.is( ':input' ) && ! self._color.error ) {
+ self.element.removeClass( 'iris-error' );
+ if ( self.element.val() !== self._color.toString() ) {
+ self.element.val( self._color.toString() );
+ }
+ }
+
+ self._paint();
+ self._inited = true;
+ self.active = false;
+ },
+ // taken from underscore.js _.debounce method
+ _debounce: function( func, wait, immediate ) {
+ var timeout, result;
+ return function() {
+ var context = this,
+ args = arguments,
+ later,
+ callNow;
+
+ later = function() {
+ timeout = null;
+ if ( ! immediate) {
+ result = func.apply( context, args );
+ }
+ };
+
+ callNow = immediate && !timeout;
+ clearTimeout( timeout );
+ timeout = setTimeout( later, wait );
+ if ( callNow ) {
+ result = func.apply( context, args );
+ }
+ return result;
+ };
+ },
+ show: function() {
+ this.picker.show();
+ },
+ hide: function() {
+ this.picker.hide();
+ },
+ toggle: function() {
+ this.picker.toggle();
+ },
+ color: function(newColor) {
+ if ( newColor === true ) {
+ return this._color.clone();
+ } else if ( newColor === undef ) {
+ return this._color.toString();
+ }
+ this.option('color', newColor);
+ }
+ };
+ // initialize the widget
+ $.widget( 'a8c.iris', Iris );
+ // add CSS
+ $( '<style id="iris-css">' + _css + '</style>' ).appendTo( 'head' );
+
+}( jQuery ));
+/*! Color.js - v0.9.11 - 2013-08-09
+* https://github.com/Automattic/Color.js
+* Copyright (c) 2013 Matt Wiebe; Licensed GPLv2 */
+(function(global, undef) {
+
+ var Color = function( color, type ) {
+ if ( ! ( this instanceof Color ) )
+ return new Color( color, type );
+
+ return this._init( color, type );
+ };
+
+ Color.fn = Color.prototype = {
+ _color: 0,
+ _alpha: 1,
+ error: false,
+ // for preserving hue/sat in fromHsl().toHsl() flows
+ _hsl: { h: 0, s: 0, l: 0 },
+ // for preserving hue/sat in fromHsv().toHsv() flows
+ _hsv: { h: 0, s: 0, v: 0 },
+ // for setting hsl or hsv space - needed for .h() & .s() functions to function properly
+ _hSpace: 'hsl',
+ _init: function( color ) {
+ var func = 'noop';
+ switch ( typeof color ) {
+ case 'object':
+ // alpha?
+ if ( color.a !== undef )
+ this.a( color.a );
+ func = ( color.r !== undef ) ? 'fromRgb' :
+ ( color.l !== undef ) ? 'fromHsl' :
+ ( color.v !== undef ) ? 'fromHsv' : func;
+ return this[func]( color );
+ case 'string':
+ return this.fromCSS( color );
+ case 'number':
+ return this.fromInt( parseInt( color, 10 ) );
+ }
+ return this;
+ },
+
+ _error: function() {
+ this.error = true;
+ return this;
+ },
+
+ clone: function() {
+ var newColor = new Color( this.toInt() ),
+ copy = ['_alpha', '_hSpace', '_hsl', '_hsv', 'error'];
+ for ( var i = copy.length - 1; i >= 0; i-- ) {
+ newColor[ copy[i] ] = this[ copy[i] ];
+ }
+ return newColor;
+ },
+
+ setHSpace: function( space ) {
+ this._hSpace = ( space === 'hsv' ) ? space : 'hsl';
+ return this;
+ },
+
+ noop: function() {
+ return this;
+ },
+
+ fromCSS: function( color ) {
+ var list,
+ leadingRE = /^(rgb|hs(l|v))a?\(/;
+ this.error = false;
+
+ // whitespace and semicolon trim
+ color = color.replace(/^\s+/, '').replace(/\s+$/, '').replace(/;$/, '');
+
+ if ( color.match(leadingRE) && color.match(/\)$/) ) {
+ list = color.replace(/(\s|%)/g, '').replace(leadingRE, '').replace(/,?\);?$/, '').split(',');
+
+ if ( list.length < 3 )
+ return this._error();
+
+ if ( list.length === 4 ) {
+ this.a( parseFloat( list.pop() ) );
+ // error state has been set to true in .a() if we passed NaN
+ if ( this.error )
+ return this;
+ }
+
+ for (var i = list.length - 1; i >= 0; i--) {
+ list[i] = parseInt(list[i], 10);
+ if ( isNaN( list[i] ) )
+ return this._error();
+ }
+
+ if ( color.match(/^rgb/) ) {
+ return this.fromRgb( {
+ r: list[0],
+ g: list[1],
+ b: list[2]
+ } );
+ } else if ( color.match(/^hsv/) ) {
+ return this.fromHsv( {
+ h: list[0],
+ s: list[1],
+ v: list[2]
+ } );
+ } else {
+ return this.fromHsl( {
+ h: list[0],
+ s: list[1],
+ l: list[2]
+ } );
+ }
+ } else {
+ // must be hex amirite?
+ return this.fromHex( color );
+ }
+ },
+
+ fromRgb: function( rgb, preserve ) {
+ if ( typeof rgb !== 'object' || rgb.r === undef || rgb.g === undef || rgb.b === undef )
+ return this._error();
+
+ this.error = false;
+ return this.fromInt( parseInt( ( rgb.r << 16 ) + ( rgb.g << 8 ) + rgb.b, 10 ), preserve );
+ },
+
+ fromHex: function( color ) {
+ color = color.replace(/^#/, '').replace(/^0x/, '');
+ if ( color.length === 3 ) {
+ color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
+ }
+
+ // rough error checking - this is where things go squirrely the most
+ this.error = ! /^[0-9A-F]{6}$/i.test( color );
+ return this.fromInt( parseInt( color, 16 ) );
+ },
+
+ fromHsl: function( hsl ) {
+ var r, g, b, q, p, h, s, l;
+
+ if ( typeof hsl !== 'object' || hsl.h === undef || hsl.s === undef || hsl.l === undef )
+ return this._error();
+
+ this._hsl = hsl; // store it
+ this._hSpace = 'hsl'; // implicit
+ h = hsl.h / 360; s = hsl.s / 100; l = hsl.l / 100;
+ if ( s === 0 ) {
+ r = g = b = l; // achromatic
+ }
+ else {
+ q = l < 0.5 ? l * ( 1 + s ) : l + s - l * s;
+ p = 2 * l - q;
+ r = this.hue2rgb( p, q, h + 1/3 );
+ g = this.hue2rgb( p, q, h );
+ b = this.hue2rgb( p, q, h - 1/3 );
+ }
+ return this.fromRgb( {
+ r: r * 255,
+ g: g * 255,
+ b: b * 255
+ }, true ); // true preserves hue/sat
+ },
+
+ fromHsv: function( hsv ) {
+ var h, s, v, r, g, b, i, f, p, q, t;
+ if ( typeof hsv !== 'object' || hsv.h === undef || hsv.s === undef || hsv.v === undef )
+ return this._error();
+
+ this._hsv = hsv; // store it
+ this._hSpace = 'hsv'; // implicit
+
+ h = hsv.h / 360; s = hsv.s / 100; v = hsv.v / 100;
+ i = Math.floor( h * 6 );
+ f = h * 6 - i;
+ p = v * ( 1 - s );
+ q = v * ( 1 - f * s );
+ t = v * ( 1 - ( 1 - f ) * s );
+
+ switch( i % 6 ) {
+ case 0:
+ r = v; g = t; b = p;
+ break;
+ case 1:
+ r = q; g = v; b = p;
+ break;
+ case 2:
+ r = p; g = v; b = t;
+ break;
+ case 3:
+ r = p; g = q; b = v;
+ break;
+ case 4:
+ r = t; g = p; b = v;
+ break;
+ case 5:
+ r = v; g = p; b = q;
+ break;
+ }
+
+ return this.fromRgb( {
+ r: r * 255,
+ g: g * 255,
+ b: b * 255
+ }, true ); // true preserves hue/sat
+
+ },
+ // everything comes down to fromInt
+ fromInt: function( color, preserve ) {
+ this._color = parseInt( color, 10 );
+
+ if ( isNaN( this._color ) )
+ this._color = 0;
+
+ // let's coerce things
+ if ( this._color > 16777215 )
+ this._color = 16777215;
+ else if ( this._color < 0 )
+ this._color = 0;
+
+ // let's not do weird things
+ if ( preserve === undef ) {
+ this._hsv.h = this._hsv.s = this._hsl.h = this._hsl.s = 0;
+ }
+ // EVENT GOES HERE
+ return this;
+ },
+
+ hue2rgb: function( p, q, t ) {
+ if ( t < 0 ) {
+ t += 1;
+ }
+ if ( t > 1 ) {
+ t -= 1;
+ }
+ if ( t < 1/6 ) {
+ return p + ( q - p ) * 6 * t;
+ }
+ if ( t < 1/2 ) {
+ return q;
+ }
+ if ( t < 2/3 ) {
+ return p + ( q - p ) * ( 2/3 - t ) * 6;
+ }
+ return p;
+ },
+
+ toString: function() {
+ var hex = parseInt( this._color, 10 ).toString( 16 );
+ if ( this.error )
+ return '';
+ // maybe left pad it
+ if ( hex.length < 6 ) {
+ for (var i = 6 - hex.length - 1; i >= 0; i--) {
+ hex = '0' + hex;
+ }
+ }
+ return '#' + hex;
+ },
+
+ toCSS: function( type, alpha ) {
+ type = type || 'hex';
+ alpha = parseFloat( alpha || this._alpha );
+ switch ( type ) {
+ case 'rgb':
+ case 'rgba':
+ var rgb = this.toRgb();
+ if ( alpha < 1 ) {
+ return "rgba( " + rgb.r + ", " + rgb.g + ", " + rgb.b + ", " + alpha + " )";
+ }
+ else {
+ return "rgb( " + rgb.r + ", " + rgb.g + ", " + rgb.b + " )";
+ }
+ break;
+ case 'hsl':
+ case 'hsla':
+ var hsl = this.toHsl();
+ if ( alpha < 1 ) {
+ return "hsla( " + hsl.h + ", " + hsl.s + "%, " + hsl.l + "%, " + alpha + " )";
+ }
+ else {
+ return "hsl( " + hsl.h + ", " + hsl.s + "%, " + hsl.l + "% )";
+ }
+ break;
+ default:
+ return this.toString();
+ }
+ },
+
+ toRgb: function() {
+ return {
+ r: 255 & ( this._color >> 16 ),
+ g: 255 & ( this._color >> 8 ),
+ b: 255 & ( this._color )
+ };
+ },
+
+ toHsl: function() {
+ var rgb = this.toRgb();
+ var r = rgb.r / 255, g = rgb.g / 255, b = rgb.b / 255;
+ var max = Math.max( r, g, b ), min = Math.min( r, g, b );
+ var h, s, l = ( max + min ) / 2;
+
+ if ( max === min ) {
+ h = s = 0; // achromatic
+ } else {
+ var d = max - min;
+ s = l > 0.5 ? d / ( 2 - max - min ) : d / ( max + min );
+ switch ( max ) {
+ case r: h = ( g - b ) / d + ( g < b ? 6 : 0 );
+ break;
+ case g: h = ( b - r ) / d + 2;
+ break;
+ case b: h = ( r - g ) / d + 4;
+ break;
+ }
+ h /= 6;
+ }
+
+ // maintain hue & sat if we've been manipulating things in the HSL space.
+ h = Math.round( h * 360 );
+ if ( h === 0 && this._hsl.h !== h ) {
+ h = this._hsl.h;
+ }
+ s = Math.round( s * 100 );
+ if ( s === 0 && this._hsl.s ) {
+ s = this._hsl.s;
+ }
+
+ return {
+ h: h,
+ s: s,
+ l: Math.round( l * 100 )
+ };
+
+ },
+
+ toHsv: function() {
+ var rgb = this.toRgb();
+ var r = rgb.r / 255, g = rgb.g / 255, b = rgb.b / 255;
+ var max = Math.max( r, g, b ), min = Math.min( r, g, b );
+ var h, s, v = max;
+ var d = max - min;
+ s = max === 0 ? 0 : d / max;
+
+ if ( max === min ) {
+ h = s = 0; // achromatic
+ } else {
+ switch( max ){
+ case r:
+ h = ( g - b ) / d + ( g < b ? 6 : 0 );
+ break;
+ case g:
+ h = ( b - r ) / d + 2;
+ break;
+ case b:
+ h = ( r - g ) / d + 4;
+ break;
+ }
+ h /= 6;
+ }
+
+ // maintain hue & sat if we've been manipulating things in the HSV space.
+ h = Math.round( h * 360 );
+ if ( h === 0 && this._hsv.h !== h ) {
+ h = this._hsv.h;
+ }
+ s = Math.round( s * 100 );
+ if ( s === 0 && this._hsv.s ) {
+ s = this._hsv.s;
+ }
+
+ return {
+ h: h,
+ s: s,
+ v: Math.round( v * 100 )
+ };
+ },
+
+ toInt: function() {
+ return this._color;
+ },
+
+ toIEOctoHex: function() {
+ // AARRBBGG
+ var hex = this.toString();
+ var AA = parseInt( 255 * this._alpha, 10 ).toString(16);
+ if ( AA.length === 1 ) {
+ AA = '0' + AA;
+ }
+ return '#' + AA + hex.replace(/^#/, '' );
+ },
+
+ toLuminosity: function() {
+ var rgb = this.toRgb();
+ return 0.2126 * Math.pow( rgb.r / 255, 2.2 ) + 0.7152 * Math.pow( rgb.g / 255, 2.2 ) + 0.0722 * Math.pow( rgb.b / 255, 2.2);
+ },
+
+ getDistanceLuminosityFrom: function( color ) {
+ if ( ! ( color instanceof Color ) ) {
+ throw 'getDistanceLuminosityFrom requires a Color object';
+ }
+ var lum1 = this.toLuminosity();
+ var lum2 = color.toLuminosity();
+ if ( lum1 > lum2 ) {
+ return ( lum1 + 0.05 ) / ( lum2 + 0.05 );
+ }
+ else {
+ return ( lum2 + 0.05 ) / ( lum1 + 0.05 );
+ }
+ },
+
+ getMaxContrastColor: function() {
+ var lum = this.toLuminosity();
+ var hex = ( lum >= 0.5 ) ? '000000' : 'ffffff';
+ return new Color( hex );
+ },
+
+ getReadableContrastingColor: function( bgColor, minContrast ) {
+ if ( ! bgColor instanceof Color ) {
+ return this;
+ }
+
+ // you shouldn't use less than 5, but you might want to.
+ var targetContrast = ( minContrast === undef ) ? 5 : minContrast;
+ // working things
+ var contrast = bgColor.getDistanceLuminosityFrom( this );
+ var maxContrastColor = bgColor.getMaxContrastColor();
+ var maxContrast = maxContrastColor.getDistanceLuminosityFrom( bgColor );
+
+ // if current max contrast is less than the target contrast, we had wishful thinking.
+ // still, go max
+ if ( maxContrast <= targetContrast ) {
+ return maxContrastColor;
+ }
+ // or, we might already have sufficient contrast
+ else if ( contrast >= targetContrast ) {
+ return this;
+ }
+
+ var incr = ( 0 === maxContrastColor.toInt() ) ? -1 : 1;
+ while ( contrast < targetContrast ) {
+ this.l( incr, true ); // 2nd arg turns this into an incrementer
+ contrast = this.getDistanceLuminosityFrom( bgColor );
+ // infininite loop prevention: you never know.
+ if ( this._color === 0 || this._color === 16777215 ) {
+ break;
+ }
+ }
+
+ return this;
+
+ },
+
+ a: function( val ) {
+ if ( val === undef )
+ return this._alpha;
+
+ var a = parseFloat( val );
+
+ if ( isNaN( a ) )
+ return this._error();
+
+ this._alpha = a;
+ return this;
+ },
+
+ // TRANSFORMS
+
+ darken: function( amount ) {
+ amount = amount || 5;
+ return this.l( - amount, true );
+ },
+
+ lighten: function( amount ) {
+ amount = amount || 5;
+ return this.l( amount, true );
+ },
+
+ saturate: function( amount ) {
+ amount = amount || 15;
+ return this.s( amount, true );
+ },
+
+ desaturate: function( amount ) {
+ amount = amount || 15;
+ return this.s( - amount, true );
+ },
+
+ toGrayscale: function() {
+ return this.setHSpace('hsl').s( 0 );
+ },
+
+ getComplement: function() {
+ return this.h( 180, true );
+ },
+
+ getSplitComplement: function( step ) {
+ step = step || 1;
+ var incr = 180 + ( step * 30 );
+ return this.h( incr, true );
+ },
+
+ getAnalog: function( step ) {
+ step = step || 1;
+ var incr = step * 30;
+ return this.h( incr, true );
+ },
+
+ getTetrad: function( step ) {
+ step = step || 1;
+ var incr = step * 60;
+ return this.h( incr, true );
+ },
+
+ getTriad: function( step ) {
+ step = step || 1;
+ var incr = step * 120;
+ return this.h( incr, true );
+ },
+
+ _partial: function( key ) {
+ var prop = shortProps[key];
+ return function( val, incr ) {
+ var color = this._spaceFunc('to', prop.space);
+
+ // GETTER
+ if ( val === undef )
+ return color[key];
+
+ // INCREMENT
+ if ( incr === true )
+ val = color[key] + val;
+
+ // MOD & RANGE
+ if ( prop.mod )
+ val = val % prop.mod;
+ if ( prop.range )
+ val = ( val < prop.range[0] ) ? prop.range[0] : ( val > prop.range[1] ) ? prop.range[1] : val;
+
+ // NEW VALUE
+ color[key] = val;
+
+ return this._spaceFunc('from', prop.space, color);
+ };
+ },
+
+ _spaceFunc: function( dir, s, val ) {
+ var space = s || this._hSpace,
+ funcName = dir + space.charAt(0).toUpperCase() + space.substr(1);
+ return this[funcName](val);
+ }
+ };
+
+ var shortProps = {
+ h: {
+ mod: 360
+ },
+ s: {
+ range: [0,100]
+ },
+ l: {
+ space: 'hsl',
+ range: [0,100]
+ },
+ v: {
+ space: 'hsv',
+ range: [0,100]
+ },
+ r: {
+ space: 'rgb',
+ range: [0,255]
+ },
+ g: {
+ space: 'rgb',
+ range: [0,255]
+ },
+ b: {
+ space: 'rgb',
+ range: [0,255]
+ }
+ };
+
+ for ( var key in shortProps ) {
+ if ( shortProps.hasOwnProperty( key ) )
+ Color.fn[key] = Color.fn._partial(key);
+ }
+
+ // play nicely with Node + browser
+ if ( typeof exports === 'object' )
+ module.exports = Color;
+ else
+ global.Color = Color;
+
+}(this));
diff --git a/lib/plugins/styling/lang/en/intro.txt b/lib/plugins/styling/lang/en/intro.txt
new file mode 100644
index 000000000..4ea55172f
--- /dev/null
+++ b/lib/plugins/styling/lang/en/intro.txt
@@ -0,0 +1,2 @@
+This tool allows you to change certain style settings of your currently selected template.
+All changes are stored in a local configuration file and are upgrade safe. \ No newline at end of file
diff --git a/lib/plugins/styling/lang/en/lang.php b/lib/plugins/styling/lang/en/lang.php
new file mode 100644
index 000000000..e50c6b912
--- /dev/null
+++ b/lib/plugins/styling/lang/en/lang.php
@@ -0,0 +1,32 @@
+<?php
+/**
+ * English language file for styling plugin
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+// menu entry for admin plugins
+$lang['js']['menu'] = 'Template Style Settings';
+
+// custom language strings for the plugin
+$lang['error'] = 'Sorry, this template does not support this functionality.';
+
+$lang['btn_preview'] = 'Preview your changes';
+$lang['btn_save'] = 'Save your changes';
+$lang['btn_reset'] = 'Reset your current changes';
+$lang['btn_revert'] = 'Revert all styles back to the template\'s default';
+
+// default guaranteed placeholders
+$lang['__text__'] = 'Main text color';
+$lang['__background__'] = 'Main background color';
+$lang['__text_alt__'] = 'Alternative text color';
+$lang['__background_alt__'] = 'Alternative background color';
+$lang['__text_neu__'] = 'Neutral text color';
+$lang['__background_neu__'] = 'Neutral background color';
+$lang['__border__'] = 'Border color';
+$lang['__highlight__'] = 'Highlight color (for search results mainly)';
+
+
+
+
+//Setup VIM: ex: et ts=4 :
diff --git a/lib/plugins/styling/plugin.info.txt b/lib/plugins/styling/plugin.info.txt
new file mode 100644
index 000000000..cdf01ee6f
--- /dev/null
+++ b/lib/plugins/styling/plugin.info.txt
@@ -0,0 +1,7 @@
+base styling
+author Andreas Gohr
+email andi@splitbrain.org
+date 2015-05-16
+name styling plugin
+desc Allows to edit style.ini replacements
+url https://www.dokuwiki.org/plugin:styling
diff --git a/lib/plugins/styling/script.js b/lib/plugins/styling/script.js
new file mode 100644
index 000000000..7c8ffae91
--- /dev/null
+++ b/lib/plugins/styling/script.js
@@ -0,0 +1,91 @@
+/* DOKUWIKI:include_once iris.js */
+
+jQuery(function () {
+ // user openend the admin page, set cookie and redirect
+ if (jQuery('#plugin__styling').length) {
+ DokuCookie.setValue('styling_plugin', 1);
+ document.location.href = document.location.href.replace(/&?do=admin/, '');
+ }
+
+ // continue only if the styling Dialog is currently enabled
+ if (DokuCookie.getValue('styling_plugin') != 1) return;
+
+ var styling_timeout = null;
+
+ // create dialog element
+ var $dialog = jQuery(document.createElement('div'));
+ jQuery('body').append($dialog);
+
+
+ /**
+ * updates the current CSS with a new preview one
+ */
+ function styling_updateCSS() {
+ var now = new Date().getTime();
+ var $style = jQuery('link[rel=stylesheet][href*="lib/exe/css.php"]');
+ $style.attr('href', DOKU_BASE + 'lib/exe/css.php?preview=1&tseed=' + now);
+ }
+
+ /**
+ * save current values and reload preview (debounced)
+ */
+ function styling_saveAndUpdate() {
+ if (styling_timeout) window.clearTimeout(styling_timeout);
+ styling_timeout = window.setTimeout(function () {
+ styling_timeout = null;
+
+ var params = $dialog.find('input[type=text]').serializeArray();
+ params[params.length] = { name: 'call', value: 'plugin_styling'};
+ params[params.length] = {name: 'run', value: 'preview'};
+
+ jQuery.post(
+ DOKU_BASE + 'lib/exe/ajax.php',
+ params,
+ styling_updateCSS
+ );
+ }, 500);
+ }
+
+ // load the dialog content and apply listeners
+ $dialog.load(
+ DOKU_BASE + 'lib/exe/ajax.php',
+ {
+ 'call': 'plugin_styling',
+ 'run': 'html',
+ 'id': JSINFO.id
+ },
+ function () {
+ // load the preview template
+ styling_updateCSS();
+
+ // open the dialog
+ $dialog.dialog({
+ 'title': LANG.plugins.styling.menu,
+ 'width': 500,
+ 'height': 500,
+ 'top': 50,
+ 'position': { 'my': 'left bottom', 'at': 'left bottom', 'of': window },
+ // bring everything back to normal
+ 'close': function (event, ui) {
+ // disable the styling plugin again
+ DokuCookie.setValue('styling_plugin', 0);
+ // reload
+ document.location.reload()
+ }
+ });
+
+ // we don't need the manual preview in JS mode
+ $dialog.find('.btn_preview').hide();
+
+ // add the color picker FIXME add saveAndUpdate to correct event
+ $dialog.find('.color').iris({ });
+
+ // listen to keyup events
+ $dialog.find('input[type=text]').keyup(function () {
+ styling_saveAndUpdate();
+ });
+
+ }
+ );
+
+});
diff --git a/lib/tpl/dokuwiki/lang/en/lang.php b/lib/tpl/dokuwiki/lang/en/lang.php
new file mode 100644
index 000000000..b7b3e7fa1
--- /dev/null
+++ b/lib/tpl/dokuwiki/lang/en/lang.php
@@ -0,0 +1,12 @@
+<?php
+
+// style.ini values
+
+$lang['__background_site__'] = 'Color for the very background (behind the content box)';
+$lang['__link__'] = 'The general link color';
+$lang['__existing__'] = 'The color for links to existing pages';
+$lang['__missing__'] = 'The color for links to non-existing pages';
+$lang['__site_width__'] = 'The width of the full site (can be any length unit: %, px, em, ...)';
+$lang['__sidebar_width__'] = 'The width of the sidebar, if any (can be any length unit: %, px, em, ...)';
+$lang['__tablet_width__'] = 'Below screensizes of this width, the site switches to tablet mode';
+$lang['__phone_width__'] = 'Below screensizes of this width, the site switches to phone mode';
diff --git a/lib/tpl/dokuwiki/lang/en/style.txt b/lib/tpl/dokuwiki/lang/en/style.txt
new file mode 100644
index 000000000..7bf3e1a82
--- /dev/null
+++ b/lib/tpl/dokuwiki/lang/en/style.txt
@@ -0,0 +1,4 @@
+If you want to adjust the logo, simply use the Media Manager to upload a ''logo.png'' into the ''wiki'' or the root namespace and it
+will be automatically used. You can also upload a ''favicon.ico'' there. If you use a closed
+wiki it is recommended to make the ''wiki'' (or root) namespace world readable in the ACL settings or
+your logo is not shown to not logged in users.