free time search options dialog
authorMichael Spahn <m.spahn@metaways.de>
Tue, 20 Jun 2017 15:32:56 +0000 (17:32 +0200)
committerCornelius Weiss <c.weiss@metaways.de>
Mon, 26 Jun 2017 11:09:57 +0000 (13:09 +0200)
Change-Id: Ieaa02c9ffcc09d5579f3b927a3c1d1c57a7111aa
Reviewed-on: http://gerrit.tine20.com/customers/4915
Reviewed-by: Cornelius Weiss <c.weiss@metaways.de>
Tested-by: Cornelius Weiss <c.weiss@metaways.de>
tine20/Calendar/Calendar.jsb2
tine20/Calendar/js/EventFinderOptionsDialog.js [new file with mode: 0644]
tine20/Tinebase/Tinebase.jsb2
tine20/Tinebase/css/Tinebase.css
tine20/Tinebase/js/RangeSliderComponent.js [new file with mode: 0644]

index f71ee63..e608e52 100644 (file)
         {
           "text": "RruleFilter.js",
           "path": "js/"
+        },
+        {
+          "text": "EventFinderOptionsDialog.js",
+          "path": "js/"
         }
       ]
     },
diff --git a/tine20/Calendar/js/EventFinderOptionsDialog.js b/tine20/Calendar/js/EventFinderOptionsDialog.js
new file mode 100644 (file)
index 0000000..c39f54e
--- /dev/null
@@ -0,0 +1,276 @@
+/*
+ * Tine 2.0
+ *
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Michael Spahn <m.spahn@metaways.de>
+ * @copyright   Copyright (c) 2017 Metaways Infosystems GmbH (http://www.metaways.de)
+ */
+Ext.ns('Tine.Calendar');
+
+/**
+ * File picker dialog
+ *
+ * @namespace   Tine.Calendar
+ * @class       Tine.Calendar.EventFinderOptionsDialog
+ * @extends     Ext.FormPanel
+ * @constructor
+ * @param       {Object} config The configuration options.
+ */
+Tine.Calendar.EventFinderOptionsDialog = Ext.extend(Ext.Panel, {
+    defaultOptions: [
+        {id: 'monday', active: true, config: [8, 18.00]},
+        {id: 'tuesday', active: true, config: [8, 18.00]},
+        {id: 'wednesday', active: true, config: [8, 18.00]},
+        {id: 'thursday', active: true, config: [8, 18.00]},
+        {id: 'friday', active: true, config: [8, 18.00]},
+        {id: 'monday', active: false, config: [8, 18.00]},
+        {id: 'monday', active: false, config: [8, 18.00]}
+    ],
+
+    windowNamePrefix: 'eventfinderoptionsdialog_',
+
+    app: null,
+    cls: 'tw-editdialog',
+    header: false,
+    border: false,
+
+    layout: 'fit',
+
+    wkdays: ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
+
+    mondaySlider: null,
+    tuesdaySlider: null,
+    wednesdaySlider: null,
+    thursdaySlider: null,
+    fridaySlider: null,
+    saturdaySlider: null,
+    sundaySlider: null,
+
+    mondayCheckbox: null,
+    tuesdayCheckbox: null,
+    wednesdayCheckbox: null,
+    thursdayCheckbox: null,
+    fridayCheckbox: null,
+    saturdayCheckbox: null,
+    sundayCheckbox: null,
+
+    stateId: 'eventFinderOptions',
+    stateConfig: [],
+
+    initComponent: function () {
+        if (null === this.app) {
+            this.app = Tine.Tinebase.appMgr.get('Calendar');
+        }
+
+        this.title = this.titleText ? this.titleText : this.app.i18n._('Event finder options');
+        this.window.setTitle(this.title);
+
+        this.initActions();
+        this.initButtons();
+
+        this.stateConfig = Ext.state.Manager.get(this.stateId, this.defaultOptions);
+
+        this.items = this.getFormItems();
+
+        this.supr().initComponent.apply(this, arguments);
+    },
+
+    initButtons: function () {
+        this.fbar = [
+            '->'
+        ];
+
+        this.fbar.push(this.action_cancel, this.action_saveAndClose);
+    },
+
+    initActions: function () {
+        this.action_saveAndClose = new Ext.Action({
+            text: this.app.i18n._('Ok'),
+            minWidth: 70,
+            ref: '../btnSaveAndClose',
+            scope: this,
+            handler: function () {
+                this.onSaveAndClose();
+            },
+            iconCls: 'action_saveAndClose'
+        });
+
+        this.action_cancel = new Ext.Action({
+            text: this.app.i18n._('Cancel'),
+            minWidth: 70,
+            scope: this,
+            handler: this.onCancel,
+            iconCls: 'action_cancel'
+        });
+
+        this.actionUpdater = new Tine.widgets.ActionUpdater({
+            containerProperty: this.recordClass ? this.recordClass.getMeta('containerProperty') : null,
+            evalGrants: this.evalGrants
+        });
+
+        this.actionUpdater.addActions([
+            this.action_saveAndClose,
+            this.action_cancel
+        ]);
+    },
+
+    onCancel: function () {
+        this.window.close();
+    },
+
+    onSaveAndClose: function () {
+        var data = [{
+            id: 'monday',
+            active: this.mondayCheckbox.getValue(),
+            config: this.mondaySlider.getRange()
+        }, {
+            id: 'tuesday',
+            active: this.tuesdayCheckbox.getValue(),
+            config: this.tuesdaySlider.getRange()
+        }, {
+            id: 'wednesday',
+            active: this.wednesdayCheckbox.getValue(),
+            config: this.wednesdaySlider.getRange()
+        }, {
+            id: 'thursday',
+            active: this.thursdayCheckbox.getValue(),
+            config: this.thursdaySlider.getRange()
+        }, {
+            id: 'friday',
+            active: this.fridayCheckbox.getValue(),
+            config: this.fridaySlider.getRange()
+        }, {
+            id: 'saturday',
+            active: this.saturdayCheckbox.getValue(),
+            config: this.saturdaySlider.getRange()
+        }, {
+            id: 'sunday',
+            active: this.sundayCheckbox.getValue(),
+            config: this.sundaySlider.getRange()
+        }];
+
+        Ext.state.Manager.set(this.stateId, data);
+        this.fireEvent('apply', this, data);
+        this.window.close();
+    },
+
+    onCheckSlider: function (cb, checked) {
+        this[cb.name + 'Slider'].setDisabled(!checked);
+    },
+
+    getCheckboxSliderRowFor: function (id) {
+        var _ = window.lodash;
+
+        var config = _.find(this.stateConfig, function (o) {
+            return o.id === id;
+        });
+
+        return {
+            layout: 'hbox',
+            layoutConfig: {
+                align: 'stretch',
+                pack: 'start'
+            },
+            height: 30,
+            items: [
+                {
+                    xtype: 'checkbox',
+                    checked: !!config && config.active,
+                    boxLabel: _.capitalize(id),
+                    name: id,
+                    anchor: '95%',
+                    flex: 1,
+                    ref: '../../../../../' + id + 'Checkbox',
+                    listeners: {scope: this, check: this.onCheckSlider}
+                }, this.getSliderFor(id, config)
+            ]
+        };
+    },
+
+    getSliderFor: function (id, config) {
+        var _ = window.lodash;
+        var sliderId = id + 'Slider';
+
+        var sliderStart = 0;
+        var sliderEnd = 23.9999;
+
+        if (config) {
+            sliderStart = config.config[0];
+            sliderEnd = config.config[1];
+        }
+
+        return new Tine.Tinebase.RangeSliderComponent({
+            width: 500,
+            ref: '../../../../../' + sliderId,
+            currentStart: sliderStart,
+            currentEnd: sliderEnd,
+            disabled: !config || !config.active
+        });
+    },
+
+    /**
+     * @todo: improve layout, rangeslidercomponent can't deal with resizing! Techically it can but it's extremely slow atm!
+     */
+    getFormItems: function () {
+        var wkdayItems = [];
+        for (var i=0,d; i<7; i++) {
+            d = (i+Ext.DatePicker.prototype.startDay)%7
+            wkdayItems.push(this.getCheckboxSliderRowFor(this.wkdays[d]));
+        }
+
+        var sliderElements = [{
+            layout: 'vbox',
+            border: false,
+
+            layoutConfig: {
+                align: 'stretch',
+                pack: 'start'
+            },
+
+            items: wkdayItems
+        }];
+
+        var items = {
+            xtype: 'tabpanel',
+            border: false,
+            plain: true,
+            defaults: {
+                hideMode: 'offsets'
+            },
+            plugins: [{
+                ptype: 'ux.tabpanelkeyplugin'
+            }],
+            activeTab: 0,
+            border: false,
+            items: [{
+                title: this.title,
+                autoScroll: true,
+                border: false,
+                frame: true,
+                layout: 'border',
+                items: [{
+                    region: 'center',
+                    layout: 'fit',
+                    border: false,
+                    items: sliderElements
+                }]
+            }]
+        };
+
+        return items;
+    }
+});
+
+/**
+ * Create new EventFinderOptionsDialog window
+ */
+Tine.Calendar.EventFinderOptionsDialog.openWindow = function (config) {
+    var window = Tine.WindowFactory.getWindow({
+        width: 800,
+        height: 300,
+        name: Tine.Calendar.EventFinderOptionsDialog.prototype.windowNamePrefix + config.recordId,
+        contentPanelConstructor: 'Tine.Calendar.EventFinderOptionsDialog',
+        contentPanelConstructorConfig: config
+    });
+    return window;
+};
index e1db9a1..ec40cab 100644 (file)
           "path": "js/"
         },
         {
+          "text": "RangeSliderComponent.js",
+          "path": "js/"
+        },
+        {
           "text": "tineInit.js",
           "path": "js/"
         }
index f8fed64..3ccf50c 100644 (file)
@@ -1092,4 +1092,28 @@ textarea.x-form-field.tinebase-editmultipledialog-noneedit {
 
 .x-form-radio-group .x-form-item {
     padding-left: 2px;
+}
+
+.rangeslider-component-sliderwrap {
+    border: 1px solid black;
+}
+
+.rangeslider-component-sliderBorder {
+    border: 1px solid black;
+    border-radius: 3px;
+    background-color: #cdcdcd;
+}
+
+.rangeslider-component .rangeslider-component-slider .rangeslider-component-slider-label {
+    display: table-cell;
+    text-align: center;
+    vertical-align: middle;
+    white-space: nowrap;
+    padding-left: 10px;
+    padding-right: 10px;
+}
+
+.rangeslider-component .rangeslider-component-slider {
+    background-color: #b7f8bd;
+    overflow: hidden;
 }
\ No newline at end of file
diff --git a/tine20/Tinebase/js/RangeSliderComponent.js b/tine20/Tinebase/js/RangeSliderComponent.js
new file mode 100644 (file)
index 0000000..38d185f
--- /dev/null
@@ -0,0 +1,235 @@
+/*
+ * Tine 2.0
+ *
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Michael Spahn <m.spahn@metaways.de>
+ * @copyright   Copyright (c) 2017 Metaways Infosystems GmbH (http://www.metaways.de)
+ */
+Ext.ns('Tine.Tinebase');
+
+/**
+ * File picker dialog
+ *
+ * @namespace   Tine.Tinebase
+ * @class       Tine.Tinebase.RangeSliderComponent
+ * @extends     Ext.Component
+ * @constructor
+ * @param       {Object} config The configuration options.
+ */
+Tine.Tinebase.RangeSliderComponent = Ext.extend(Ext.BoxComponent, {
+    minWidth: 100,
+    height: 25,
+
+    slider: null,
+    sliderResizer: null,
+    sliderLabel: null,
+    sliderBorder: null,
+
+    maxRange: 23.99,
+
+    currentStart: 0,
+    currentEnd: 23.99,
+
+    cls: 'rangeslider-component',
+
+    name: null,
+
+    /**
+     * @private
+     */
+    initComponent: function () {
+        this.supr().initComponent.apply(this, arguments);
+        this.on('afterrender', this.onAfterRender);
+    },
+
+    /**
+     * We have an element called sliderBorder which can be styled.
+     *
+     * The sliderWrap is used to calculate positions of the slider within this element, therefor you shouldn't apply
+     * box model affecting styles to the slider itself or the the sliderWrap.
+     *
+     * The slider is a simple div which is managed by Ext.Resizable and contains a span which can be used as a label.
+     *
+     * @todo: maybe this can be done better
+     *
+     * @private
+     */
+    initSlider: function () {
+        this.sliderWrap = new Ext.Element(document.createElement('div'));
+
+        this.sliderBorder = new Ext.Element(document.createElement('div'));
+        this.sliderBorder.addClass('rangeslider-component-sliderBorder');
+
+        this.slider = new Ext.Element(document.createElement('div'));
+        this.slider.addClass('rangeslider-component-slider');
+
+        this.sliderLabel = new Ext.Element(document.createElement('span'));
+        this.sliderLabel.setHeight(this.height);
+        this.sliderLabel.addClass('rangeslider-component-slider-label');
+
+        this.slider.appendChild(this.sliderLabel);
+        this.sliderWrap.appendChild(this.slider);
+
+        this.sliderBorder.appendChild(this.sliderWrap);
+
+        this.getEl().appendChild(this.sliderBorder);
+
+        this.sliderResizer = new Ext.Resizable(this.slider, {
+            handles: 'e w',
+            pinned: true,
+            dynamic: true
+        });
+    },
+
+    /**
+     * @private
+     */
+    onAfterRender: function () {
+        this.initSlider();
+        this.setSliderWidth(this.currentStart, this.currentEnd);
+    },
+
+    /**
+     * @private
+     *
+     * In case you won't use this class for a hour, min slider, you should override this function and update the label yourself
+     */
+    updateLabel: function () {
+        this.sliderLabel.setWidth(this.sliderResizer.getEl().getWidth());
+
+        if (this.sliderLabel.getWidth() > this.slider.getWidth()) {
+            this.sliderLabel.hide();
+        } else {
+            this.sliderLabel.show();
+        }
+
+        var hoursStart = this.trunc(this.currentStart);
+        var minStart = this.trunc(Math.round((this.currentStart % 1) * 100) * 0.60);
+
+        var hoursEnd = this.trunc(this.currentEnd);
+        var minEnd = this.trunc((this.currentEnd % 1) * 100 * 0.60);
+
+        var startDate = new Date();
+        startDate.setHours(hoursStart);
+        startDate.setMinutes(minStart);
+
+        var endDate = new Date();
+        endDate.setHours(hoursEnd);
+        endDate.setMinutes(minEnd);
+
+        var pattern = 'H:i';
+
+        this.sliderLabel.update(startDate.format(pattern) + ' - ' + endDate.format(pattern));
+    },
+
+    /**
+     * @private
+     * @param x
+     * @return {*}
+     */
+    trunc: function (x) {
+        if (isNaN(x)) {
+            return NaN;
+        }
+        if (x > 0) {
+            return Math.floor(x);
+        }
+        return Math.ceil(x);
+    },
+
+    /**
+     * @private
+     */
+    getCropFactor: function () {
+        return this.sliderWrap.getWidth(true) / this.maxRange;
+    },
+
+    /**
+     * @private
+     */
+    onSliderResize: function () {
+        var el = this.sliderResizer.getEl();
+        var offsetLeft = el.getOffsetsTo(this.sliderWrap)[0];
+        var offsetWidth = el.dom.offsetWidth;
+        var cropFactor = this.getCropFactor();
+
+        this.currentStart = offsetLeft / cropFactor;
+        this.currentEnd = (offsetLeft + offsetWidth) / cropFactor;
+
+        var invalidState = false;
+
+        if (this.currentEnd > this.maxRange) {
+            this.currentEnd = this.maxRange;
+
+            invalidState = true;
+        }
+
+        if (this.currentStart < 0) {
+            this.currentStart = 0;
+
+            invalidState = true;
+        }
+
+        if (true === invalidState) {
+            this.setSliderWidth(this.currentStart, this.currentEnd);
+        }
+
+        this.updateLabel();
+    },
+
+    /**
+     *  @private
+     */
+    resizeElement: function () {
+        var box = Ext.Resizable.prototype.resizeElement.apply(this.sliderResizer);
+
+        this.onSliderResize();
+
+        return box;
+    },
+
+    /**
+     * Set the slider width
+     *
+     * @param start 0-23.99
+     * @param end 0-23.99
+     */
+    setSliderWidth: function (start, end) {
+        // End is not allowed to be smaller
+        if (end < start) {
+            return;
+        }
+
+        this.currentStart = start;
+        this.currentEnd = end;
+
+        var cropFactor = this.getCropFactor();
+        var leftOffset = start * cropFactor;
+        var width = (end * cropFactor) - (start * cropFactor);
+
+        this.slider.alignTo(this.sliderWrap, 'tl-tl', [leftOffset, 0]);
+
+        this.sliderResizer.resizeElement = this.resizeElement.createDelegate(this);
+        this.sliderResizer.on('resize', this.onSliderResize.createDelegate(this));
+
+        this.sliderResizer.resizeTo(width, this.sliderResizer.getEl().getHeight());
+    },
+
+    /**
+     * Returns current span
+     */
+    getRange: function () {
+        return [this.currentStart, this.currentEnd];
+    },
+
+    /**
+     * Set slider to a certain position by entering an array with start and end
+     */
+    setRange: function (range) {
+        if (!Ext.isArray(range) || range.length !== 2) {
+            return;
+        }
+
+        this.setSliderWidth(range[0], range[1]);
+    }
+});
\ No newline at end of file