Thomas Bruederli
2014-08-14 29f7b272a54ccb268148c48c2eefb00748dc2512
Add Zen mode to message compose body (#1489198) using the Idered/zen-form jQuery plugin
3 files added
5 files modified
782 ■■■■■ changed files
program/localization/en_US/labels.inc 5 ●●●●● patch | view | raw | blame | history
skins/larry/images/zen-form-sprites.png patch | view | raw | blame | history
skins/larry/includes/footer.html 2 ●●●●● patch | view | raw | blame | history
skins/larry/mail.css 15 ●●●●● patch | view | raw | blame | history
skins/larry/templates/compose.html 4 ●●●● patch | view | raw | blame | history
skins/larry/ui.js 24 ●●●●● patch | view | raw | blame | history
skins/larry/zen-form.css 340 ●●●●● patch | view | raw | blame | history
skins/larry/zen-form.js 392 ●●●●● patch | view | raw | blame | history
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)';
skins/larry/images/zen-form-sprites.png
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();
});
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;
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>
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')]();
  }
skins/larry/zen-form.css
New file
@@ -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;
}
skins/larry/zen-form.js
New file
@@ -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);