From e9950defb240dfe19a22cf1cce34e0fd25e58ee9 Mon Sep 17 00:00:00 2001 From: Bart Visscher Date: Thu, 22 Sep 2011 22:22:52 +0200 Subject: [PATCH] Make event category choice multiselect --- apps/calendar/ajax/editeventform.php | 16 +- apps/calendar/ajax/neweventform.php | 8 +- apps/calendar/css/style.css | 2 + apps/calendar/index.php | 12 +- apps/calendar/js/calendar.js | 7 + apps/calendar/lib/object.php | 18 +- apps/calendar/templates/part.eventform.php | 8 +- core/css/jquery.multiselect.css | 23 + core/js/jquery.multiselect.js | 658 +++++++++++++++++++++ lib/base.php | 2 +- 10 files changed, 730 insertions(+), 24 deletions(-) create mode 100644 core/css/jquery.multiselect.css create mode 100644 core/js/jquery.multiselect.js diff --git a/apps/calendar/ajax/editeventform.php b/apps/calendar/ajax/editeventform.php index 3b8a30f70f..f310db3e79 100644 --- a/apps/calendar/ajax/editeventform.php +++ b/apps/calendar/ajax/editeventform.php @@ -20,8 +20,8 @@ if(!OC_USER::isLoggedIn()) { die(''); } -$calendars = OC_Calendar_Calendar::allCalendars(OC_User::getUser()); -$categories = OC_Calendar_Object::getCategoryOptions($l10n); +$calendar_options = OC_Calendar_Calendar::allCalendars(OC_User::getUser()); +$category_options = OC_Calendar_Object::getCategoryOptions($l10n); $repeat_options = OC_Calendar_Object::getRepeatOptions($l10n); $id = $_GET['id']; @@ -55,19 +55,23 @@ switch($dtstart->getDateType()) { $summary = isset($vevent->SUMMARY) ? $vevent->SUMMARY->value : ''; $location = isset($vevent->LOCATION) ? $vevent->LOCATION->value : ''; -$category = isset($vevent->CATEGORIES) ? $vevent->CATEGORIES->value : ''; +$categories = array(); +if (isset($vevent->CATEGORIES)){ + $categories = explode(',', $vevent->CATEGORIES->value); + $categories = array_map('trim', $categories); +} $repeat = isset($vevent->CATEGORY) ? $vevent->CATEGORY->value : ''; $description = isset($vevent->DESCRIPTION) ? $vevent->DESCRIPTION->value : ''; $tmpl = new OC_Template('calendar', 'part.editevent'); $tmpl->assign('id', $id); -$tmpl->assign('calendars', $calendars); -$tmpl->assign('categories', $categories); +$tmpl->assign('calendar_options', $calendar_options); +$tmpl->assign('category_options', $category_options); $tmpl->assign('repeat_options', $repeat_options); $tmpl->assign('title', $summary); $tmpl->assign('location', $location); -$tmpl->assign('category', $category); +$tmpl->assign('categories', $categories); $tmpl->assign('calendar', $data['calendarid']); $tmpl->assign('allday', $allday); $tmpl->assign('startdate', $startdate); diff --git a/apps/calendar/ajax/neweventform.php b/apps/calendar/ajax/neweventform.php index 1d8a61dffd..132294b496 100644 --- a/apps/calendar/ajax/neweventform.php +++ b/apps/calendar/ajax/neweventform.php @@ -25,8 +25,8 @@ if(!OC_USER::isLoggedIn()) { die(''); } -$calendars = OC_Calendar_Calendar::allCalendars(OC_User::getUser()); -$categories = OC_Calendar_Object::getCategoryOptions($l10n); +$calendar_options = OC_Calendar_Calendar::allCalendars(OC_User::getUser()); +$category_options = OC_Calendar_Object::getCategoryOptions($l10n); $repeat_options = OC_Calendar_Object::getRepeatOptions($l10n); $startday = substr($_GET['d'], 0, 2); $startmonth = substr($_GET['d'], 2, 2); @@ -56,8 +56,8 @@ if($endtime == 23) { } $tmpl = new OC_Template('calendar', 'part.newevent'); -$tmpl->assign('calendars', $calendars); -$tmpl->assign('categories', $categories); +$tmpl->assign('calendar_options', $calendar_options); +$tmpl->assign('category_options', $category_options); $tmpl->assign('startdate', $startday . '-' . $startmonth . '-' . $startyear); $tmpl->assign('starttime', ($starttime <= 9 ? '0' : '') . $starttime . ':' . $startminutes); $tmpl->assign('enddate', $endday . '-' . $endmonth . '-' . $endyear); diff --git a/apps/calendar/css/style.css b/apps/calendar/css/style.css index 5d08402422..b119806276 100644 --- a/apps/calendar/css/style.css +++ b/apps/calendar/css/style.css @@ -68,3 +68,5 @@ color:#333; #fromtime.disabled, #totime.disabled { color:#A9A9A9; } +select#category{width:140px;} +button.category{margin:0 3px;} diff --git a/apps/calendar/index.php b/apps/calendar/index.php index c93ea93bb6..b4e7d5ff48 100644 --- a/apps/calendar/index.php +++ b/apps/calendar/index.php @@ -17,7 +17,7 @@ * 59 Temple Place, Suite 330, Boston, * * MA 02111-1307 USA * *************************************************/ -require_once ("../../lib/base.php"); +require_once ('../../lib/base.php'); OC_Util::checkLoggedIn(); // Create default calendar ... $calendars = OC_Calendar_Calendar::allCalendars(OC_User::getUser()); @@ -25,8 +25,10 @@ if( count($calendars) == 0){ OC_Calendar_Calendar::addCalendar(OC_User::getUser(),'default','Default calendar'); $calendars = OC_Calendar_Calendar::allCalendars(OC_User::getUser()); } -OC_UTIL::addScript("calendar", "calendar"); -OC_UTIL::addStyle("calendar", "style"); -OC_APP::setActiveNavigationEntry("calendar_index"); -$output = new OC_TEMPLATE("calendar", "calendar", "user"); +OC_UTIL::addScript('calendar', 'calendar'); +OC_UTIL::addStyle('calendar', 'style'); +OC_UTIL::addScript('', 'jquery.multiselect'); +OC_UTIL::addStyle('', 'jquery.multiselect'); +OC_APP::setActiveNavigationEntry('calendar_index'); +$output = new OC_TEMPLATE('calendar', 'calendar', 'user'); $output -> printPage(); diff --git a/apps/calendar/js/calendar.js b/apps/calendar/js/calendar.js index 5864977eb3..8c1a4afac0 100644 --- a/apps/calendar/js/calendar.js +++ b/apps/calendar/js/calendar.js @@ -288,6 +288,13 @@ Calendar={ $( "#to" ).datepicker({ dateFormat : 'dd-mm-yy' }); + $('#category').multiselect({ + header: false, + noneSelectedText: $('#category').attr('title'), + selectedList: 2, + minWidth:'auto', + classes: 'category', + }); $('#event').dialog({ width : 500, close : function(event, ui) { diff --git a/apps/calendar/lib/object.php b/apps/calendar/lib/object.php index 702033c169..3bb6543d3f 100644 --- a/apps/calendar/lib/object.php +++ b/apps/calendar/lib/object.php @@ -301,7 +301,6 @@ class OC_Calendar_Object{ public static function getCategoryOptions($l10n) { return array( - $l10n->t('None'), $l10n->t('Birthday'), $l10n->t('Business'), $l10n->t('Call'), @@ -345,6 +344,11 @@ class OC_Calendar_Object{ $errarr['cal'] = 'true'; $errnum++; } + + if(isset($request['categories']) && !is_array($request['categories'])){ + $errors['categories'] = $l10n->t('Not an array'); + } + $fromday = substr($request['from'], 0, 2); $frommonth = substr($request['from'], 3, 2); $fromyear = substr($request['from'], 6, 4); @@ -434,7 +438,7 @@ class OC_Calendar_Object{ { $title = $request["title"]; $location = $request["location"]; - $cat = $request["category"]; + $categories = $request["categories"]; $allday = isset($request["allday"]); $from = $request["from"]; $fromtime = $request["fromtime"]; @@ -493,15 +497,21 @@ class OC_Calendar_Object{ if($location != ""){ $vevent->LOCATION = $location; + }else{ + unset($vevent->LOCATION); } if($description != ""){ $des = str_replace("\n","\\n", $description); $vevent->DESCRIPTION = $des; + }else{ + unset($vevent->DESCRIPTION); } - if($cat != ""){ - $vevent->CATEGORIES = $cat; + if(!empty($categories)){ + $vevent->CATEGORIES = join(',', $categories); + }else{ + unset($vevent->CATEGORIES); } /*if($repeat == "true"){ diff --git a/apps/calendar/templates/part.eventform.php b/apps/calendar/templates/part.eventform.php index c63d709e26..5b12407330 100644 --- a/apps/calendar/templates/part.eventform.php +++ b/apps/calendar/templates/part.eventform.php @@ -16,10 +16,10 @@ t("Category");?>: - "> ' . $category . ''; + foreach($_['category_options'] as $category){ + echo ''; } ?> @@ -27,7 +27,7 @@ ' + title + ''); + }); + + // insert into the DOM + checkboxContainer.html( html.join('') ); + + // cache some moar useful elements + this.labels = menu.find('label'); + + // set widths + this._setButtonWidth(); + this._setMenuWidth(); + + // remember default value + this.button[0].defaultValue = this.update(); + + // broadcast refresh event; useful for widgets + if( !init ){ + this._trigger('refresh'); + } + }, + + // updates the button text. call refresh() to rebuild + update: function(){ + var o = this.options, + $inputs = this.labels.find('input'), + $checked = $inputs.filter(':checked'), + numChecked = $checked.length, + value; + + if( numChecked === 0 ){ + value = o.noneSelectedText; + } else { + if($.isFunction( o.selectedText )){ + value = o.selectedText.call(this, numChecked, $inputs.length, $checked.get()); + } else if( /\d/.test(o.selectedList) && o.selectedList > 0 && numChecked <= o.selectedList){ + value = $checked.map(function(){ return this.title; }).get().join(', '); + } else { + value = o.selectedText.replace('#', numChecked).replace('#', $inputs.length); + } + } + + this.buttonlabel.html( value ); + return value; + }, + + // binds events + _bindEvents: function(){ + var self = this, button = this.button; + + function clickHandler(){ + self[ self._isOpen ? 'close' : 'open' ](); + return false; + } + + // webkit doesn't like it when you click on the span :( + button + .find('span') + .bind('click.multiselect', clickHandler); + + // button events + button.bind({ + click: clickHandler, + keypress: function( e ){ + switch(e.which){ + case 27: // esc + case 38: // up + case 37: // left + self.close(); + break; + case 39: // right + case 40: // down + self.open(); + break; + } + }, + mouseenter: function(){ + if( !button.hasClass('ui-state-disabled') ){ + $(this).addClass('ui-state-hover'); + } + }, + mouseleave: function(){ + $(this).removeClass('ui-state-hover'); + }, + focus: function(){ + if( !button.hasClass('ui-state-disabled') ){ + $(this).addClass('ui-state-focus'); + } + }, + blur: function(){ + $(this).removeClass('ui-state-focus'); + } + }); + + // header links + this.header + .delegate('a', 'click.multiselect', function( e ){ + // close link + if( $(this).hasClass('ui-multiselect-close') ){ + self.close(); + + // check all / uncheck all + } else { + self[ $(this).hasClass('ui-multiselect-all') ? 'checkAll' : 'uncheckAll' ](); + } + + e.preventDefault(); + }); + + // optgroup label toggle support + this.menu + .delegate('li.ui-multiselect-optgroup-label a', 'click.multiselect', function( e ){ + e.preventDefault(); + + var $this = $(this), + $inputs = $this.parent().nextUntil('li.ui-multiselect-optgroup-label').find('input:visible:not(:disabled)'), + nodes = $inputs.get(), + label = $this.parent().text(); + + // trigger event and bail if the return is false + if( self._trigger('beforeoptgrouptoggle', e, { inputs:nodes, label:label }) === false ){ + return; + } + + // toggle inputs + self._toggleChecked( + $inputs.filter(':checked').length !== $inputs.length, + $inputs + ); + + self._trigger('optgrouptoggle', e, { + inputs: nodes, + label: label, + checked: nodes[0].checked + }); + }) + .delegate('label', 'mouseenter.multiselect', function(){ + if( !$(this).hasClass('ui-state-disabled') ){ + self.labels.removeClass('ui-state-hover'); + $(this).addClass('ui-state-hover').find('input').focus(); + } + }) + .delegate('label', 'keydown.multiselect', function( e ){ + e.preventDefault(); + + switch(e.which){ + case 9: // tab + case 27: // esc + self.close(); + break; + case 38: // up + case 40: // down + case 37: // left + case 39: // right + self._traverse(e.which, this); + break; + case 13: // enter + $(this).find('input')[0].click(); + break; + } + }) + .delegate('input[type="checkbox"], input[type="radio"]', 'click.multiselect', function( e ){ + var $this = $(this), + val = this.value, + checked = this.checked, + tags = self.element.find('option'); + + // bail if this input is disabled or the event is cancelled + if( this.disabled || self._trigger('click', e, { value:val, text:this.title, checked:checked }) === false ){ + e.preventDefault(); + return; + } + + // toggle aria state + $this.attr('aria-selected', checked); + + // change state on the original option tags + tags.each(function(){ + if( this.value === val ){ + this.selected = checked; + } else if( !self.options.multiple ){ + this.selected = false; + } + }); + + // some additional single select-specific logic + if( !self.options.multiple ){ + self.labels.removeClass('ui-state-active'); + $this.closest('label').toggleClass('ui-state-active', checked ); + + // close menu + self.close(); + } + + // fire change on the select box + self.element.trigger("change"); + + // setTimeout is to fix multiselect issue #14 and #47. caused by jQuery issue #3827 + // http://bugs.jquery.com/ticket/3827 + setTimeout($.proxy(self.update, self), 10); + }); + + // close each widget when clicking on any other element/anywhere else on the page + $(document).bind('mousedown.multiselect', function( e ){ + if(self._isOpen && !$.contains(self.menu[0], e.target) && !$.contains(self.button[0], e.target) && e.target !== self.button[0]){ + self.close(); + } + }); + + // deal with form resets. the problem here is that buttons aren't + // restored to their defaultValue prop on form reset, and the reset + // handler fires before the form is actually reset. delaying it a bit + // gives the form inputs time to clear. + $(this.element[0].form).bind('reset.multiselect', function(){ + setTimeout(function(){ self.update(); }, 10); + }); + }, + + // set button width + _setButtonWidth: function(){ + var width = this.element.outerWidth(), + o = this.options; + + if( /\d/.test(o.minWidth) && width < o.minWidth){ + width = o.minWidth; + } + + // set widths + this.button.width( width ); + }, + + // set menu width + _setMenuWidth: function(){ + var m = this.menu, + width = this.button.outerWidth()- + parseInt(m.css('padding-left'),10)- + parseInt(m.css('padding-right'),10)- + parseInt(m.css('border-right-width'),10)- + parseInt(m.css('border-left-width'),10); + + m.width( width || this.button.outerWidth() ); + }, + + // move up or down within the menu + _traverse: function( which, start ){ + var $start = $(start), + moveToLast = which === 38 || which === 37, + + // select the first li that isn't an optgroup label / disabled + $next = $start.parent()[moveToLast ? 'prevAll' : 'nextAll']('li:not(.ui-multiselect-disabled, .ui-multiselect-optgroup-label)')[ moveToLast ? 'last' : 'first'](); + + // if at the first/last element + if( !$next.length ){ + var $container = this.menu.find('ul:last'); + + // move to the first/last + this.menu.find('label')[ moveToLast ? 'last' : 'first' ]().trigger('mouseover'); + + // set scroll position + $container.scrollTop( moveToLast ? $container.height() : 0 ); + + } else { + $next.find('label').trigger('mouseover'); + } + }, + + // This is an internal function to toggle the checked property and + // other related attributes of a checkbox. + // + // The context of this function should be a checkbox; do not proxy it. + _toggleCheckbox: function( prop, flag ){ + return function(){ + !this.disabled && (this[ prop ] = flag); + + if( flag ){ + this.setAttribute('aria-selected', true); + } else { + this.removeAttribute('aria-selected'); + } + } + }, + + _toggleChecked: function( flag, group ){ + var $inputs = (group && group.length) ? + group : + this.labels.find('input'), + + self = this; + + // toggle state on inputs + $inputs.each(this._toggleCheckbox('checked', flag)); + + // update button text + this.update(); + + // gather an array of the values that actually changed + var values = $inputs.map(function(){ + return this.value; + }).get(); + + // toggle state on original option tags + this.element + .find('option') + .each(function(){ + if( !this.disabled && $.inArray(this.value, values) > -1 ){ + self._toggleCheckbox('selected', flag).call( this ); + } + }); + + // trigger the change event on the select + if( $inputs.length ) { + this.element.trigger("change"); + } + }, + + _toggleDisabled: function( flag ){ + this.button + .attr({ 'disabled':flag, 'aria-disabled':flag })[ flag ? 'addClass' : 'removeClass' ]('ui-state-disabled'); + + this.menu + .find('input') + .attr({ 'disabled':flag, 'aria-disabled':flag }) + .parent()[ flag ? 'addClass' : 'removeClass' ]('ui-state-disabled'); + + this.element + .attr({ 'disabled':flag, 'aria-disabled':flag }); + }, + + // open the menu + open: function( e ){ + var self = this, + button = this.button, + menu = this.menu, + speed = this.speed, + o = this.options; + + // bail if the multiselectopen event returns false, this widget is disabled, or is already open + if( this._trigger('beforeopen') === false || button.hasClass('ui-state-disabled') || this._isOpen ){ + return; + } + + var $container = menu.find('ul:last'), + effect = o.show, + pos = button.position(); + + // figure out opening effects/speeds + if( $.isArray(o.show) ){ + effect = o.show[0]; + speed = o.show[1] || self.speed; + } + + // set the scroll of the checkbox container + $container.scrollTop(0).height(o.height); + + // position and show menu + if( $.ui.position && !$.isEmptyObject(o.position) ){ + o.position.of = o.position.of || button; + + menu + .show() + .position( o.position ) + .hide() + .show( effect, speed ); + + // if position utility is not available... + } else { + menu.css({ + top: pos.top+button.outerHeight(), + left: pos.left + }).show( effect, speed ); + } + + // select the first option + // triggering both mouseover and mouseover because 1.4.2+ has a bug where triggering mouseover + // will actually trigger mouseenter. the mouseenter trigger is there for when it's eventually fixed + this.labels.eq(0).trigger('mouseover').trigger('mouseenter').find('input').trigger('focus'); + + button.addClass('ui-state-active'); + this._isOpen = true; + this._trigger('open'); + }, + + // close the menu + close: function(){ + if(this._trigger('beforeclose') === false){ + return; + } + + var o = this.options, effect = o.hide, speed = this.speed; + + // figure out opening effects/speeds + if( $.isArray(o.hide) ){ + effect = o.hide[0]; + speed = o.hide[1] || this.speed; + } + + this.menu.hide(effect, speed); + this.button.removeClass('ui-state-active').trigger('blur').trigger('mouseleave'); + this._isOpen = false; + this._trigger('close'); + }, + + enable: function(){ + this._toggleDisabled(false); + }, + + disable: function(){ + this._toggleDisabled(true); + }, + + checkAll: function( e ){ + this._toggleChecked(true); + this._trigger('checkAll'); + }, + + uncheckAll: function(){ + this._toggleChecked(false); + this._trigger('uncheckAll'); + }, + + getChecked: function(){ + return this.menu.find('input').filter(':checked'); + }, + + destroy: function(){ + // remove classes + data + $.Widget.prototype.destroy.call( this ); + + this.button.remove(); + this.menu.remove(); + this.element.show(); + + return this; + }, + + isOpen: function(){ + return this._isOpen; + }, + + widget: function(){ + return this.menu; + }, + + // react to option changes after initialization + _setOption: function( key, value ){ + var menu = this.menu; + + switch(key){ + case 'header': + menu.find('div.ui-multiselect-header')[ value ? 'show' : 'hide' ](); + break; + case 'checkAllText': + menu.find('a.ui-multiselect-all span').eq(-1).text(value); + break; + case 'uncheckAllText': + menu.find('a.ui-multiselect-none span').eq(-1).text(value); + break; + case 'height': + menu.find('ul:last').height( parseInt(value,10) ); + break; + case 'minWidth': + this.options[ key ] = parseInt(value,10); + this._setButtonWidth(); + this._setMenuWidth(); + break; + case 'selectedText': + case 'selectedList': + case 'noneSelectedText': + this.options[key] = value; // these all needs to update immediately for the update() call + this.update(); + break; + case 'classes': + menu.add(this.button).removeClass(this.options.classes).addClass(value); + break; + } + + $.Widget.prototype._setOption.apply( this, arguments ); + } +}); + +})(jQuery); diff --git a/lib/base.php b/lib/base.php index 9542170df5..9b77780076 100644 --- a/lib/base.php +++ b/lib/base.php @@ -115,7 +115,7 @@ class OC{ OC_Util::addScript( "jquery-showpassword" ); OC_Util::addScript( "jquery-tipsy" ); OC_Util::addScript( "js" ); - OC_Util::addScript( "multiselect" ); + //OC_Util::addScript( "multiselect" ); OC_Util::addScript('search','result'); OC_Util::addStyle( "styles" ); OC_Util::addStyle( "multiselect" );