From 29f7b272a54ccb268148c48c2eefb00748dc2512 Mon Sep 17 00:00:00 2001
From: Thomas Bruederli <thomas@roundcube.net>
Date: Thu, 14 Aug 2014 13:33:37 -0400
Subject: [PATCH] Add Zen mode to message compose body (#1489198) using the Idered/zen-form jQuery plugin

---
 skins/larry/images/zen-form-sprites.png |    0 
 skins/larry/zen-form.css                |  340 ++++++++++++++++++++++++
 skins/larry/includes/footer.html        |    2 
 program/localization/en_US/labels.inc   |    5 
 skins/larry/mail.css                    |   15 +
 skins/larry/zen-form.js                 |  392 ++++++++++++++++++++++++++++
 skins/larry/templates/compose.html      |    4 
 skins/larry/ui.js                       |   24 +
 8 files changed, 781 insertions(+), 1 deletions(-)

diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc
index 449b278..c2c2f5e 100644
--- a/program/localization/en_US/labels.inc
+++ b/program/localization/en_US/labels.inc
@@ -300,6 +300,11 @@
 $labels['addreplyto'] = 'Add Reply-To';
 $labels['addfollowupto'] = 'Add Followup-To';
 
+// zen mode labels
+$labels['editfullscreen'] = 'Edit in fullscreen';
+$labels['exitfullscreen'] = 'Exit fullscreen mode';
+$labels['switchtheme'] = 'Switch theme';
+
 // mdn
 $labels['mdnrequest'] = 'The sender of this message has asked to be notified when you read this message. Do you wish to notify the sender?';
 $labels['receiptread'] = 'Return Receipt (read)';
diff --git a/skins/larry/images/zen-form-sprites.png b/skins/larry/images/zen-form-sprites.png
new file mode 100755
index 0000000..8cd36ef
--- /dev/null
+++ b/skins/larry/images/zen-form-sprites.png
Binary files differ
diff --git a/skins/larry/includes/footer.html b/skins/larry/includes/footer.html
index 6cd3e62..0f00d1f 100644
--- a/skins/larry/includes/footer.html
+++ b/skins/larry/includes/footer.html
@@ -7,6 +7,8 @@
 $(document).ready(function(){
 	UI.set('errortitle', '<roundcube:label name="errortitle" quoting="javascript" />');
 	UI.set('toggleoptions', '<roundcube:label name="toggleadvancedoptions" quoting="javascript" />');
+	UI.set('exitfullscreen', '<roundcube:label name="exitfullscreen" quoting="javascript" />');
+	UI.set('switchtheme', '<roundcube:label name="switchtheme" quoting="javascript" />');
 	UI.init();
 });
 
diff --git a/skins/larry/mail.css b/skins/larry/mail.css
index 7eb3242..a78bfe0 100644
--- a/skins/larry/mail.css
+++ b/skins/larry/mail.css
@@ -1298,6 +1298,21 @@
 	bottom: 42px;
 }
 
+#composebodycontainer .go-zen {
+	position: absolute;
+	top: 6px;
+	right: 4px;
+}
+
+#composebodycontainer .icon-go-zen {
+	display: inline-block;
+	width: 16px;
+	height: 14px;
+	background: url('data:image/gif;base64,R0lGODlhDgAMAPABAHl5ef///yH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAQAsAAAAAA4ADAAAAh+EEaln6t+YRHGegHB2bW3PfUk3itqoWdSqns8rwUcBADs=') 1px 1px no-repeat;
+	text-indent: -5000px;
+	overflow: hidden;
+}
+
 #composebody {
 	position: absolute;
 	top: 0;
diff --git a/skins/larry/templates/compose.html b/skins/larry/templates/compose.html
index 04a987f..dfac0e4 100644
--- a/skins/larry/templates/compose.html
+++ b/skins/larry/templates/compose.html
@@ -3,6 +3,7 @@
 <head>
 <title><roundcube:object name="pagetitle" /></title>
 <roundcube:include file="/includes/links.html" />
+<link rel="stylesheet" type="text/css" href="/zen-form.css" />
 <roundcube:if condition="config:enable_spellcheck" />
 <link rel="stylesheet" type="text/css" href="/googiespell.css" />
 <roundcube:endif />
@@ -169,6 +170,7 @@
 	<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" tabindex="1" />
+		<a href="#" class="go-zen" title="<roundcube:label name='editfullscreen' />"><span class="icon-go-zen"><roundcube:label name="editfullscreen" /></span></a>
 	</div>
 	<div id="compose-attachments" class="rightcol" role="region" aria-labelledby="aria-label-composeattachments">
 		<h2 id="aria-label-composeattachments" class="voice"><roundcube:label name="attachments" /></h2>
@@ -217,6 +219,8 @@
 	</ul>
 </div>
 
+<script type="text/javascript" src="/zen-form.js"></script>
+
 <roundcube:include file="/includes/footer.html" />
 
 </body>
diff --git a/skins/larry/ui.js b/skins/larry/ui.js
index 153abdc..463e956 100644
--- a/skins/larry/ui.js
+++ b/skins/larry/ui.js
@@ -212,6 +212,25 @@
 
         new rcube_splitter({ id:'composesplitterv', p1:'#composeview-left', p2:'#composeview-right',
           orientation:'v', relative:true, start:206, min:170, size:12, render:layout_composeview }).init();
+
+        // enable zen-mode for message body
+        if ($.fn.zenForm) {
+            $('#composebody').zenForm({ theme: 'light' })
+                .on('zf-initialized', function(event, zenbox) {
+                    var subject = $('#compose-subject').val(),
+                        $zenbox = $(zenbox);
+                    $('<h2>').addClass('zen-forms-subject')
+                        .text(subject ? subject : rcmail.gettext('nosubject'))
+                        .insertBefore('.zen-forms-close-button', zenbox);
+
+                    $zenbox.find('.zen-forms-close-button').attr('title', env.exitfullscreen)
+                    $zenbox.find('.zen-forms-theme-switch').attr('title', env.switchtheme)
+                    $zenbox.find('.input').first().focus();
+                })
+                .on('zf-destroyed', function(event) {
+                    $('#composebody').focus();
+                });
+        }
       }
       else if (rcmail.env.action == 'list' || !rcmail.env.action) {
         var previewframe = $('#mailpreviewframe').is(':visible');
@@ -497,7 +516,7 @@
     form.css('overflow', ovflw > 0 ? 'auto' : 'hidden');
 
     w = body.parent().width() - 5;
-    h = body.parent().height() - 8;
+    h = body.parent().height() - 16;
     body.width(w).height(h);
 
     $('#composebodycontainer > div').width(w+8);
@@ -508,6 +527,9 @@
 
     var abooks = $('#directorylist');
     $('#compose-contacts .scroller').css('top', abooks.position().top + abooks.outerHeight());
+
+    // hide zen-mode switch in HTML mode
+    $('a.go-zen')[($('#composebody_ifr').is(':visible') ? 'hide' : 'show')]();
   }
 
 
diff --git a/skins/larry/zen-form.css b/skins/larry/zen-form.css
new file mode 100755
index 0000000..3e8b8f6
--- /dev/null
+++ b/skins/larry/zen-form.css
@@ -0,0 +1,340 @@
+/* ================================== *\
+ * Zen Form
+ * ================================== */
+
+.zen-forms {
+	font: normal 18px/1.2 "Segoe UI", Frutiger, "Frutiger Linotype", "Dejavu Sans", "Helvetica Neue", Arial, sans-serif;
+	position: fixed;
+	z-index: 99999;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	padding: 20px 40px 20px 20px;
+	-webkit-box-sizing: border-box;
+	   -moz-box-sizing: border-box;
+	        box-sizing: border-box;
+}
+
+.zen-forms-body-wrap {
+	width: 0;
+	height: 0;
+	overflow: hidden;
+}
+
+body {
+	max-width: 100%;
+}
+
+.zen-forms-input-wrap {
+	max-width: 800px;
+	position: relative;
+	margin: 0 auto;
+	height: 90%;
+}
+
+/**
+ * Buttons style
+ */
+
+.zen-forms-header {
+	max-width: 800px;
+	margin: 0 auto 16px auto;
+}
+
+.zen-forms-header .zen-forms-subject {
+	float: left;
+	display: inline;
+	margin: 0;
+	color: #808080;
+	font-size: 16px;
+}
+
+.zen-forms-close-button,
+.zen-forms-theme-switch {
+	float: right;
+	position: relative;
+	display: inline-block;
+	cursor: pointer;
+	font-size: .8em;
+	padding: 0 .5em;
+	vertical-align: top;
+	width: 18px;
+	height: 18px;
+	text-indent: -5000px;
+	overflow: hidden;
+	white-space: nowrap;
+}
+
+.zen-icon {
+	background-image: url(images/zen-form-sprites.png);
+	display: inline-block;
+	line-height: 1;
+	position: absolute;
+	zoom: 1;
+	width: 16px;
+	height: 16px;
+	top: 1px;
+	left: 1px;
+}
+
+.zen-icon:before {
+	content: "";
+	display: block;
+	width: 0;
+	height: 100%;
+}
+
+.light-theme .zen-forms-close-button:hover .zen-icon,
+.zen-icon--close {
+	background-position: -16px 0;
+}
+
+.light-theme .zen-forms-theme-switch:hover .zen-icon,
+.zen-icon--theme {
+	background-position: 0 0;
+}
+
+.light-theme .zen-icon--close,
+.zen-forms-close-button:hover .zen-icon {
+	background-position: -16px -16px;
+}
+
+.light-theme .zen-icon--theme,
+.zen-forms-theme-switch:hover .zen-icon {
+	background-position: 0 -16px;
+}
+
+
+/**
+ * Inputs basic style
+ */
+
+.zen-forms-header:after,
+.zen-forms-input-wrap:after {
+	clear: both;
+	content: '';
+	display: table;
+	margin-bottom: 2px;
+}
+
+.zen-forms .input {
+	box-shadow: none;
+	background: none;
+	text-shadow: none;
+	padding: 7px 0;
+	width: 100%;
+	height: 100%;
+	border-radius: 0;
+	border: none;
+	margin: 0;
+	cursor: pointer;
+	font: inherit;
+}
+
+.zen-forms .input:focus {
+	cursor: text;
+}
+
+.zen-forms .select {
+	display: none;
+}
+
+.zen-forms .custom-select-wrap + label {
+	padding: 8px 3px;
+	position: static;
+	display: inline-block;
+	width: auto;
+	float: left;
+}
+
+.zen-forms .custom-select-wrap {
+	display: inline-block;
+	vertical-align: top;
+	max-width: 100%;
+}
+
+.zen-forms .custom-select {
+	border-radius: 5px;
+	top: 0;
+	right: 0;
+	left: 0;
+	overflow: auto;
+	max-height: 300px;
+}
+
+.zen-forms .custom-select a {
+	display: inline-block;
+	display: block;
+	text-decoration: none;
+	padding: 6px 10px;
+	display: none;
+	max-width: 100%;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+.zen-forms .custom-select span {
+	position: relative;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	max-width: 100%;
+}
+
+.zen-forms .custom-select.is-open a,
+.zen-forms .custom-select .selected {
+	position: static;
+	display: block;
+}
+
+.zen-forms textarea {
+	resize: none;
+}
+
+.zen-forms label {
+	cursor: pointer;
+	padding: 8px 8px 8px 0;
+	display: inline-block;
+	vertical-align: top;
+	font: inherit;
+	-webkit-box-sizing: border-box;
+	   -moz-box-sizing: border-box;
+	        box-sizing: border-box;
+}
+
+.zen-forms .input + label {
+	position: absolute;
+	display: none;
+	top: 0;
+	width: 100%;
+}
+
+.zen-forms .empty + label {
+	display: block;
+}
+
+.zen-forms .input:focus + label {
+	display: none;
+}
+
+@media (min-width: 1024px) {
+
+	.zen-forms .input:focus + label {
+		display: block;
+		background: #000;
+		font-size: .8em;
+		padding: 0 .5em;
+		line-height: 2;
+		border-radius: 3px;
+		margin: 5px 15px 0 0;
+		right: 100%;
+		width: auto;
+		white-space: nowrap;
+		color: #fff;
+		opacity: .5;
+	}
+
+	.zen-forms .input:focus + label:after {
+		content: '';
+		top: 50%;
+		left: 100%;
+		width: 0px;
+		height: 0px;
+		margin-top: -4px;
+		position: absolute;
+		border-style: solid;
+		border-width: 4px 0 4px 4px;
+		border-color: transparent transparent transparent #000000;
+	}
+
+}
+
+.zen-forms :focus {
+	outline-color: transparent;
+	outline-style: none;
+}
+
+/**
+ * Dark theme
+ */
+.zen-forms {
+	background: #151a1c;
+}
+
+.zen-forms .zen-forms-close-button,
+.zen-forms .zen-forms-theme-switch {
+	color: #768991;
+}
+
+.zen-forms .zen-forms-close-button:hover,
+.zen-forms .zen-forms-theme-switch:hover {
+	color: #707071;
+}
+
+.zen-forms label {
+	color: #415056;
+}
+
+.zen-forms .input {
+	color: #768991;
+}
+
+.zen-forms .custom-select {
+	background-color: #151a1c;
+	border: 2px solid #0f1314;
+}
+
+.zen-forms .custom-select a {
+	color: #768991;
+}
+
+.zen-forms .custom-select.is-open a:hover {
+	background-color: #181e20;
+}
+
+.zen-forms .custom-select.is-open .selected {
+	background-color: #0f1314;
+}
+
+/**
+ * Light theme
+ */
+.zen-forms.light-theme {
+	background: #fefefe;
+}
+
+.zen-forms.light-theme .zen-forms-close-button,
+.zen-forms.light-theme .zen-forms-theme-switch {
+	color: #707071;
+}
+
+.zen-forms.light-theme .zen-forms-close-button:hover,
+.zen-forms.light-theme .zen-forms-theme-switch:hover {
+	color: #768991;
+}
+
+.zen-forms.light-theme label {
+	color: #959697;
+}
+
+.zen-forms.light-theme .input {
+	color: #707071;
+}
+
+.zen-forms.light-theme .custom-select {
+	background-color: #fefefe;
+	border: 2px solid #e5e5e5;
+}
+
+.zen-forms.light-theme .custom-select a {
+	color: #707071;
+}
+
+.zen-forms.light-theme .custom-select.is-open a:hover {
+	background-color: #f5f5f5;
+}
+
+.zen-forms.light-theme .custom-select.is-open .selected {
+	background-color: #e5e5e5;
+}
diff --git a/skins/larry/zen-form.js b/skins/larry/zen-form.js
new file mode 100755
index 0000000..b75f15e
--- /dev/null
+++ b/skins/larry/zen-form.js
@@ -0,0 +1,392 @@
+/** Zen Forms 1.0.3 | MIT License | git.io/zen-form */
+
+(function ($) {
+
+    $.fn.zenForm = function (settings) {
+
+        settings = $.extend({
+            trigger: '.go-zen',
+            theme: 'dark'
+        }, settings);
+
+        /**
+         * Helper functions
+         */
+        var Utils = {
+
+            /**
+             * (Un)Wrap body content to hide overflow
+             */
+            bodyWrap: function () {
+
+                var $body = $('body'),
+                    $wrap = $body.children('.zen-forms-body-wrap');
+
+                if ($wrap.length) {
+                    $wrap.children().unwrap();
+                } else {
+                    $body.wrapInner('<div class="zen-forms-body-wrap"/>');
+                }
+
+            }, // bodyWrap
+
+            /**
+             * Watch inputs and add "empty" class if needed
+             */
+            watchEmpty: function () {
+
+                App.Environment.find('input, textarea, select').each(function () {
+
+                   $(this).on('change', function () {
+
+                        $(this)[$(this).val() ? 'removeClass' : 'addClass']('empty');
+
+                   }).trigger('change');
+
+                });
+
+            },
+
+            /**
+             * Custom styled selects
+             */
+            customSelect: function ($select, $customSelect) {
+
+                var $selected;
+
+                $customSelect.on('click', function (event) {
+
+                    event.stopPropagation();
+
+                    $selected = $customSelect.find('.selected');
+
+                    $customSelect.toggleClass('is-open');
+
+                    if ($customSelect.hasClass('is-open')) {
+                        $customSelect.scrollTop(
+                            $selected.position().top - $selected.outerHeight()
+                        );
+                    }
+
+
+                }).find('a').on('click', function () {
+
+                    $(this).addClass('selected').siblings().removeClass('selected');
+
+                    $select.val($(this).data('value'));
+
+                });
+
+            }, // customSelect
+
+            /**
+             * Hide any elements(mostly selects) when clicked outside them
+             */
+            manageSelects: function () {
+
+                $(document).on('click', function () {
+                    $('.is-open').removeClass('is-open');
+                });
+
+            }, // manageSelects
+
+            /**
+             * Hide any elements(mostly selects) when clicked outside them
+             */
+            focusFirst: function () {
+
+                var $first = App.Environment.find('input').first();
+
+                // we need to re-set value to remove focus selection
+                $first.focus().val($first.val());
+
+            } // focusFirst
+
+        }, // Utils
+
+        /**
+         * Core functionality
+         */
+        App = {
+
+            /**
+             * Orginal form element
+             */
+            Form: null,
+
+            /**
+             * Wrapper element
+             */
+            Environment: null,
+
+            /**
+             * Functions to create and manipulate environment
+             */
+            env: {
+
+
+                /**
+                 * Object where elements created with App.env.addObject are appended
+                 */
+                wrapper: null,
+
+                create: function () {
+
+                    // Callback: zf-initialize
+                    App.Form.trigger('zf-initialize');
+
+                    Utils.bodyWrap();
+
+                    App.Environment = $('<div>', {
+                        class: 'zen-forms' + (settings.theme == 'dark' ? '' : ' light-theme')
+                    }).hide().appendTo('body').fadeIn(200);
+
+                    // ESC to exit. Thanks @ktmud
+                    $('body').on('keydown', function (event) {
+
+                        if (event.which == 27)
+                            App.env.destroy($elements);
+
+                    });
+
+                    return App.Environment;
+
+                }, // create
+
+                /**
+                 * Update orginal inputs with new values and destroy Environment
+                 */
+                destroy: function ($elements) {
+
+                    // Callback: zf-destroy
+                    App.Form.trigger('zf-destroy', App.Environment);
+
+                    $('body').off('keydown');
+
+                    // Update orginal inputs with new values
+                    $elements.each(function (i) {
+
+                        var $el = $('#zen-forms-input' + i);
+
+                        if ($el.length) {
+                            $(this).val($el.val());
+                        }
+
+                    });
+
+                    Utils.bodyWrap();
+
+                    // Hide and remove Environment
+                    App.Environment.fadeOut(200, function () {
+
+                        App.env.wrapper = null;
+
+                        App.Environment.remove();
+
+                    });
+
+                    // Callback: zf-destroyed
+                    App.Form.trigger('zf-destroyed');
+
+                }, // destroy
+
+                /**
+                 * Append inputs, textareas to Environment
+                 */
+                add: function ($elements) {
+
+                    var $el, $label, value, id, ID, label;
+
+                    $elements.each(function (i) {
+
+                        App.env.wrapper = App.env.createObject('div', {
+                            class: 'zen-forms-input-wrap'
+                        }).appendTo(App.Environment);
+
+                        $el = $(this);
+
+                        value = $el.val();
+
+                        id = $el.attr('id');
+
+                        ID = 'zen-forms-input' + i;
+
+                        label = $el.data('label') || $("label[for=" + id + "]").text() || $el.attr('placeholder') || '';
+
+                        // Exclude specified elements
+                        if ($.inArray( $el.attr('type'), ['checkbox', 'radio', 'submit']) == -1) {
+
+                            if ($el.is('input') )
+                                App.env.addInput($el, ID, value);
+                            else if ($el.is('select') )
+                                App.env.addSelect($el, ID);
+                            else
+                                App.env.addTextarea($el, ID, value);
+
+                            $label = App.env.addObject('label', {
+                                for: ID,
+                                text: label
+                            });
+
+                            if ($el.is('select') )
+                                $label.prependTo(App.env.wrapper);
+
+                        }
+
+                    });
+
+                    // Callback: zf-initialized
+                    App.Form.trigger('zf-initialized', App.Environment);
+
+                }, // add
+
+                addInput: function ($input, ID, value) {
+
+                    return App.env.addObject('input', {
+                        id: ID,
+                        value: value,
+                        class: 'input',
+                        type: $input.attr('type')
+                    });
+
+                }, // addInput
+
+                addTextarea: function ($textarea, ID, value) {
+
+                    return App.env.addObject('textarea', {
+                        id: ID,
+                        text: value,
+                        rows: 5,
+                        class: 'input'
+                    });
+
+                }, // addTextarea
+
+                addSelect: function ($orginalSelect, ID) {
+
+                    var $select = App.env.addObject('select', {
+                            id: ID,
+                            class: 'select'
+                        }),
+                        $options = $orginalSelect.find('option'),
+                        $customSelect = App.env.addObject('div', {
+                            class: 'custom-select-wrap',
+                            html: '<div class="custom-select"></div>'
+                        }).children();
+
+                    $select.append($options.clone());
+
+                    $.each($options, function (i, option) {
+
+                        App.env.createObject('a', {
+                            href: '#',
+                            html: '<span>' + $(option).text() + '</span>' ,
+                            'data-value': $(option).attr('value'),
+                            class: $(option).prop('selected') ? 'selected' : ''
+                        }).appendTo($customSelect);
+
+                    });
+
+                    $select.val($orginalSelect.val());
+
+                    Utils.customSelect($select, $customSelect);
+
+                    return $customSelect;
+
+                }, // addSelect
+
+                /**
+                 * Wrapper for creating jQuery objects
+                 */
+                createObject: function (type, params, fn, fnMethod) {
+
+                    return $('<'+type+'>', params).on(fnMethod || 'click', fn);
+
+                }, // createObject
+
+                /**
+                 * Wrapper for adding jQuery objects to wrapper
+                 */
+                addObject: function (type, params, fn, fnMethod) {
+
+                    return App.env.createObject(type, params, fn, fnMethod).appendTo(App.env.wrapper || App.Environment);
+
+                }, // addObject
+
+                switchTheme: function () {
+
+                    App.Environment.toggleClass('light-theme');
+
+                } // switchTheme
+
+            }, // env
+
+            zen: function ($elements) {
+
+                // Create environment
+                App.env.create();
+
+                // Add wrapper div for close and theme buttons
+                App.env.wrapper = App.env.createObject('div', {
+                    class: 'zen-forms-header'
+                }).appendTo(App.Environment);
+
+                // Add close button
+                App.env.addObject('a', {
+                    class: 'zen-forms-close-button',
+                    html: '<i class="zen-icon zen-icon--close"></i> Exit Zen Mode'
+                }, function () {
+                    App.env.destroy($elements);
+                });
+
+                // Add theme switch button
+                App.env.addObject('a', {
+                    class: 'zen-forms-theme-switch',
+                    html: '<i class="zen-icon zen-icon--theme"></i> Switch theme'
+                }, function () {
+                    App.env.switchTheme();
+                });
+
+                // Add inputs and textareas from form
+                App.env.add($elements);
+
+                // Additional select functionality
+                Utils.manageSelects();
+
+                // Select first input
+                Utils.focusFirst();
+
+                // Add .empty class for empty inputs
+                Utils.watchEmpty();
+
+            } // zen
+
+        }; // App
+
+        App.Form = $(this);
+
+        var $elements = App.Form.is('form') ? App.Form.find('input, textarea, select') : App.Form;
+
+        $(settings.trigger).on('click', function (event) {
+
+            event.preventDefault();
+
+            App.zen($elements);
+
+        });
+
+        // Command: destroy
+        App.Form.on('destroy', function () {
+            App.env.destroy($elements);
+        });
+
+        // Command: init
+        App.Form.on('init', function () {
+            App.zen($elements);
+        });
+
+        return this;
+
+    };
+
+})(jQuery);

--
Gitblit v1.9.1