diff --git a/Extensible-config.js b/Extensible-config.js index d0adc8bd..b42d1e0b 100644 --- a/Extensible-config.js +++ b/Extensible-config.js @@ -3,15 +3,15 @@ Extensible = { }; /** * ================================================================================================= - * + * * THIS FILE IS FOR *DEV* MODE ONLY, NOT FOR PRODUCTION USE! - * + * * ================================================================================================= - * + * * This is intended as a development mode only convenience so that you can configure all include * paths for all Extensible examples in one place. For production deployment you should configure * your application with your own custom includes and/or Ext.Loader configuration directly. - * + * * ================================================================================================= */ Extensible.Config = { @@ -21,66 +21,69 @@ Extensible.Config = { defaults: { /** * The mode to use for loading framework files. Valid values are: - * + * * - 'release': minified single file (e.g. ext-all.js) * - 'debug': (default) non-minifed single file (e.g. ext-all-debug.js) * - 'dynamic': uses Ext.Loader to load classes individually (e.g., ext.js). NOTE: this * option does not work for IE, which will be defaulted to the 'debug' option. - * + * - 'dynamic-extensible': Loads the Extensible framework dynically and the EXT JS framework from a + * non-minified single file. This loads much faster than the 'dynamic' mode. NOTE: This + * option does not work for IE, which will be defaulted to the 'debug' option. + * * Typically the default of 'debug' is the best trade-off between code readability and * load/execution speed. If you need to step into framework files frequently during * debugging you might switch to 'dynamic' mode -- it is much slower during initial * page load but generally provides a faster and easier debugging experience. - * + * * Note that for debug and release modes to reflect any code or CSS changes made to Extensible * files you must rebuild the framework after each change using the scripts provided under * the `/build` folder (requires Java). If you cannot build the framework and you've made any * changes to Extensible files you should use dynamic mode to ensure that changes are reflected. - * + * * @config {String} mode */ - mode: 'debug', - + mode: 'dynamic-extensible', + /** * The root path to the Ext JS framework (defaults to loading 4.1.0 from the Sencha CDN via * 'http://cdn.sencha.io/ext-4.1.0-gpl/'). Path should be absolute and should end with a '/'. - * + * * Note that the Sencha CDN does not always provide the most current version of Ext JS * available (for example, support subscribers often have access to more up-to-date builds). * If the version you need is not hosted you'll have to download it locally and update this * path accordingly. - * + * * Alternate example values: - * + * * // Older Ext JS versions: * http://cdn.sencha.io/ext-4.0.2/ - * + * * // Direct to cachefly.net, e.g. if sencha.io is down: * http://extjs.cachefly.net/ext-4.1.0-gpl/ - * + * * // A custom absolute path: * http://localhost/extjs/ * http://mydomain/extjs/4.0.2/ - * + * * @config {String} extJsRoot */ extJsRoot: 'http://cdn.sencha.io/ext-4.2.0-gpl/', - + /** * The root path to the Extensible framework (defaults to the current url of this script file, * 'Extensible-config.js', which is shipped in the root folder of Extensible). Path should * be absolute and should end with a '/'. - * + * * Alternate example values: - * + * * // A custom absolute path: * http://localhost/extensible/ * http://mydomain/extensible/1.0.1/ - * + * * @config {String} extensibleRoot */ extensibleRoot: null, // initialized dynamically in getSdkPath() - + /** * True to allow the default browser behavior of caching the Extensible JS and CSS files * after initial load (defaults to true), or false to append a unique cache-buster parameter @@ -88,17 +91,17 @@ Extensible.Config = { * actively changing and debugging Extensible code). If true, the current version number of * Extensible will still be used to force a reload with each new version of the framework, but * after the initial load of each version the cached files will be used. - * + * * This option only applies when using `debug` or `dynamic` modes. In `release` mode the Extensible * version number will be used to ensure that Extensible files are always cached after the initial * load of each release and this option will be ignored. Note that when using `dynamic` mode you * would additionally have to ensure that the Ext.Loader's `disableCaching` option is true in order - * to add the cache buster parameter to each dynamically-loaded class. - * + * to add the cache buster parameter to each dynamically-loaded class. + * * Note that this option does not affect the caching of Ext JS files in any way. If you are * using dynamic loading, the Ext Loader will govern caching, otherwise the default browser * caching will be in effect. - * + * * @config {Boolean} cacheExtensible */ cacheExtensible: true, @@ -106,13 +109,13 @@ Extensible.Config = { /** * Language files to load for the Ext JS and Extensible frameworks. Valid values are ISO language codes of * supported languages. See directory src/locale for a list of supported languages. Examples are: - * + * * - 'en' * - 'en_GB' * - 'de' * - 'fr' * - etc... - * + * * NOTE: This setting will NOT work for Ext versions < 4.1 due to how the locale files were written * in 4.0.x. Because the 4.0.x locale files check for existence of classes by reference rather than * by name, they do not play nicely when loaded asynchronously (Ext may load later, causing runtime @@ -124,16 +127,16 @@ Extensible.Config = { */ language: null }, - + /** * Sets up all configurable properties and writes all includes to the document. */ init: function() { var me = this, config = window.ExtensibleDefaults || {}; - + me.isIE = /msie/.test(navigator.userAgent.toLowerCase()); - + me.mode = config.mode || me.defaults.mode; me.extJsRoot = config.extJsRoot || me.defaults.extJsRoot; me.extensibleRoot = config.extensibleRoot || me.defaults.extensibleRoot || me.getSdkPath(); @@ -143,16 +146,16 @@ Extensible.Config = { me.adjustPaths(); me.writeIncludes(); }, - + // private -- returns the current url to this script file, which is shipped in the SDK root folder getSdkPath: function() { var scripts = document.getElementsByTagName('script'), thisScriptSrc = scripts[scripts.length - 1].src, sdkPath = thisScriptSrc.substring(0, thisScriptSrc.lastIndexOf('/') + 1); - + return sdkPath; }, - + // private -- helper function for ease of deployment adjustPaths: function() { if (this.extensibleRoot.indexOf('ext.ensible.com') > -1) { @@ -160,58 +163,53 @@ Extensible.Config = { this.mode = 'release'; } }, - + includeStylesheet: function(filePath) { document.write(''); }, - + includeScript: function(filePath) { document.write(''); }, - + // private -- write out the CSS and script includes to the document writeIncludes: function() { var me = this, - cacheBuster = '?_dc=' + (me.cacheExtensible ? Extensible.version : (+new Date)), - suffixExt = '', - suffixExtensible = ''; - - switch (me.mode) { - case 'debug': - suffixExt = '-all-debug'; - suffixExtensible = '-all-debug'; - break; - - case 'release': - suffixExt = '-all'; - suffixExtensible = '-all' - // For release we want to refresh the cache on first load, but allow caching - // after that, so use the version number instead of a unique string - cacheBuster = '?_dc=' + Extensible.version; - break; - - default: - // IE does not work in dynamic mode for the Extensible examples currently - // based on how it (mis)handles loading of scripts when mixing includes - // and in-page scripts. Make sure IE always uses the regular debug versions. - if (me.isIE) { - suffixExt = '-all-debug'; - suffixExtensible = '-all-debug'; - } - else { - suffixExt = '-debug'; - suffixExtensible = '-bootstrap'; - } - } - + cacheBuster = '?_dc=' + (me.cacheExtensible ? Extensible.version : (+new Date)); + + // Include style sheets me.includeStylesheet(me.extJsRoot + 'resources/css/ext-all.css'); - me.includeStylesheet(me.extensibleRoot + 'resources/css/extensible-all.css' + cacheBuster); + if (me.mode === 'release') { + me.includeStylesheet(me.extensibleRoot + 'resources/css/extensible-all.css' + cacheBuster); + } else { + me.includeStylesheet(me.extensibleRoot + 'resources/css/calendar.css' + cacheBuster); + me.includeStylesheet(me.extensibleRoot + 'resources/css/calendar-colors.css' + cacheBuster); + me.includeStylesheet(me.extensibleRoot + 'resources/css/recurrence.css' + cacheBuster); + } me.includeStylesheet(me.extensibleRoot + 'examples/examples.css?_dc=' + Extensible.version); - - me.includeScript(me.extJsRoot + 'ext' + suffixExt + '.js'); - me.includeScript(me.extensibleRoot + 'lib/extensible' + suffixExtensible + '.js' + cacheBuster); + + // Include JS files + if (me.mode === 'debug' || me.isIE) { + // IE does not work in dynamic mode for the Extensible examples currently + // based on how it (mis)handles loading of scripts when mixing includes + // and in-page scripts. Make sure IE always uses the regular debug versions. + me.includeScript(me.extJsRoot + 'ext-all-debug.js'); + me.includeScript(me.extensibleRoot + 'lib/extensible-all-debug.js' + cacheBuster); + } else if (me.mode === 'release') { + // For release we want to refresh the cache on first load, but allow caching + // after that, so use the version number instead of a unique string + cacheBuster = '?_dc=' + Extensible.version; + me.includeScript(me.extJsRoot + 'ext-all.js'); + me.includeScript(me.extensibleRoot + 'lib/extensible-all.js' + cacheBuster); + } else { + if (me.mode === 'dynamic-extensible') { + me.includeScript(me.extJsRoot + 'ext-all-debug.js'); + } else { + me.includeScript(me.extJsRoot + 'ext-debug.js'); + } + me.includeScript(me.extensibleRoot + 'lib/extensible-bootstrap.js' + cacheBuster); + } me.includeScript(me.extensibleRoot + 'examples/examples.js?_dc=' + Extensible.version); - if (me.language) { me.includeScript(me.extJsRoot + 'locale/ext-lang-' + me.language + '.js'); me.includeScript(me.extensibleRoot + 'src/locale/extensible-lang-' + me.language + '.js' + cacheBuster); diff --git a/build/resources/extensible.jsb2 b/build/resources/extensible.jsb2 index 36777d8a..a9da86d8 100644 --- a/build/resources/extensible.jsb2 +++ b/build/resources/extensible.jsb2 @@ -38,6 +38,9 @@ },{ "text": "Month.js", "path": "../../src/calendar/template/" + },{ + "text": "AgendaBody.js", + "path": "../../src/calendar/template/" },{ "text": "CalendarScrollManager.js", "path": "../../src/calendar/dd/" @@ -128,6 +131,15 @@ },{ "text": "MultiWeek.js", "path": "../../src/calendar/view/" + },{ + "text": "AgendaHeader.js", + "path": "../../src/calendar/view/" + },{ + "text": "AgendaBody.js", + "path": "../../src/calendar/view/" + },{ + "text": "Agenda.js", + "path": "../../src/calendar/view/" },{ "text": "CalendarPanel.js", "path": "../../src/calendar/" diff --git a/examples/calendar/TestApp/App.js b/examples/calendar/TestApp/App.js index c2b18f5d..97a73440 100644 --- a/examples/calendar/TestApp/App.js +++ b/examples/calendar/TestApp/App.js @@ -1,6 +1,6 @@ Ext.Loader.setConfig({ enabled: true, - //disableCaching: false, + disableCaching: false, paths: { "Extensible": "../../../src", "Extensible.example": "../.." @@ -105,6 +105,7 @@ Ext.define('Extensible.example.calendar.TestApp.App', { //viewStartHour: 6, //viewEndHour: 18, //minEventDisplayMinutes: 15 + startDay: 0, showTime: false }, @@ -119,7 +120,19 @@ Ext.define('Extensible.example.calendar.TestApp.App', { multiWeekViewCfg: { //weekCount: 3 }, - + + agendaViewCfg: { + linkDatesToDayView: true, + dateRangeDefault: '3months' + }, + + listViewCfg: { + linkDatesToDayView: true, + dateRangeDefault: '3months', + simpleList: true, + groupBy: 'month' + }, + // Some optional CalendarPanel configs to experiment with: //readOnly: true, //showDayView: false, @@ -127,6 +140,8 @@ Ext.define('Extensible.example.calendar.TestApp.App', { //showWeekView: false, //showMultiWeekView: false, //showMonthView: false, + showAgendaView: true, + showListView: true, //showNavBar: false, //showTodayText: false, //showTime: false, @@ -135,6 +150,12 @@ Ext.define('Extensible.example.calendar.TestApp.App', { //title: 'My Calendar', // the header of the calendar, could be a subtitle for the app listeners: { + 'datechange': { + fn: function(vw, startDt, viewStart, viewEnd){ + this.updateTitle(viewStart, viewEnd); + }, + scope: this + }, 'eventclick': { fn: function(vw, rec, el){ this.clearMsg(); diff --git a/examples/calendar/basic.js b/examples/calendar/basic.js index d7574900..f4a8c7f2 100644 --- a/examples/calendar/basic.js +++ b/examples/calendar/basic.js @@ -26,7 +26,10 @@ Ext.onReady(function(){ renderTo: 'simple', title: 'Basic Calendar', width: 700, - height: 500 + height: 500, + activeItem: 3, // default to month view + showAgendaView: true, + showListView: true }); // @@ -38,6 +41,8 @@ Ext.onReady(function(){ eventStore: eventStore, renderTo: 'panel', title: 'Calendar with Panel Configs', + showAgendaView: true, + showListView: true, activeItem: 1, // default to week view width: 700, height: 500, diff --git a/examples/calendar/custom-mappings.js b/examples/calendar/custom-mappings.js index e66e8272..9d2e5abd 100644 --- a/examples/calendar/custom-mappings.js +++ b/examples/calendar/custom-mappings.js @@ -81,6 +81,9 @@ Ext.onReady(function(){ calendarStore: calendarStore, renderTo: 'cal', title: 'Custom Event Mappings', + showAgendaView: true, + showListView: true, + activeItem: 3, // default to month view width: 800, height: 700 }); diff --git a/examples/calendar/custom-views.html b/examples/calendar/custom-views.html index edf67f9d..90a293ee 100644 --- a/examples/calendar/custom-views.html +++ b/examples/calendar/custom-views.html @@ -31,7 +31,7 @@ .x-cal-default-ad .ext-cal-evm, .x-cal-default .ext-cal-picker-icon, .x-cal-default-x dl, - .x-calendar-list-menu li em .x-cal-default { + .x-calendar-agenda-menu li em .x-cal-default { background: #59638F; } diff --git a/examples/calendar/data/Events.js b/examples/calendar/data/Events.js index 6dc74aac..6887d2b8 100644 --- a/examples/calendar/data/Events.js +++ b/examples/calendar/data/Events.js @@ -8,7 +8,7 @@ Ext.define('Extensible.example.calendar.data.Events', { s = (s || 0); return Ext.Date.add(today, Ext.Date.SECOND, d + h + m + s); }; - + return { "evts" : [{ "id" : 1001, @@ -24,7 +24,7 @@ Ext.define('Extensible.example.calendar.data.Events', { "start" : makeDate(0, 11, 30), "end" : makeDate(0, 13), "loc" : "Chuy's!", - "url" : "http : //chuys.com", + "url" : "http://chuys.com", "notes" : "Order the queso", "rem" : "15" },{ diff --git a/examples/calendar/doc-types.js b/examples/calendar/doc-types.js index d6416410..46f409a7 100644 --- a/examples/calendar/doc-types.js +++ b/examples/calendar/doc-types.js @@ -75,6 +75,8 @@ Ext.onReady(function(){ }), renderTo: 'cal', title: 'Doctype Tester', + showAgendaView: true, + showListView: true, activeItem: 1, width: 800, height: 700 diff --git a/examples/calendar/localization.js b/examples/calendar/localization.js index 2270d14d..16e58722 100644 --- a/examples/calendar/localization.js +++ b/examples/calendar/localization.js @@ -108,6 +108,13 @@ Ext.onReady(function() { multiDayViewCfg: { dayCount: 5 }, + showAgendaView: true, + showListView: true, + listViewCfg: { + dateRangeDefault: '3months', + groupBy: 'month' + }, + activeItem: 4, // default to month view eventStore: Ext.create('Extensible.calendar.data.MemoryEventStore', { // defined in ../data/Events.js data: Ext.create('Extensible.example.calendar.data.Events') diff --git a/examples/calendar/remote/php/app.php b/examples/calendar/remote/php/app.php new file mode 100644 index 00000000..e69de29b diff --git a/examples/calendar/remote/recurrence.js b/examples/calendar/remote/recurrence.js index 0cca44a7..9c6680f1 100644 --- a/examples/calendar/remote/recurrence.js +++ b/examples/calendar/remote/recurrence.js @@ -1,6 +1,6 @@ Ext.Loader.setConfig({ enabled: true, - //disableCaching: false, + disableCaching: false, paths: { "Extensible": "../../../src", "Extensible.example": "../../" @@ -149,6 +149,20 @@ Ext.onReady(function() { eventStore: eventStore, calendarStore: calendarStore, title: 'Recurrence Calendar', + showAgendaView: true, + showListView: true, + + agendaViewCfg: { + linkDatesToDayView: true, + dateRangeDefault: '3months' + }, + + listViewCfg: { + linkDatesToDayView: true, + dateRangeDefault: '3months', + groupBy: 'week' + }, + // This is the magical config that enables the recurrence edit // widget to appear in the event form. Without it, any existing diff --git a/examples/calendar/remote/remote.js b/examples/calendar/remote/remote.js index df53ccad..54a1d002 100644 --- a/examples/calendar/remote/remote.js +++ b/examples/calendar/remote/remote.js @@ -1,6 +1,6 @@ Ext.Loader.setConfig({ enabled: true, - //disableCaching: false, + disableCaching: false, paths: { "Extensible": "../../../src", "Extensible.example": "../../" @@ -121,7 +121,26 @@ Ext.onReady(function() { region: 'center', // it will be used in a border layout below eventStore: eventStore, calendarStore: calendarStore, - title: 'Remote Calendar' + title: 'Remote Calendar', + showAgendaView: true, + showListView: true, + activeItem: 3, // month view + + // Any generic view options that should be applied to all sub views: + viewConfig: { + startDay: 0 + }, + + agendaViewCfg: { + linkDatesToDayView: true, + dateRangeDefault: '3months' + }, + + listViewCfg: { + linkDatesToDayView: true, + dateRangeDefault: '3months', + groupBy: 'month' + } }); Ext.create('Ext.container.Viewport', { @@ -133,6 +152,7 @@ Ext.onReady(function() { collapsible: true, split: true, autoScroll: true, + showListView: true, contentEl: 'sample-overview' // from remote.html }, calendarPanel diff --git a/examples/calendar/tabpanel.js b/examples/calendar/tabpanel.js index e469fd55..77924c1d 100644 --- a/examples/calendar/tabpanel.js +++ b/examples/calendar/tabpanel.js @@ -33,9 +33,17 @@ Ext.onReady(function(){ width: 700, height: 500, activeItem: 1, + showAgendaView: true, + showListView: true, // this is a good idea since we are in a TabPanel and we don't want // the user switching tabs on us while we are editing an event: - editModal: true + editModal: true, + + listViewCfg: { + dateRangeDefault: '3months', + groupBy: 'week' + } + }; // diff --git a/examples/calendar/window.js b/examples/calendar/window.js index 87d651fd..27c9dd62 100644 --- a/examples/calendar/window.js +++ b/examples/calendar/window.js @@ -26,6 +26,9 @@ Ext.onReady(function(){ items: { // xtype is supported: xtype: 'extensible.calendarpanel', + activeItem: 3, // default to month view + showAgendaView: true, + showListView: true, eventStore: Ext.create('Extensible.calendar.data.MemoryEventStore', { // defined in ../data/Events.js data: Ext.create('Extensible.example.calendar.data.Events') diff --git a/lib/extensible-bootstrap.js b/lib/extensible-bootstrap.js index 913e7893..ff564875 100644 --- a/lib/extensible-bootstrap.js +++ b/lib/extensible-bootstrap.js @@ -243,7 +243,7 @@ Ext.define('Extensible', { * @return {Boolean} True if the date is a week day, else false */ isWeekday: function(dt) { - return dt.getDay() % 6 !== 0; + return !this.isWeekend(dt); }, /** @@ -344,6 +344,14 @@ Ext.define('Extensible', { clearTime: function(dt, clone) { return Ext.Date.clearTime(dt, clone); + }, + + getMonth: function(dt) { + return dt.getMonth(); + }, + + getDate: function(dt) { + return dt.getDate(); } } }); diff --git a/resources/css/calendar.css b/resources/css/calendar.css index d003db7d..7d9722c1 100644 --- a/resources/css/calendar.css +++ b/resources/css/calendar.css @@ -343,9 +343,19 @@ border-left: 1px solid #C3D9FF; cursor: pointer; } +/* Mehran Ziadloo: RTL { */ +/* Original: .ext-cal-day-first { border-left: 0; } +*/ +*[style*="ltr"] .ext-cal-day-first { + border-left: 0; +} +*[style*="rtl"] .ext-cal-day-first { + border-right: 0; +} +/* RTL } */ .ext-cal-ev, .ext-cal-dtitle { @@ -535,8 +545,10 @@ td.ext-cal-dtitle-today div { -moz-border-radius:0; -webkit-border-radius:0; border-radius:0; - position: relative; + position: relative; } +/* Mehran Ziadloo : RTL { */ +/* Original { .ext-cal-ev-spanleft { -moz-border-radius-topright:5px; -moz-border-radius-bottomright:5px; @@ -546,7 +558,7 @@ td.ext-cal-dtitle-today div { border-bottom-right-radius:5px; xpadding-left:5px; } -.ext-cal-ev-spanright { +*[style*='ltr'] .ext-cal-ev-spanright { -moz-border-radius-topleft:5px; -moz-border-radius-bottomleft:5px; -webkit-border-top-left-radius:5px; @@ -555,6 +567,45 @@ td.ext-cal-dtitle-today div { border-bottom-left-radius:5px; xpadding-right:5px; } +*/ +*[style*='ltr'] .ext-cal-ev-spanleft { + -moz-border-radius-topright:5px; + -moz-border-radius-bottomright:5px; + -webkit-border-top-right-radius:5px; + -webkit-border-bottom-right-radius:5px; + border-top-right-radius:5px; + border-bottom-right-radius:5px; + xpadding-left:5px; +} +*[style*='ltr'] .ext-cal-ev-spanright { + -moz-border-radius-topleft:5px; + -moz-border-radius-bottomleft:5px; + -webkit-border-top-left-radius:5px; + -webkit-border-bottom-left-radius:5px; + border-top-left-radius:5px; + border-bottom-left-radius:5px; + xpadding-right:5px; +} +*[style*='rtl'] .ext-cal-ev-spanleft { + -moz-border-radius-topleft:5px; + -moz-border-radius-bottomleft:5px; + -webkit-border-top-left-radius:5px; + -webkit-border-bottom-left-radius:5px; + border-top-left-radius:5px; + border-bottom-left-radius:5px; + xpadding-right:5px; +} +*[style*='rtl'] .ext-cal-ev-spanright { + -moz-border-radius-topright:5px; + -moz-border-radius-bottomright:5px; + -webkit-border-top-right-radius:5px; + -webkit-border-bottom-right-radius:5px; + border-top-right-radius:5px; + border-bottom-right-radius:5px; + xpadding-left:5px; +} +/* RTL } */ + /* IE/ Opera: */ .ext-cal-ev-spanboth .ext-cal-evm { margin: 0; @@ -648,6 +699,8 @@ td.ext-cal-dtitle-today div { /* http://stackoverflow.com/questions/5148041/does-firefox-support-position-relative-on-table-elements */ display: block; } +/* Mehran Ziadloo: RTL { */ +/* Original: .ext-calendar-picker .x-form-text { padding-left: 27px; } @@ -670,6 +723,55 @@ td.ext-cal-dtitle-today div { top: 3px; left: 4px; } +*/ +*[style*="ltr"] .ext-calendar-picker .x-form-text { + padding-left: 27px; +} +*[style*="ltr"] .ext-calendar-picker .x-combo-list-item { + padding-left: 24px; +} +*[style*="ltr"] .ext-cal-picker-icon { + width: 20px; + height: 16px; + margin-right: 5px; + -moz-border-radius:5px; + -webkit-border-radius:5px; + border-radius:5px; + float: left; +} +/* This is the selected icon that always shows: */ +*[style*="ltr"] .ext-cal-picker-mainicon { + float: none; + position: absolute; + top: 3px; + left: 4px; +} + +*[style*="rtl"] .ext-calendar-picker .x-form-text { + padding-right: 27px; +} +*[style*="rtl"] .ext-calendar-picker .x-combo-list-item { + padding-right: 24px; +} +*[style*="rtl"] .ext-cal-picker-icon { + width: 20px; + height: 16px; + margin-left: 5px; + -moz-border-radius:5px; + -webkit-border-radius:5px; + border-radius:5px; + float: right; +} +/* This is the selected icon that always shows: */ +*[style*="rtl"] .ext-cal-picker-mainicon { + float: none; + position: absolute; + top: 3px; + right: 4px; +} + +/* RTL } */ + /* * Event resize handle @@ -755,6 +857,66 @@ td.ext-cal-dtitle-today div { display: none; } +/* ----------------------------------------- + * Agenda view specific styles + */ +.ext-cal-agenda { + border-top: 1px solid #99BBE8; +} + +.ext-cal-agenda-hd { + border-bottom: 1px solid #99BBE8; +} + +.ext-cal-agenda-hd .x-panel-body { + background-color: #F0F4FA !important; +} +.ext-cal-agenda-hd .x-toolbar { + background-color: #F0F4FA !important; + background-image: none !important; +} + +.ext-cal-icon-evt-add { + background-image: url(../images/default/ext/add.gif) !important; +} + +.ext-cal-agenda-bd .ext-cal-evt-agenda td { + padding: 2px 10px; + font-size: 12px; + vertical-align: top; +} + +.ext-cal-agenda-bd .ext-cal-evt-agenda-details td { + padding: 1px 10px 1px 0; +} + +.ext-cal-agenda-bd .ext-cal-day-link { + cursor: pointer; +} + +.ext-cal-agenda-bd hr { + margin:10px 0; + color: #CCC; + background-color:#CCC; + height:1px; + border:0; +} + +/* new styles, adjust for white skin */ +.ext-cal-agenda-bd td.ext-cal-agenda-group-header { + color: #A7C6DF; + font-size: 16px; + line-height: 14px; + font-weight: bold; + padding-top: 10px; + padding-bottom: 0px; +} + +.ext-cal-agenda-bd .ext-cal-evt-agenda td.ext-cal-evt-hours { + padding: 2px 2px; +} + + /******************************************* * * Calendar navigation picker styles @@ -969,9 +1131,11 @@ td.ext-cal-dtitle-today div { .extensible-cal-icon-cal-show { background-image:url(../images/default/silk/calendar_view_month.png) !important; } +/* Brian: Image color_wheel.png does not exist. Can this style be removed? .extensible-cal-icon-cal-colors { background-image:url(../images/default/silk/color_wheel.png) !important; } +*/ /******************************************* * diff --git a/resources/images/default/ext/add.gif b/resources/images/default/ext/add.gif new file mode 100644 index 00000000..93195256 Binary files /dev/null and b/resources/images/default/ext/add.gif differ diff --git a/src/Extensible.js b/src/Extensible.js index 913e7893..ff564875 100644 --- a/src/Extensible.js +++ b/src/Extensible.js @@ -243,7 +243,7 @@ Ext.define('Extensible', { * @return {Boolean} True if the date is a week day, else false */ isWeekday: function(dt) { - return dt.getDay() % 6 !== 0; + return !this.isWeekend(dt); }, /** @@ -344,6 +344,14 @@ Ext.define('Extensible', { clearTime: function(dt, clone) { return Ext.Date.clearTime(dt, clone); + }, + + getMonth: function(dt) { + return dt.getMonth(); + }, + + getDate: function(dt) { + return dt.getDate(); } } }); diff --git a/src/calendar/CalendarPanel.js b/src/calendar/CalendarPanel.js index 002b6130..6d66e191 100644 --- a/src/calendar/CalendarPanel.js +++ b/src/calendar/CalendarPanel.js @@ -14,7 +14,8 @@ Ext.define('Extensible.calendar.CalendarPanel', { 'Extensible.calendar.view.Week', 'Extensible.calendar.view.Month', 'Extensible.calendar.view.MultiDay', - 'Extensible.calendar.view.MultiWeek' + 'Extensible.calendar.view.MultiWeek', + 'Extensible.calendar.view.Agenda' ], /** @@ -58,6 +59,18 @@ Ext.define('Extensible.calendar.CalendarPanel', { * If all other views are hidden, the month view will show by default even if this config is false. */ showMonthView: true, + /** + * @cfg {Boolean} showAgendaView + * True to include the agenda view (and toolbar button), false to hide them (defaults to false). + */ + showAgendaView: false, + /** + * @cfg {Boolean} showListView + * True to include the list view (and toolbar button), false to hide them (defaults to false). The list view + * is an instance of {@link Extensible.calendar.view.Agenda agenda view} that is preconfigured to show a simple list + * of events rather than an agenda style list of events. + */ + showListView: false, /** * @cfg {Boolean} showNavBar * True to display the calendar navigation toolbar, false to hide it (defaults to true). Note that @@ -111,6 +124,11 @@ Ext.define('Extensible.calendar.CalendarPanel', { * Text to use for the 'Jump to:' navigation label. */ jumpToText: 'Jump to:', + /** + * @cfg {String} jumpToDateFormat + * Date format to use for the 'Jump to:' navigation date field. + */ + jumpToDateFormat: 'y/n/j', /** * @cfg {String} goText * Text to use for the 'Go' navigation button. @@ -151,6 +169,16 @@ Ext.define('Extensible.calendar.CalendarPanel', { * Text to use for the 'Month' nav bar button. */ monthText: 'Month', + /** + * @cfg {String} agendaText + * Text to use for the 'Agenda' nav bar button. + */ + agendaText: 'Agenda', + /** + * @cfg {String} listText + * Text to use for the 'List' nav bar button. + */ + listText: 'List', /** * @cfg {Boolean} editModal * True to show the default event editor window modally over the entire page, false to allow user @@ -215,6 +243,17 @@ Ext.define('Extensible.calendar.CalendarPanel', { * A config object that will be applied only to the {@link Extensible.calendar.view.Month MonthView} * managed by this CalendarPanel. */ + /** + * @cfg {Object} agendaViewCfg + * A config object that will be applied only to the {@link Extensible.calendar.view.Agenda agenda view} managed + * by this CalendarPanel. + */ + /** + * @cfg {Object} listViewCfg + * A config object that will be applied only to the {@link Extensible.calendar.view.Agenda list view} managed + * by this CalendarPanel. List view is an instance of {@link Extensible.calendar.view.Agenda agenda view} that is + * preconfigured to show a simple list of events rather than a agenda style list of events. + */ /** * @cfg {Object} editViewCfg * A config object that will be applied only to the {@link Extensible.calendar.form.EventDetails @@ -294,14 +333,26 @@ Ext.define('Extensible.calendar.CalendarPanel', { }); this.viewCount++; } - if(this.showMonthView || this.viewCount === 0) { + if(this.showMonthView) { this.tbar.items.push({ id: this.id+'-tb-month', text: this.monthText, handler: this.onMonthNavClick, scope: this, toggleGroup: this.id+'-tb-views' }); this.viewCount++; this.showMonthView = true; } - + if(this.showAgendaView){ + this.tbar.items.push({ + id: this.id+'-tb-agenda', text: this.agendaText, handler: this.onAgendaNavClick, scope: this, toggleGroup: this.id+'-tb-views' + }); + this.viewCount++; + } + if(this.showListView || this.viewCount === 0){ + this.tbar.items.push({ + id: this.id+'-tb-list', text: this.listText, handler: this.onListNavClick, scope: this, toggleGroup: this.id+'-tb-views' + }); + this.viewCount++; + } + var idx = this.viewCount-1; this.activeItem = (this.activeItem === undefined ? idx : (this.activeItem > idx ? idx : this.activeItem)); @@ -630,7 +681,62 @@ Ext.define('Extensible.calendar.CalendarPanel', { this.initEventRelay(month); this.add(month); } + if(this.showAgendaView){ + var agenda = Ext.applyIf({ + xtype: 'extensible.agendaview', + title: this.agendaText, + listeners: { + 'dayclick': { + fn: function(vw, dt){ + this.showDay(dt); + }, + scope: this + }, + 'datechange': { + fn: function(){ + // AgendaView allows the changing of start and end dates from within in the view. Update + // the nav state this happens. + this.updateNavState(); + }, + scope: this + } + } + }, sharedViewCfg); + + agenda = Ext.apply(Ext.apply(agenda, this.viewConfig), this.agendaViewCfg); + agenda.id = this.id+'-agenda'; + this.initEventRelay(agenda); + this.add(agenda); + } + if(this.showListView){ + var list = Ext.applyIf({ + xtype: 'extensible.agendaview', + title: this.listText, + simpleList: true, + groupBy: 'month', + listeners: { + 'dayclick': { + fn: function(vw, dt){ + this.showDay(dt); + }, + scope: this + }, + 'datechange': { + fn: function(){ + // AgendaView allows the changing of start and end dates from within in the view. Update + // the nav state this happens. + this.updateNavState(); + }, + scope: this + } + } + }, sharedViewCfg); + list = Ext.apply(Ext.apply(list, this.viewConfig), this.listViewCfg); + list.id = this.id+'-list'; + this.initEventRelay(list); + this.add(list); + } this.add(Ext.applyIf({ xtype: 'extensible.eventeditform', id: this.id+'-edit', @@ -900,6 +1006,11 @@ Ext.define('Extensible.calendar.CalendarPanel', { this.fireViewChange(); return this; }, + + // private + showDay: function(dt) { + this.setActiveView(this.id+'-day', dt); + }, showWeek: function(dt) { this.setActiveView(this.id+'-week', dt); @@ -952,7 +1063,17 @@ Ext.define('Extensible.calendar.CalendarPanel', { onMonthNavClick: function() { this.setActiveView(this.id+'-month'); }, - + + // private + onAgendaNavClick: function(){ + this.setActiveView(this.id+'-agenda'); + }, + + // private + onListNavClick: function(){ + this.setActiveView(this.id+'-list'); + }, + /** * Return the calendar view that is currently active, which will be a subclass of * {@link Extensible.calendar.view.AbstractCalendar AbstractCalendar}. diff --git a/src/calendar/form/EventWindow.js b/src/calendar/form/EventWindow.js index f289ab73..99362b3d 100644 --- a/src/calendar/form/EventWindow.js +++ b/src/calendar/form/EventWindow.js @@ -66,6 +66,7 @@ Ext.define('Extensible.calendar.form.EventWindow', { // General configs closeAction: 'hide', + closable: false, modal: false, resizable: false, constrain: true, @@ -77,7 +78,18 @@ Ext.define('Extensible.calendar.form.EventWindow', { formPanelConfig: { border: false }, - + + /** + * Add close tool to panel header. When closing the editor it is important to cleanup the record if dirty. + * Handle it the same way as the cancel button. + */ + tools: [{ + type:'close', + handler: function(evt, el, header){ + header.ownerCt.onCancel(); + } + }], + /** * @cfg {Boolean} allowDefaultAdd * @since 1.6.0 @@ -309,7 +321,7 @@ Ext.define('Extensible.calendar.form.EventWindow', { rec.data[EventMappings.EndDate.name] = end; rec.data[EventMappings.IsAllDay.name] = !!o[EventMappings.IsAllDay.name] || - (start.getDate() !== Extensible.Date.add(end, {millis: 1}).getDate()); + (Extensible.Date.getDate(start) !== Extensible.Date.getDate(Extensible.Date.add(end, {millis: 1}))); rec.data[EventMappings.CalendarId.name] = me.calendarStore ? me.calendarStore.getAt(0).data[Extensible.calendar.data.CalendarMappings.CalendarId.name] : ''; diff --git a/src/calendar/template/AgendaBody.js b/src/calendar/template/AgendaBody.js new file mode 100644 index 00000000..bc934ea2 --- /dev/null +++ b/src/calendar/template/AgendaBody.js @@ -0,0 +1,551 @@ +/** + * @class Extensible.calendar.template.AgendaBody + * @extends Ext.XTemplate + * + *

This class is currently beta code and the API is still subject to change before the next release.

+ * + *

This is the template used to render the {@link Extensible.calendar.view.AgendaBody AgendaBody}.

+ * + *

This template is automatically bound to the underlying event store by the + * calendar components and expects records of type {@link Extensible.calendar.data.EventModel}.

+ * + * @author Gabriel Sidler, sidler@teamup.com + * @constructor + * @param {Object} config The config object + */ +Ext.define('Extensible.calendar.template.AgendaBody', { + extend: 'Ext.XTemplate', + + requires: [], + + /** + * @cfg {Boolean} linkDatesToDayView + * True to link dates to the {@link Extensible.calendar.view.Day day view}. + */ + linkDatesToDayView: true, + + /** + * @cfg {Boolean} simpleList + *

If true, a simple list of events is displayed, else, an agenda-style list is displayed. + * Defaults to false.

+ */ + simpleList: false, + + /** + * @cfg {String} groupBy + *

Defines the grouping to be applied to the list of events. This property only has an effect if property + * {@link #simpleList} is true. Supported values are month, week and none. Any other + * values will disable grouping. Default value is none.

+ */ + groupBy: 'none', + + /** + * @cfg {String} dayDateFormat + * The date format for day's date in the list of events (defaults to 'D M j'). + */ + dayDateFormat: 'D M j', + /** + * @cfg {String} hourFormat + * The format for event start and end times (defaults to 'g:ia'). + */ + hourFormat: 'g:ia', + /** + * @cfg {String} allDayText + * Text used to display in times column for all-day events and for events that last the entire day. + */ + allDayText: 'All day', + /** + * @cfg {String} locationText + * Label used for the event location output. + */ + locationText: 'Location', + /** + * @cfg {String} webLinkText + * Label used for the web link output. + */ + webLinkText: 'Web Link', + /** + * @cfg {String} notesText + * Label used for the event notes output. + */ + notesText: 'Notes', + /** + * @cfg {String} noEventsText + * Text used where there are no events for the selected date range. + */ + noEventsText: 'There are no events for the selected date range.', + + /** + * @cfg {String} prevLinkText + * Text used for the previous link. + */ + prevLinkText: 'Previous', + + /** + * @cfg {String} nextLinkText + * Text used for the next link. + */ + nextLinkText: 'Next', + + /** + * @cfg {String} reminderTooltip + * Text used as tooltip for the reminder icon. + */ + reminderTooltip: 'Reminder is activated', + + /** + * @cfg {String} recurringTooltip + * Text used as tooltip for the reminder icon. + */ + recurringTooltip: 'Recurring event', + + /** + * @cfg {String} showEventDetails + * If true, events are displayed with all details, otherwise only a one-line summary is shown. + */ + showEventDetails: false, + /** + * @cfg {Integer} maxNotesLength + * The maximum number of characters shown for the notes text. + */ + maxNotesLength: 100, + /** + * @cfg {String} prevLinkSelector + * The class name applied to the previous link. + */ + prevLinkSelector: 'ext-cal-agenda-bd-prev-link', + /** + * @cfg {String} nextLinkSelector + * The class name applied to the previous link. + */ + nextLinkSelector: 'ext-cal-agenda-bd-next-link', + + + // private + constructor: function(config){ + + Ext.apply(this, config); + + // AgendaBody support two templates, an agenda list template and a simple list template. + if (this.simpleList){ + Extensible.calendar.template.AgendaBody.superclass.constructor.call(this, this.getTemplateForSimpleList()); + } else { + Extensible.calendar.template.AgendaBody.superclass.constructor.call(this, this.getTemplateForAgendaList()); + } + }, + + // private + applyTemplate : function(o){ + if (Ext.getVersion().isLessThan('4.1')) { + return Extensible.calendar.template.AgendaBody.superclass.applyTemplate.call(this, o); + } + else { + return this.applyOut(o, []).join(''); + } + }, + + /** + * Returns the template used for the agenda list. + * @return {Array} A array of strings making up the template. + */ + getTemplateForAgendaList: function() { + return [ + '', + '', + '', + '', + // '', + '', + '', // events is a MixedCollection + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '
{[Ext.Date.format(values.date, \"D M j\")]}', + '{[Ext.Date.format(values.date, this.dayDateFormat)]}', + '
{[this.getEventTimesMarkupForAgendaList(values, parent.date)]}', + '{[this.getTitleMarkup(values)]}', + '', + // Display event with all details + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '
', this.locationText, ':{[values.data[Extensible.calendar.data.EventMappings.Location.name]]}
', this.webLinkText, ':{[this.getWebLinkMarkup(values, true)]}
', this.notesText, ':{[this.getNotesMarkup(values)]}
', + '
', + '

', + this.noEventsText, + '
' + ]; + }, + + /** + * Returns the template used for the simple list. + * @return {Array} A array of strings making up the template. + */ + getTemplateForSimpleList: function() { + return [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', // events is a MixedCollection + '', + '', + '', + '{[this.getEventTimesMarkupForSimpleList(values, parent.date)]}', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '
{[this.getGroupHeaderMarkup(values)]}

', + '{[Ext.Date.format(values.date, this.dayDateFormat)]}', + '
', + '{[this.getTitleMarkup(values)]}', + '', + // Display event with all details + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '
', this.locationText, ':{[values.data[Extensible.calendar.data.EventMappings.Location.name]]}
', this.webLinkText, ':{[this.getWebLinkMarkup(values, true)]}
', this.notesText, ':{[this.getNotesMarkup(values)]}
', + '
', + '

', + this.noEventsText, + '
' + ]; + }, + + /** + * Returns event start and end times formatted for output on the agenda list. See also {@link #getEventTimesMarkupForSimpleList}. + * @param {Extensible.calendar.data.EventModel} evt + * @param {Date} dt The date for which to produce output. + * @return {String} + */ + getEventTimesMarkupForAgendaList: function(evt, dt) { + var result, + M = Extensible.calendar.data.EventMappings, + currentDt = Ext.Date.clearTime(dt), + startDt = Ext.Date.clearTime(evt.data[M.StartDate.name], true), + endDt = Ext.Date.clearTime(evt.data[M.EndDate.name], true), + startDtTime = evt.data[M.StartDate.name], + endDtTime = evt.data[M.EndDate.name]; + + // There are five cases to consider: + // Case Output example + // ---------------------------------------------------------------+--------------- + // 1) Event is all-day event All day + // 2) Event is not all-day event + // 2.1) Start time and end time are on the current day 8:00am - 11:00am + // 2.2) Start time on current date, end time on later date 8:00 >> + // 2.3) Start time on earlier date, end time on current date >> 11:00am + // 2.4) Start time on earlier date, end time on later day All day + if (evt.data[M.IsAllDay.name]) { + result = this.allDayText; // Case 1 + } else { + if (Extensible.Date.compare(currentDt, startDt) == 0) { + if (Extensible.Date.compare(currentDt, endDt) == 0) { + result = Ext.Date.format(startDtTime, this.hourFormat) + ' - ' + Ext.Date.format(endDtTime, this.hourFormat); // Case 2.1 + } else { + result = Ext.Date.format(startDtTime, this.hourFormat) + ' »'; // Case 2.2 + } + } else { + if (Extensible.Date.compare(currentDt, endDt) == 0) { + result = '» ' + Ext.Date.format(endDtTime, this.hourFormat); // Case 2.3 + } else { + result = this.allDayText; // Case 2.4 + } + } + } + return result; + }, + + /** + * Returns event start and end times formatted for output on the simple list. See also {@link #getEventTimesMarkupForAgendaList}. + * @param {Extensible.calendar.data.EventModel} evt + * @param {Date} dt The date for which to produce output. + * @return {String} + */ + getEventTimesMarkupForSimpleList: function(evt, dt) { + var result, + M = Extensible.calendar.data.EventMappings, + currentDt = Ext.Date.clearTime(dt), + startDt = Ext.Date.clearTime(evt.data[M.StartDate.name], true), + endDt = Ext.Date.clearTime(evt.data[M.EndDate.name], true), + startDtTime = evt.data[M.StartDate.name], + endDtTime = evt.data[M.EndDate.name], + startHourStr = '', + untilStr = '-', + endDtStr = '', + endHourStr = ''; + + // This function generates HTML output that contains the following information: + // - Event start hour + // - Event end date + // - Event end hour + // Note that the event start date is not part of the output because the start date is displayed once for + // all events on the same day. + // + // There are several cases to consider: + // 1) All-day event that starts and ends on the current day. + // 2) All-day event that starts on the current day and ends on a later day. + // 3) Non-all-day event that starts and ends on the current day. + // 4) Non-all-day event that starts on the current day and ends on a later day. + // + // Generated values for the four cases are: + // Evt start hour | Evt end date | Evt end hour + // 1) All day | | + // 2) All day | Mon May 18 | + // 3) 8:00am | 5:00pm | + // 4) 8:00am | Mon May 18 | 5:00pm + + if (evt.data[M.IsAllDay.name]) { + if (startDt.getTime() == endDt.getTime()) { + // Case 1 + startHourStr = this.allDayText; + untilStr = ''; + } else { + // Case 2 + startHourStr = this.allDayText; + endDtStr = Ext.Date.format(endDt, this.dayDateFormat); + } + } else { + if (startDt.getTime() == endDt.getTime()) { + // Case 3 + startHourStr = Ext.Date.format(startDtTime, this.hourFormat); + endDtStr = Ext.Date.format(endDtTime, this.hourFormat); + } else { + // Case 4 + startHourStr = Ext.Date.format(startDtTime, this.hourFormat); + endDtStr = Ext.Date.format(endDt, this.dayDateFormat); + endHourStr = Ext.Date.format(endDtTime, this.hourFormat); + } + } + + result = [ + '', startHourStr, '', untilStr, '', + '', endDtStr, '', endHourStr, '']; + return result.join(''); + }, + + /** + * Returns the markup for the event title. + * @param {Extensible.calendar.data.EventModel} evt + * @return {String} + */ + getTitleMarkup: function(evt) { + var result, + M = Extensible.calendar.data.EventMappings, + title = evt.data[M.Title.name]; + result = [ + '', + !title || title.length == 0 ? this.defaultEventTitleText : title, + this.getReminderFlagMarkup(evt), + this.getRecurrenceFlagMarkup(evt), + '' + ]; + if (evt.data[M.Location.name] && evt.data[M.Location.name] != '' && !this.showEventDetails) { + result.push( + ' - ', + evt.data[M.Location.name] + ); + } + return result.join(''); + }, + + /** + * Returns the markup for the reminder flag, if a reminder is active. Otherwise an empty string is returned. + * @param {Extensible.calendar.data.EventModel} evt + * @return {String} + */ + getReminderFlagMarkup: function(evt) { + var M = Extensible.calendar.data.EventMappings; + return evt.data[M.Reminder.name] && evt.data[M.Reminder.name] != '' ? ' ' : ''; + }, + + /** + * Returns the markup for the recurrence flag, if recurrence is active. Otherwise an empty string is returned. + * @param {Extensible.calendar.data.EventModel} evt + * @return {String} + */ + getRecurrenceFlagMarkup: function(evt) { + var M = Extensible.calendar.data.EventMappings; + return evt.data[M.RRule.name] && evt.data[M.RRule.name] != '' ? ' ' : ''; + }, + + /** + * Returns the markup for the web link. If no web link is defined, an empty string is returned. + * @param {Extensible.calendar.data.EventModel} evt + * @param {Boolean} removeProtocol If true the 'http://' string is removed from the web link. This can be useful + * to display the web link in a user friendly way. If the web link is missing the protocol string and this + * parameter is false, then the protocol string is prepended. Defaults to false. + * @return {String} + */ + getWebLinkMarkup: function(evt, removeProtocol) { + var M = Extensible.calendar.data.EventMappings, + l = evt.data[M.Url.name]; + if (l && l != "") { + if (l.indexOf('http://') == 0) { + l = l.substr(7); + } + if (removeProtocol) { + return l; + } else { + return 'http://' + l; + } + } else { + return ''; + } + }, + + /** + * Returns the markup for the event notes. If no event notes are defined, an empty string is returned. The notes + * are limited to the number of characters specified by configuration option {@link #maxNotesLength}. + * @param {Extensible.calendar.data.EventModel} evt + * @return {String} + */ + getNotesMarkup: function(evt) { + var M = Extensible.calendar.data.EventMappings, + n = evt.data[M.Notes.name]; + return n.length > this.maxNotesLength ? n.substring(0, this.maxNotesLength-3) + '...' : n; + }, + + + /** + * Returns the markup for an event group header. The type of group header returned depends on the configured + * event grouping (see {@link #groupBy}). For example: + * Monthly grouping: June 2012 + * Weekly grouping: Week 23: Mon Jun 3 - Sun Jun 10 + * No grouping: Empty string + * @param {Object} group + * @return {String} + */ + getGroupHeaderMarkup: function(group) { + var result; + + if (this.groupBy == 'month') { + result = [Ext.Date.format(group.startDt, "F Y")]; + } else if (this.groupBy == 'week') { + if (Ext.Date.clearTime(group.startDt, true).getTime() == Ext.Date.clearTime(group.endDt, true).getTime()) { + // This is a partical week with only one day left. Don't show date range, just current date. + result = ['Week ', group.weekNo, ': ', Ext.Date.format(group.startDt, this.dayDateFormat)]; + } else { + result = ['Week ', group.weekNo, ': ', Ext.Date.format(group.startDt, this.dayDateFormat), ' - ', Ext.Date.format(group.endDt, this.dayDateFormat)]; + } + } else { + result = ['']; + } + return result.join(''); + }, + + /** + * Returns true if passed event has notes, false otherwise. This is a small helper function for the template. + * @param {Extensible.calendar.data.EventModel} An event record. + * @return {Boolean} + */ + eventHasNotes: function(evt) { + var n = evt.data[Extensible.calendar.data.EventMappings.Notes.name]; + return n && n != ""; + }, + + /** + * Returns true if passed event has a location assigned, false otherwise. This is a small helper function for the template. + * @param {Extensible.calendar.data.EventModel} An event record. + * @return {Boolean} + */ + eventHasLocation: function(evt) { + var l = evt.data[Extensible.calendar.data.EventMappings.Location.name]; + return l && l != ""; + }, + + /** + * Returns true if passed event has a link assigned, false otherwise. This is a small helper function for the template. + * @param {Extensible.calendar.data.EventModel} An event record. + * @return {Boolean} + */ + eventHasLink: function(evt) { + var url = evt.data[Extensible.calendar.data.EventMappings.Url.name]; + return url && url != ""; + }, + + /** + * Returns true if group titles are to be displayed. This is a small helper function for the template. + * @return {Boolean} + */ + hasGroupTitle: function() { + return this.groupBy == 'month' || this.groupBy == 'week' ? true : false; + } + +}, +function() { + this.createAlias('apply', 'applyTemplate'); +}); \ No newline at end of file diff --git a/src/calendar/template/BoxLayout.js b/src/calendar/template/BoxLayout.js index d27c3aa4..54ce8363 100644 --- a/src/calendar/template/BoxLayout.js +++ b/src/calendar/template/BoxLayout.js @@ -100,7 +100,7 @@ Ext.define('Extensible.calendar.template.BoxLayout', { weeks = [[]], today = Extensible.Date.today(), dt = Ext.Date.clone(this.viewStart), - thisMonth = this.startDate.getMonth(); + thisMonth = Extensible.Date.getMonth(this.startDate); for (; w < this.weekCount || this.weekCount === -1; w++) { if(dt > this.viewEnd) { @@ -110,10 +110,10 @@ Ext.define('Extensible.calendar.template.BoxLayout', { for (var d = 0; d < this.dayCount; d++) { isToday = dt.getTime() === today.getTime(); - showMonth = first || (dt.getDate() === 1); - prevMonth = (dt.getMonth() < thisMonth) && this.weekCount === -1; - nextMonth = (dt.getMonth() > thisMonth) && this.weekCount === -1; - isWeekend = dt.getDay() % 6 === 0; + showMonth = first || (Extensible.Date.getDate(dt) === 1); + prevMonth = (Extensible.Date.getMonth(dt) < thisMonth) && this.weekCount === -1; + nextMonth = (Extensible.Date.getMonth(dt) > thisMonth) && this.weekCount === -1; + isWeekend = Extensible.Date.isWeekend(dt); if(dt.getDay() === 1) { // The ISO week format 'W' is relative to a Monday week start. If we diff --git a/src/calendar/util/WeekEventRenderer.js b/src/calendar/util/WeekEventRenderer.js index e2a49188..2d43c674 100644 --- a/src/calendar/util/WeekEventRenderer.js +++ b/src/calendar/util/WeekEventRenderer.js @@ -224,7 +224,7 @@ Ext.define('Extensible.calendar.util.WeekEventRenderer', { } // Move to the next date and restart the loop - currentDate = Extensible.Date.add(currentDate, {days: 1}); + currentDate = Ext.Date.clearTime(Extensible.Date.add(currentDate, {days: 1})); } } } diff --git a/src/calendar/view/AbstractCalendar.js b/src/calendar/view/AbstractCalendar.js index 1053729d..cc9b0ee2 100644 --- a/src/calendar/view/AbstractCalendar.js +++ b/src/calendar/view/AbstractCalendar.js @@ -9,7 +9,7 @@ */ Ext.define('Extensible.calendar.view.AbstractCalendar', { extend: 'Ext.Component', - + requires: [ 'Ext.CompositeElement', 'Extensible.calendar.form.EventDetails', @@ -684,6 +684,9 @@ Ext.define('Extensible.calendar.view.AbstractCalendar', { return; } Extensible.log('refresh (AbstractCalendar), reload = ' + reloadData); + if (this.isDestroyed) { + return; + } if (reloadData === true) { this.reloadStore(); @@ -742,7 +745,7 @@ Ext.define('Extensible.calendar.view.AbstractCalendar', { this.sortEventRecordsForDay(evts); this.prepareEventGrid(evts, w, d); } - currentDt = Extensible.Date.add(currentDt, {days: 1}); + currentDt = Ext.Date.clearTime(Extensible.Date.add(currentDt, {days: 1})); } } this.currentWeekCount = w; @@ -1335,8 +1338,9 @@ Ext.define('Extensible.calendar.view.AbstractCalendar', { case 0: case 1: me.viewStart = me.dayCount < 7 && !me.startDayIsStatic ? - start: Dt.add(start, {days: -offset, clearTime: true}); - me.viewEnd = Dt.add(me.viewStart, {days: me.dayCount || 7, seconds: -1}); + Ext.Date.clearTime(start) : Dt.add(start, {days: -offset, clearTime: true}); + me.viewEnd = Dt.add(me.viewStart, {days: me.dayCount || 7, clearTime: true}); + me.viewEnd = Dt.add(me.viewEnd, {seconds: -1}); return; case -1: @@ -1350,7 +1354,8 @@ Ext.define('Extensible.calendar.view.AbstractCalendar', { me.viewStart = Dt.add(start, {days: -offset, clearTime: true}); // start from current month start, not view start: - var end = Dt.add(start, {months: 1, seconds: -1}); + var end = Dt.add(start, {months: 1, clearTime: true}); + end = Dt.add(end, {seconds: -1}); // fill out to the end of the week: offset = me.startDay; @@ -1359,12 +1364,14 @@ Ext.define('Extensible.calendar.view.AbstractCalendar', { offset -= 7; } - me.viewEnd = Dt.add(end, {days: 6 - end.getDay() + offset}); + me.viewEnd = Dt.add(end, {days: 6 - end.getDay() + offset, clearTime: true}); return; default: me.viewStart = Dt.add(start, {days: -offset, clearTime: true}); - me.viewEnd = Dt.add(me.viewStart, {days: me.weekCount * 7, seconds: -1}); + me.viewEnd = Dt.add(me.viewStart, {days: me.weekCount * 7, clearTime: true}); + me.viewEnd = Dt.add(me.viewEnd, {seconds: -1}); + return; } }, @@ -1441,8 +1448,27 @@ Ext.define('Extensible.calendar.view.AbstractCalendar', { // remain sorted sequentially by start time. This seems more proper // but can make for a less visually-compact layout when there are // many such events mixed together closely on the calendar. - return a[M.StartDate.name].getTime() - b[M.StartDate.name].getTime(); + + // Events are sorted by three criteria: Start time, end time and + // calendar id. The calendar id is used as the third sort criteria + // to ensure that events are always ordered the same way. Without + // that third criteria, events that start at the same time and end at + // the same time would be ordered randomly. + var sortStartDate = a[M.StartDate.name].getTime() - b[M.StartDate.name].getTime() + if (sortStartDate){ + return sortStartDate; + } + var sortEndDate = b[M.EndDate.name].getTime() - a[M.EndDate.name].getTime(); //descending + if (sortEndDate){ + return sortEndDate; + } + var sortCalendar = a[M.CalendarId.name] - b[M.CalendarId.name];//ascending + if (sortCalendar){ + return sortCalendar; + } + return 0; } + }, this)); }, @@ -1558,14 +1584,29 @@ Ext.define('Extensible.calendar.view.AbstractCalendar', { Ext.each(operation.records, function(rec) { if (rec.dirty) { if (rec.phantom) { - rec.unjoin(this.eventStore); + this.store.remove(rec); } else { rec.reject(); } } }, this); - + + // Restore deleted records back to their original positions. + // This code was copied from ExtJS V4.2.2 Ext.data.Store, function rejectChanges(). In order to maintain + // backwards compatibility with version 4.0.7, this function cannot be called directly. + var recs = this.store.removed, + len = recs.length, + i = 0, rec; + + for (i = len-1; i >= 0; i--) { + rec = recs[i]; + this.store.insert(rec.removedFrom || 0, rec); + rec.reject(); + } + // Since removals are cached in a simple array we can simply reset it here. + this.store.removed.length = 0; + if (this.fireEvent('eventexception', this, response, operation) !== false) { this.notifyOnException(response, operation); } diff --git a/src/calendar/view/Agenda.js b/src/calendar/view/Agenda.js new file mode 100644 index 00000000..550f4daa --- /dev/null +++ b/src/calendar/view/Agenda.js @@ -0,0 +1,334 @@ +/** + * @class Extensible.calendar.view.Agenda + * @extends Ext.Container + * + *

This class is currently beta code and the API is still subject to change before the next release.

+ * + *

Agenda view display events as a chronologically sorted list. It supports two types of list:

+ * + *

1) Agenda lists: An agenda list is a list where for each day of the view period all events that are taking place + * on that day are listed. For example, an event that lasts seven days is listed seven times, once for each day. + * This view is very similar to the agenda view in Google calendar.

+ * + *

2) Simple lists: A simple list is a list where each event is listed once, independent of the duration of the + * event. This is suited for event calendars or to present the results of a search for events. Simple list mode is + * activated by setting property {@link #simpleList} to true.
+ * Additionally, simple lists support grouping of events by month and week. Grouping is enabled with property + * {@link #groupBy}. If grouping is enabled, each group of events starts with a group header displaying the + * month or week.

+ * + *

Agenda view supports CRUD operations on events, filtering of events based on calendar and a selectable date + * range. The view can be switched between a summary view and a details view.

+ * + *

The view is divided into two main sections: the {@link Extensible.calendar.view.AgendaHeader header} and the + * {@link Extensible.calendar.view.AgendaBody event list}. The header hosts a form and a toolbar that can be + * used to filter events, choose display options, apply action on events, etc. Both header and toolbar are + * easily configurable.

+ * + *

Unlike other calendar views, this view is not actually a subclass of {@link Extensible.calendar.view.AbstractCalendar AbstractCalendar}. + * Instead it is a {@link Ext.Container} subclass that internally creates and manages the layouts of + * a {@link Extensible.calendar.view.AgendaHeader AgendaHeader} and a {@link Extensible.calendar.view.AgendaBody AgendaBody}. + * As such this class accepts any config values that are valid for AgendaHeaderView and AgendaBodyView and passes those through + * to the contained views. It also supports the interface required of any calendar view and in turn calls methods + * on the contained views as necessary.

+ * + * @author Gabriel Sidler, sidler@teamup.com + * @constructor + * @param {Object} config The config object + */ +Ext.define('Extensible.calendar.view.Agenda', { + extend: 'Ext.Container', + alias: 'widget.extensible.agendaview', + + requires: [ + 'Extensible.calendar.view.AbstractCalendar', + 'Extensible.calendar.view.AgendaHeader', + 'Extensible.calendar.view.AgendaBody' + ], + + /** + * @cfg {String} hideMode + *

How this component should be hidden. Supported values are 'visibility' + * (css visibility), 'offsets' (negative offset position) and 'display' + * (css display).

+ *

Note: For calendar views the default is 'offsets' rather than the Ext JS default of + * 'display' in order to preserve scroll position after hiding/showing a scrollable view like Day or Week.

+ */ + hideMode: 'offsets', + + /** + * @cfg {Boolean} simpleList + *

If true, a simple list of events is displayed, else, an agenda-style list is displayed. See the introduction + * of this class for more details. Defaults to false.

+ */ + simpleList: false, + + /** + * @cfg {String} groupBy + *

Defines the grouping to be applied to the list of events. This property only has an effect if property + * {@link #simpleList} is true. Supported values are month, week and none. Any other + * values will disable grouping. Default value is none.

+ */ + groupBy: 'none', + + /** + * @property ownerCalendarPanel + * @type Extensible.calendar.CalendarPanel + * If this view is hosted inside a {@link Extensible.calendar.CalendarPanel} this property will reference + * it. If the view was created directly outside of a CalendarPanel this property will be undefined. Read-only. + */ + + // private + layout: { + type: 'vbox', + align: 'stretch' + }, + + // private + isAgendaView: true, + + // private + initComponent : function(){ + + // Pass on initial configuration to sub-components + var cfg = Ext.apply({}, this.initialConfig); + + var header = Ext.applyIf({ + xtype: 'extensible.agendaheaderview', + id: this.id+'-hd', + stateful: this.stateful, + stateId: this.id+'-hd', + ownerCalendarView: this, + listeners: { + formchange: {fn: this.onFormChange, scope: this}, + addevent: {fn: this.onAddEvent, scope: this} + } + }, cfg); + + var body = Ext.applyIf({ + xtype: 'extensible.agendabodyview', + id: this.id+'-bd', + simpleList: this.simpleList, + groupBy: this.groupBy, + ownerCalendarPanel: this.ownerCalendarPanel, + ownerCalendarView: this + }, cfg); + + this.items = [header, body]; + this.addCls('ext-cal-agenda ext-cal-ct'); + + this.callParent(arguments); + }, + + // private + afterRender : function(){ + var filterConfig; + + this.callParent(arguments); + + this.header = Ext.getCmp(this.id+'-hd'); + this.body = Ext.getCmp(this.id+'-bd'); + + this.body.on('eventsrendered', this.forceSize, this); + this.on('resize', this.onResize, this); + + filterConfig = this.header.getForm().getFieldValues(); + this.body.setFilterConfig(filterConfig); + }, + + // private + refresh : function(){ + Extensible.log('refresh (AgendaView)'); + if (this.isDestroyed) { + return; + } + // this.header.refresh(); + this.body.refresh(); + }, + + + // private + onFormChange: function(header, form, field) { + var filterConfig = form.getFieldValues(); + + this.body.setFilterConfig(filterConfig); + if (field.getId() == this.id + '-hd-showdetails') { + // Refresh the header form without reloading the events + this.body.refresh(false); + } else { + // Reset start date. This will trigger a reload of the events with the changed filter settings. + this.setStartDate(this.getStartDate()); + } + }, + + // private + onAddEvent: function(hd, bt) { + var M = Extensible.calendar.data.EventMappings, + D = Ext.Date, + data = {}, + // now = new Date(), + now = this.body.getStartDate(), + today = D.clearTime(now, true); + + data[M.StartDate.name] = D.add(today, D.HOUR, now.getHours() + 1); + data[M.EndDate.name] = D.add(today, D.HOUR, now.getHours() + 2); + data[M.IsAllDay.name] = true; + + this.body.showEventEditor(data, bt.getEl()); + }, + + // private + forceSize: function(){ + // The defer call is mainly for good ol' IE, but it doesn't hurt in + // general to make sure that the window resize is good and done first + // so that we can properly calculate sizes. + /* + Ext.defer(function(){ + var ct = this.el.up('.x-panel-body'), + hd = this.el.down('.ext-cal-agenda-header'), + h = ct.getHeight() - hd.getHeight(); + + this.el.down('.ext-cal-body-ct').setHeight(h-1); + }, 1, this); + */ + }, + + // private + onResize : function(){ + this.forceSize(); + Ext.defer(this.refresh, Ext.isIE ? 1 : 0, this); //IE needs the defer + }, + + /* + * We have to "relay" this Component method so that the hidden + * state will be properly reflected when the views' active state changes + */ + doHide: function(){ + this.header.doHide.apply(this, arguments); + this.body.doHide.apply(this, arguments); + }, + + /** + * Returns the start and end boundary dates currently displayed in the view. The method + * returns an object literal that contains the following properties: + * For example:

+     var bounds = view.getViewBounds();
+     alert('Start: '+bounds.start);
+     alert('End: '+bounds.end);
+     
+ * @return {Object} An object literal containing the start and end values + */ + getViewBounds : function(){ + return this.body.getViewBounds(); + }, + + /** + * Returns the start date of the view, as set by {@link #setStartDate}. Note that this may not + * be the first date displayed in the rendered calendar -- to get the start and end dates displayed + * to the user use {@link #getViewBounds}. + * @return {Date} The start date + */ + getStartDate : function(){ + return this.body.getStartDate(); + }, + + /** + * Sets the start date used to calculate the view boundaries to display. The displayed view will be the + * earliest and latest dates that match the view requirements and contain the date passed to this function. + * @param {Date} dt The date used to calculate the new view boundaries + */ + setStartDate: function(dt){ + this.body.setStartDate(dt, true); + }, + + // private + renderItems: function(){ + this.body.renderItems(); + }, + + /** + * Returns true if the view is currently displaying today's date, else false. + * @return {Boolean} True or false + */ + isToday : function(){ + return this.body.isToday(); + }, + + /** + * Updates the view to contain the passed date + * @param {Date} dt The date to display + * @return {Date} The new view start date + */ + moveTo : function(dt){ + var newDt = this.body.moveTo(dt, true); + this.header.moveTo(newDt); + return newDt; + }, + + /** + * Updates the view to the next consecutive date(s) + * @return {Date} The new view start date + */ + moveNext : function(){ + var newDt = this.body.moveNext(true); + this.header.moveTo(newDt); + return newDt; + }, + + /** + * Updates the view to the previous consecutive date(s) + * @return {Date} The new view start date + */ + movePrev : function(){ + var newDt = this.body.movePrev(true); + this.header.moveTo(newDt); + return newDt; + }, + + /** + * Shifts the view by the passed number of days relative to the currently set date + * @param {Number} value The number of days (positive or negative) by which to shift the view + * @return {Date} The new view start date + */ + moveDays : function(value){ + var newDt = this.body.moveDays(value, true); + this.header.moveTo(newDt); + return newDt; + }, + + /** + * Updates the view to show today + * @return {Date} Today's date + */ + moveToday : function(){ + var newDt = this.body.moveToday(true); + this.header.moveTo(newDt); + return newDt; + }, + + /** + * Show the currently configured event editor view (by default the shared instance of + * {@link Extensible.calendar.form.EventWindow EventEditWindow}). + * @param {Extensible.calendar.data.EventModel} rec The event record + * @param {Ext.Element/HTMLNode} animateTarget The reference element that is being edited. By default this is + * used as the target for animating the editor window opening and closing. If this method is being overridden to + * supply a custom editor this parameter can be ignored if it does not apply. + * @return {Extensible.calendar.view.Day} this + */ + showEventEditor : function(rec, animateTarget){ + return Extensible.calendar.view.AbstractCalendar.prototype.showEventEditor.apply(this, arguments); + }, + + /** + * Dismiss the currently configured event editor view (by default the shared instance of + * {@link Extensible.calendar.form.EventWindow EventEditWindow}, which will be hidden). + * @param {String} dismissMethod (optional) The method name to call on the editor that will dismiss it + * (defaults to 'hide' which will be called on the default editor window) + * @return {Extensible.calendar.view.Day} this + */ + dismissEventEditor : function(dismissMethod){ + return Extensible.calendar.view.AbstractCalendar.prototype.dismissEventEditor.apply(this, arguments); + } +}); \ No newline at end of file diff --git a/src/calendar/view/AgendaBody.js b/src/calendar/view/AgendaBody.js new file mode 100644 index 00000000..d09bda94 --- /dev/null +++ b/src/calendar/view/AgendaBody.js @@ -0,0 +1,673 @@ +/** + * @class Extensible.calendar.view.AgendaBody + * @extends Extensible.calendar.view.AbstractCalendar + * + *

This class is currently beta code and the API is still subject to change before the next release.

+ * + *

This is the body area view within the agenda view. Normally you should not need to use this class directly + * -- instead you should use {@link Extensible.calendar.view.Agenda Agenda} view which aggregates this class and the + * {@link Extensible.calendar.view.AgendaHeader AgendaHeader} view into a single unified view + * presented by {@link Extensible.calendar.CalendarPanel CalendarPanel}.

+ * + *

This component displays the list of events and supports CRUD operations on events. The layout of the events + * is controlled by template {@link Extensible.calendar.template.AgendaBody}.

+ * + * @author Gabriel Sidler, sidler@teamup.com + * @constructor + * @param {Object} config The config object + */ +Ext.define('Extensible.calendar.view.AgendaBody', { + extend: 'Extensible.calendar.view.AbstractCalendar', + alias: 'widget.extensible.agendabodyview', + + requires: [ + 'Ext.XTemplate', + 'Extensible.calendar.template.AgendaBody' + ], + + /** + * @cfg {Boolean} linkDatesToDayView + * True to link dates to the {@link Extensible.calendar.view.Day day view}. + */ + linkDatesToDayView: true, + + /** + * @cfg {String} dateRangeDefault + * Defines the default value for the date range. Supported values are: day, week, month, + * 3months and year. Defaults to month. + */ + dateRangeDefault: 'month', + + /** + * @cfg {Boolean} simpleList + *

If true, a simple list of events is displayed, else, an agenda-style list is displayed. + * Defaults to false.

+ */ + simpleList: false, + + /** + * @cfg {String} groupBy + *

Defines the grouping to be applied to the list of events. This property only has an effect if property + * {@link #simpleList} is true. Supported values are month, week and none. Any other + * values will disable grouping. Default value is none.

+ */ + groupBy: 'none', + + /** + * @property ownerCalendarView + * @type {Ext.Container} + * A reference to the calendar view that hosts this view. Read-only. + */ + + // private properties + /* + * Private + * @property filterConfig + * @type {Object} + * An object that contains key/value pairs to be used as the filtering configuration when loading events. + * Use method {@link #setFilterConfig} to set this property. This ensures that the new filter configuration is put + * into operation immediately. + */ + dayLinkSelector: '.ext-cal-day-link', + dayLinkIdDelimiter: 'ext-cal-day-', + prevLinkSelector: 'ext-cal-agenda-bd-prev-link', + nextLinkSelector: 'ext-cal-agenda-bd-next-link', + flex: 1, + autoScroll: true, + padding: '10 0 10 0', + + // private + initComponent : function(){ + + this.filterConfig = { + period: this.dateRangeDefault, + groupby: this.groupBy, + details: false + }; + + this.addEvents({ + /** + * @event dayclick + * Fires after the user clicks on a day date + * @param {Extensible.calendar.view.AgendaBody} this + * @param {Date} dt The date that was clicked on. + */ + dayclick: true + }); + + this.callParent(arguments); + }, + + // Private + renderTemplate : function(){ + var templateParams = this.getTemplateParams(); + + if (this.simpleList){ + templateParams.groups = this.getTemplateEventDataForSimpleList(); + } else { + templateParams.days = this.getTemplateEventDataForAgenda(); + } + if(this.tpl){ + this.tpl.overwrite(this.el, templateParams); + this.lastRenderStart = Ext.Date.clone(this.viewStart); + this.lastRenderEnd = Ext.Date.clone(this.viewEnd); + } + }, + + /** + *

Returns the template event data for rendering events as a simple list (property {@link #simpleList} is true). + * Optionally, the events can be grouped by month or week. The grouping is controlled by parameter {@link #groupBy}.

+ * + *

Returns an array of group objects containing an array of day objects containing and array of events. + * If grouping is disabled, then the top-level array contains only one group object. The following illustrates the + * returned data structure.

+ *

+[
+    group1,
+    group2,
+    {
+        startDt: ,
+        endDt: ,
+        weekNo: ,   // Exists only if grouped by week
+        weekYear: , // Exists only if grouped by week
+        days: [
+            day1,
+            day2,
+            {
+                date: date,
+                events: [
+                    event1,
+                    event2,
+                    ....
+                ]
+            },
+            day3,
+            ....
+        ]
+    },
+    group3,
+    ....
+]
+     * 
+ * + * @return {Array} + */ + getTemplateEventDataForSimpleList : function(){ + var M = Extensible.calendar.data.EventMappings, + events, + event, + groups = {}, + groupsArray = [], + daysArray, + viewBounds = this.getViewBounds(), + startDtView = viewBounds.start, + endDtView = viewBounds.end, + startDtGroup, endDtGroup, + startDtEvent, endDtEvent, + Dt = Extensible.Date, + group, + groupKey, + dayKey, + weekInfo; + + // Loop over all events of within view period. + events = this.getEvents(); + for (var i=0; i endDtView.getTime()) { + endDtGroup = endDtView; + } + } else if (this.groupBy == 'week') { + // End date is the end of the current week or the end date of the view, whichever is earlier. + // Take into consideration that week start day is configurable. + dayOfWeek = startDtGroup.getDay(); + endDtGroup = Ext.Date.add(Ext.Date.add(startDtGroup, Ext.Date.DAY, (6 - dayOfWeek + this.startDay) % 7 + 1), Ext.Date.SECOND, -1); + if (endDtGroup.getTime() > endDtView.getTime()) { + endDtGroup = endDtView; + } + } else { + // There is only one group, therefore end date of group is end date of view + endDtGroup = endDtView; + } + + // Check if we have reached the end of the viewing period + if (startDtGroup.getTime() > endDtEvent.getTime() || startDtGroup.getTime() > endDtView.getTime()) { + break; + } + + // Create group key. The group key is used to find a group in the list of groups. The + // format of the group key depends on the configured grouping. + if (this.groupBy == 'month') { + groupKey = Ext.Date.format(startDtGroup, 'my'); + } else if (this.groupBy == 'week') { + weekInfo = this.getWeekInfo(Ext.Date.clearTime(endDtGroup)); + groupKey = weekInfo.weekNo + '_' + weekInfo.weekYear; + } else { + groupKey = 'defaultKey'; + } + + // Create a day key + dayKey = Ext.Date.format(startDtEvent, 'dmy'); + + // Check if an array representing the current group already exists + group = groups[groupKey]; + if (typeof group == 'undefined') { + group = { + startDt: startDtGroup, + endDt: endDtGroup, + days: {} + }; + if (this.groupBy == 'week') { + group.weekNo = weekInfo.weekNo; + group.weekYear = weekInfo.weekYear; + } + groups[groupKey] = group; + } + + // Add event to list of events for the day on which the event starts. + day = group.days[dayKey]; + if (typeof day == 'undefined') { + day = { + date: startDtEvent, + events: Ext.create(Ext.util.MixedCollection) // Use a MixedCollection here such that we can use the existing event sorting function that works with MixedCollections + }; + group.days[dayKey] = day; + } + day.events.add(event.data[M.EventId.name], event); + } + } + + // Sort events within days, days within groups and groups among themselves. To be able to sort, the groups and days + // objects are converted to arrays. + for (groupKey in groups) { + group = groups[groupKey]; + + daysArray = []; + for (dayKey in group.days) { + var day = group.days[dayKey]; + this.sortEventRecordsForDay(day.events); + daysArray.push(day); + } + // Sort days within group + daysArray.sort(function(a, b) { + return Dt.compare(b.date, a.date); + }); + group.days = daysArray; + groupsArray.push(group); + } + // Sort groups + groupsArray.sort(function(a, b) { + return Dt.compare(b.startDt, a.startDt); + }); + + return groupsArray; + }, + + /** + *

Returns the template event data for rendering events in agenda style (property {@link #simpleList} is false).

+ * + *

Returns an array of day objects containing an array of events. The following illustrates the returned data structure.

+ *

+[
+    day1,
+    day2,
+    {
+        date: date,
+        events: [
+            event1,
+            event2,
+            ....
+        ]
+    },
+    day3,
+    ....
+]
+    * 
+ * + * @return {Array} + */ + getTemplateEventDataForAgenda : function(){ + var M = Extensible.calendar.data.EventMappings, + events, + event, + days = {}, + daysArray = [], + viewBounds = this.getViewBounds(), + startDtView = viewBounds.start, + endDtView = viewBounds.end, + startDtEvent, endDtEvent, + currDt, + Dt = Extensible.Date; + + // Loop over all events within view period. Single-day events produce one item per event. + // Multi-day events produce multiple items, one for each day. + events = this.getEvents(); + for (var i=0; i 0 ? currDt = startDtView : currDt = startDtEvent; + + // Loop over each day the event spans and that is within the view period. + while (Dt.compare(currDt, endDtEvent) >= 0 && Dt.compare(currDt, endDtView) >= 0) { + var day; + + // Check if already a day record exists for the current day + day = days[Ext.Date.format(currDt, 'dmy')]; + if (typeof day == 'undefined') { + day = { + date: currDt, + events: Ext.create(Ext.util.MixedCollection) // Use a MixedCollection here such that we can use the existing event sorting function that works with MixedCollections + }; + days[Ext.Date.format(currDt, 'dmy')] = day; + } + day.events.add(event.data[M.EventId.name], event); + + currDt = Ext.Date.add(currDt, Ext.Date.DAY, 1); + } + } + + // Convert days from object to array and sort events within a day + for (date in days) { + this.sortEventRecordsForDay(days[date].events); + daysArray.push(days[date]); + } + // Sort days + daysArray.sort(function(a, b) { + return Dt.compare(b.date, a.date); + }); + + return daysArray; + }, + + /** + * Returns a list of events for the current view period. Attributes needed for processing the + * template are applied to the events (event colors, custom classes). + * + * @return {Ext.util.MixedCollection} Returns a collection of events objects of class + * Extensible.calendar.data.EventModel. + */ + getEvents : function(){ + var M = Extensible.calendar.data.EventMappings, + CM = Extensible.calendar.data.CalendarMappings, + events, + extraClasses, + colorCls = 'x-cal-default', + + events = this.store.queryBy(function(rec){ + return this.isEventVisible(rec.data); + }, this); + + // Loop over all events of view period and apply additional attributes to events that are needed for output. + for (var i=0; iFor a given date, returns the number of the week and the year to which the week belongs.

+ *

In different parts of the world weeks are numbered differently. This function covers the + * three major conventions. The convention used is determined by the configuration of the first + * day of the week.

+ *
+     * First day of week  Convention                                     Region
+     * Sunday             Week cont. Jan 1 is first week of year         USA, Canada, Mexico
+     * Monday             Week cont. first Thur is first week of year    Most of Europe (ISO 8601 standard)
+     * Saturday           Week cont. Jan 1 is first week of year         Most of the Middle East
+     * 
+ * + *

For more information see + * http://en.wikipedia.org/wiki/Week_number#Week_numbering and + * + * http://www.pjh2.de/datetime/weeknumber/wnd.php?l=en#Legend.

+ * + * @param {Date} date The date for which the week information is calculated. + * @return {Object} An object literal with two attributes: weekNo and weekYear. + */ + getWeekInfo : function(date){ + var weekNo, + weekYear, + oneJan; + + // Determine week number + if (this.startDay == 0) { + // Week starts on Sunday + // Code from http://javascript.about.com/library/blweekyear.htm + oneJan = new Date(date.getFullYear(), 0, 1); + weekNo = Math.ceil((((date.getTime() - oneJan.getTime()) / 86400000) + oneJan.getDay() + 1) / 7); + } else if (this.startDay == 6) { + // Week starts on Saturday + oneJan = new Date(date.getFullYear(), 0, 1); + weekNo = Math.ceil((((date.getTime() - oneJan.getTime()) / 86400000) + oneJan.getDay() + 1) / 7); + } else { + // Week starts on Monday + weekNo = parseInt(Ext.Date.format(date, 'W'), 10) + } + + // Determine year to which week belongs. + if (date.getMonth() == 11 && weekNo == 1) { + // Date is at the end of December but week belongs to next year. + weekYear = date.getFullYear() + 1; + } else if (date.getMonth() == 0 && weekNo > 50) { + // Date is at the beginning of January but week belongs to previous year. + weekYear = date.getFullYear() - 1; + } else { + weekYear = date.getFullYear(); + } + + return { + weekNo: weekNo, + weekYear: weekYear + } + }, + + + // private + afterRender : function(){ + if(!this.tpl){ + this.tpl = Ext.create('Extensible.calendar.template.AgendaBody', { + id: this.id, + simpleList: this.simpleList, + groupBy: this.groupBy, + linkDatesToDayView: this.linkDatesToDayView, + defaultEventTitleText: this.defaultEventTitleText, + prevLinkSelector: this.prevLinkSelector, + nextLinkSelector: this.nextLinkSelector + }); + this.tpl.compile(); + } + this.addCls('ext-cal-agenda-bd ext-cal-ct'); + + this.callParent(arguments); + }, + + /** + * Returns an object containing all key/value params to be passed when loading the event store. + * Override this function if you need to pass additional parameters when loading the store. + * @return {Object} An object containing all params to be sent when loading the event store + */ + getStoreParams : function(){ + // This is needed if you require the default start and end dates to be included + var params = this.getStoreDateParams(); + + // Apply filter settings from the header form + Ext.applyIf(params, this.filterConfig); + + // Here is where you can add additional custom params, e.g.: + // params.now = Ext.Date.format(new Date(), this.dateParamFormat); + // params.foo = 'bar'; + // params.number = 123; + + return params; + }, + + // private + refresh : function(reloadData){ + Extensible.log('refresh (AgendaView)'); + if (this.isDestroyed) { + return; + } + this.callParent(arguments); + }, + + /** + * This method is here to fulfill the interface of {@link Extensible.view.AbstractCalendar}. It does not + * do anything except to confirm that events have been rendered. For this view, events are rendered by method + * {@link #renderTemplate}. + */ + renderItems : function(){ + this.fireEvent('eventsrendered', this); + }, + + /** + * Sets the filter configuration to be used when calculating view bounds and loading events. + * @param {Object} filterConfig An object of key/value pairs representing filter conditions. + */ + setFilterConfig: function(filterConfig) { + this.filterConfig = filterConfig; + this.tpl.showEventDetails = this.filterConfig.details ? true: false; + this.groupBy = this.filterConfig.groupby; + this.tpl.groupBy = this.filterConfig.groupby; + }, + + /** + * Helper function that converts a string expressing a date period into an object that can be used as + * parameter for the {@link Extensible.Date#add} function. + * @param {String} period Supported values are: day, week, month, 3months and + * year. If an unknown value is passed for period, then months is used. + * @param {Boolean} subtract If true, then the return object specifies a subtraction operation instead of an + * addition operation. Defaults to false. + */ + getDateAddParam: function(period, subtract) { + subtract = subtract || false; + + if (period == 'day') { + return subtract ? {days: -1} : {days: 1}; + } else if (period == 'week') { + return subtract ? {days: -7} : {days: 7}; + } else if (period == '3months') { + return subtract ? {months: -3} : {months: 3}; + } else if (period == 'year') { + return subtract ? {years: -1} : {years: 1}; + } else { + return subtract ? {months: -1} : {months: 1}; + } + }, + + // private + setViewBounds : function(startDate){ + var me = this, + Dt = Extensible.Date, + start = startDate || me.startDate, + period = me.filterConfig.period || this.dateRangeDefault, + addParam; + + addParam = this.getDateAddParam(period, false); + addParam.seconds = -1; + + me.viewStart = Dt.add(start, {days: 0, clearTime: true}); + me.viewEnd = Dt.add(me.viewStart, addParam); + }, + + // private + getDayEl : function(dt){ + return Ext.get(this.getDayId(dt)); + }, + + // private + getDayId : function(dt){ + if(Ext.isDate(dt)){ + dt = Ext.Date.format(dt, 'Ymd'); + } + return this.id + this.dayElIdDelimiter + dt; + }, + + /** + * Moves the view one period forward. + * @return {Date} The new view start date + */ + moveNext : function(/*private*/reload){ + var me = this, + period = me.filterConfig.period || this.dateRangeDefault, + addParam; + addParam = this.getDateAddParam(period); + return this.moveTo(Extensible.Date.add(this.viewStart, addParam), reload); + }, + + /** + * Moves the view one day backwards. + * @return {Date} The new view start date + */ + movePrev : function(/*private*/reload){ + var me = this, + period = me.filterConfig.period || this.dateRangeDefault, + addParam; + addParam = this.getDateAddParam(period, true); + return this.moveTo(Extensible.Date.add(this.viewStart, addParam), reload); + }, + + /** + * Returns true if the view is currently displaying today's date, else false. + * @return {Boolean} True or false + */ + isToday : function(){ + var today = Ext.Date.clearTime(new Date()).getTime(); + return this.viewStart.getTime() == today; + }, + + // private + onClick : function(e, t){ + var el; + + // Handle click on an existing event + if(Extensible.calendar.view.AgendaBody.superclass.onClick.apply(this, arguments)){ + // The superclass handled the click already so exit + return; + } + + // Handle click on a date. Jump to day view if active. + if(el = e.getTarget(this.dayLinkSelector, 3)){ + var dt = el.id.split(this.dayLinkIdDelimiter)[1]; + this.fireEvent('dayclick', this, Ext.Date.parseDate(dt, 'Ymd')); + } + + // Handle click on next or previous links + // ext-cal-bd-prev-link + if(el = e.getTarget('.' + this.prevLinkSelector, 3)){ + this.ownerCalendarView.movePrev(true); + } + if(el = e.getTarget('.' + this.nextLinkSelector, 3)){ + this.ownerCalendarView.moveNext(true); + } + + }, + + // inherited docs + isActiveView: function() { + var calendarPanel = this.ownerCalendarPanel, + calendarView = this.ownerCalendarView; + return (calendarPanel && calendarView && calendarPanel.getActiveView().id === calendarView.id); + } + +}); \ No newline at end of file diff --git a/src/calendar/view/AgendaHeader.js b/src/calendar/view/AgendaHeader.js new file mode 100644 index 00000000..b9724b93 --- /dev/null +++ b/src/calendar/view/AgendaHeader.js @@ -0,0 +1,479 @@ +/** + * @class Extensible.calendar.view.AgendaHeader + * @extends Ext.form.Panel + * + *

This class is currently beta code and the API is still subject to change before the next release.

+ * + *

This is the header area container within the {@link Extensible.calendar.view.Agenda Agenda} view. Normally you should + * not need to use this class directly -- instead you should use {@link Extensible.calendar.view.Agenda Agenda} view which + * aggregates this class and the {@link Extensible.calendar.view.AgendaBody AgendaBody} view into a single unified view + * presented by {@link Extensible.calendar.CalendarPanel CalendarPanel}.

+ * + *

This header consists of a form and a toolbar. Both can easily be extended or hidden. The header form is intended + * to host filter and display configuration settings while the toolbar is useful to offers actions that can be applied + * to the list of events, for example to add events or print events.

+ * + *

To modify or hide the form and the toolbar, override functions {@link #getFormConfig} and {@link #getToolbarConfig}. + * The form field values will be submitted automatically as parameters of requests to load the event store. They can + * be used on the backend to select or filter events.

+ * + * @author Gabriel Sidler, sidler@teamup.com + * @constructor + * @param {Object} config The config object + */ +Ext.define('Extensible.calendar.view.AgendaHeader', { + extend: 'Ext.form.Panel', + alias: 'widget.extensible.agendaheaderview', + + requires: [ + 'Ext.form.ComboBox', + 'Ext.Button', + 'Ext.data.Store', + 'Ext.tip.QuickTipManager' + ], + + /** + * @property ownerCalendarView + * @type Ext.Container + * A reference to the calendar view that hosts this view. Read-only. + */ + + /** + * @cfg {Boolean} simpleList + *

If true, a simple list of events is displayed, else, an agenda-style list is displayed. + * Defaults to false.

+ */ + simpleList: false, + + /** + * @cfg {Boolean} readOnly + * True to prevent the view from providing CRUD capabilities, false to enable CRUD (the default). + */ + + /** + * @cfg {String} dateRangeOneDay + * The text used for date range option one day. + */ + dateRangeOneDay: 'One day', + + /** + * @cfg {String} dateRangeOneWeek + * The text used for date range option one week. + */ + dateRangeOneWeek: 'One week', + + /** + * @cfg {String} dateRangeOneMonth + * The text used for date range option one month. + */ + dateRangeOneMonth: 'One month', + + /** + * @cfg {String} dateRangeThreeMonths + * The text used for date range option 3 months. + */ + dateRangeThreeMonths: 'Three months', + + /** + * @cfg {String} dateRangeOneYear + * The text used for date range option one year. + */ + dateRangeOneYear: 'One year', + + /** + * @cfg {String} dateRangeText + * The label text used for the date range field. + */ + dateRangeText: 'Date range', + + /** + * @cfg {String} groupByMonths + * The text used for group by option Month. + */ + groupByMonths: 'Month', + + /** + * @cfg {String} groupByWeek + * The text used for group by option Week. + */ + groupByWeek: 'Week', + + /** + * @cfg {String} groupByNone + * The text used for group by option None. + */ + groupByNone: 'None', + + /** + * @cfg {String} groupByText + * The label text used for the group by field. + */ + groupByText: 'Group by', + + /** + * @cfg {String} showDetailsText + * The label text used for the details field. + */ + showDetailsText: 'Show details', + + /** + * @cfg {String} addBtnText + * The caption used for the add button. + */ + addBtnText: 'Add event', + + /** + * @cfg {String} resetBtnText + * The caption used for the reset button. + */ + resetBtnText: 'Reset', + + /** + * @cfg {String} dateRangeDefault + * Defines the default value for the date range input field. Defaults to month. See + * {@link #getDateRangeOptions} for a list of supported date range default values. + */ + dateRangeDefault: 'month', + + /** + * @cfg {String} groupBy + * Defines the default value for the groupby input field. Defaults to none. See + * {@link #getGroupByOptions} for a list of supported default values. + */ + groupBy: 'none', + + /** + * @cfg {Boolean} showDetailsDefault + * Defines the default value for the checkbox to show details. Defaults to false. + */ + showDetailsDefault: false, + + // private configs + cls: 'ext-cal-agenda-hd', + preventHeader: true, + autoHeight: true, + border: 0, + defaults: { + labelWidth: 100 + }, + + + // private + initComponent : function(){ + var tbItems = this.getToolbarConfig(); + + this.dateRangeOptions = this.getDateRangeOptions(); + this.groupByOptions = this.getGroupByOptions(); + + this.items = this.getFormConfig(); + if (this.items.length == 0) { + this.bodyStyle = {padding: '0px', border: 0}; + } else { + this.bodyStyle = {padding: '10px 10px 5px 10px'}; + } + + if (tbItems.length > 0) { + this.dockedItems = [{ + xtype: 'toolbar', + dock: 'bottom', + ui: 'default', + cls: 'ext-cal-agenda-hd-tb', + items: tbItems + }]; + } + + if (this.items.length == 0 && tbItems.length == 0) { + this.style = {borderBottom: '0px'}; + } + + this.callParent(arguments); + + this.addEvents({ + /** + * @event formchange + * Fires after the filter form changes. + * @param {Extensible.calendar.view.AgendaHeader} this + * @param {Ext.form.Basic} form The filter form. + * @param {Ext.form.field.Field} field Form field that changed. + * @param {Object} newValue New form field value. + * @param {Object} oldValue Old form field value. + * @param {Object} eOpts The options object passed to {@link Ext.util.Observable.addListener}. + */ + formchange: true, + + /** + * @event addevent + * Fires after the user clicks the add event button. + * @param {Extensible.calendar.view.AgendaHeader} this + * @param {Ext.button.Button} button The button clicked. + * @param {Event} event + * @param {Object} eOpts The options object passed to {@link Ext.util.Observable.addListener}. + */ + addevent: true + }); + + }, + + /** + *

This function is called by this form panel to obtain the definition of form fields. Override this function to + * modify the form fields displayed by this panel.

+ * @return {Array} An array of Object + */ + getFormConfig: function() { + var formItems = { + xtype: 'fieldcontainer', + labelWidth: 100, + height: 45, + fieldDefaults: { + // padding: 20, + labelAlign: 'top', + width: 150, + margins: '0 20 0 0' + }, + layout: 'hbox', + items: [{ + xtype: 'combo', + id: this.id+'-daterange', + mode: 'local', + value: this.dateRangeDefault, + triggerAction: 'all', + forceSelection: true, + editable: false, + fieldLabel: this.dateRangeText, + name: 'period', + displayField: 'name', + valueField: 'value', + queryMode: 'local', + store: Ext.create('Ext.data.Store', { + fields : ['name', 'value'], + data : this.dateRangeOptions + }), + // This fixes a bug that a blank item is not properly supported. See Sencha forum and source of Ext.view.BoundList. + // http://www.sencha.com/forum/showthread.php?41431-Empty-string-as-ComboBox-entry-text&p=195882 + tpl: '
  • {name} 
' + }] + }; + + if (this.simpleList) { + formItems.items.push({ + xtype: 'combo', + id: this.id+'-groupby', + mode: 'local', + value: this.groupBy, + triggerAction: 'all', + forceSelection: true, + editable: false, + fieldLabel: this.groupByText, + name: 'groupby', + displayField: 'name', + valueField: 'value', + queryMode: 'local', + store: Ext.create('Ext.data.Store', { + fields : ['name', 'value'], + data : this.groupByOptions + }), + // This fixes a bug that a blank item is not properly supported. See Sencha forum and source of Ext.view.BoundList. + // http://www.sencha.com/forum/showthread.php?41431-Empty-string-as-ComboBox-entry-text&p=195882 + tpl: '
  • {name} 
' + }); + } + + formItems.items.push({ + xtype: 'checkboxfield', + id: this.id+'-showdetails', + value: this.showDetailsDefault, + inputvalue: '1', + fieldLabel: this.showDetailsText, + name: 'details' + }); + + return [formItems]; + }, + + /** + *

This function is called by this form panel to obtain the definition of the toolbar content. Override this function to + * modify what goes into the toolbar. If no toolbar is required, return an empty array from this function.

+ * @return {Array} An array of Object. + */ + getToolbarConfig: function() { + var result = []; + if (this.readOnly !== true) { + result.push({ + text: this.addBtnText, + iconCls: 'ext-cal-icon-evt-add', + listeners: { + click: { + fn: this.onAddEvent, + scope: this + } + } + }); + } + result.push( + '->', + { + text : this.resetBtnText, + handler: function() { + this.up('form').getForm().reset(); + } + } + ); + return result; + }, + + /** + *

Returns the options available in the date range combo box. Override this function to change the available + * options for the date range select list.

+ *

Returns an array of objects where each object has two attributes name and value. The + * attribute name is the display string, the attribute value is the value returned as the + * field value of the combo box. The default configuration is:


+     [
+     {name : 'One Day',   value: 'day'},
+     {name : 'One Week',  value: 'week'},
+     {name : 'One Month',  value: 'month'},
+     {name : '3 Months',  value: '3months'},
+     {name : 'One Year', value: 'year'}
+     ]
+     

+ * @return {Object} + */ + getDateRangeOptions: function() { + return [ + {name : this.dateRangeOneDay, value: 'day'}, + {name : this.dateRangeOneWeek, value: 'week'}, + {name : this.dateRangeOneMonth, value: 'month'}, + {name : this.dateRangeThreeMonths, value: '3months'}, + {name : this.dateRangeOneYear, value: 'year'} + ]; + }, + + /** + *

Returns the options available in the group by combo box. Override this function to change the available + * options for the group by select list.

+ *

Returns an array of objects where each object has two attributes name and value. The + * attribute name is the display string, the attribute value is the value returned as the + * field value of the combo box. The default configuration is:


+     [
+     {name : 'None',   value: 'none'},
+     {name : 'Month',  value: 'month'},
+     {name : 'Week',  value: 'week'}
+     ]
+     

+ * @return {Object} + */ + getGroupByOptions: function() { + return [ + {name : this.groupByNone, value: 'none'}, + {name : this.groupByMonths, value: 'month'}, + {name : this.groupByWeek, value: 'week'} + ]; + }, + + /* Private + * Event handler that is called when the form changes. + * @param {Ext.form.field.Field} field + * @param {Object} newValue + * @param {Object} oldValue + * @param {Object} eOpts + */ + onFormChange: function(field, newValue, oldValue, eOpts){ + this.fireEvent('formchange', this, this.getForm(), field, newValue, oldValue, eOpts); + this.saveState(); + }, + + /* Private + * Event handler that is called when the user clicks on the add event button. + * @param {Extensible.calendar.view.AgendaHeader} this + * @param {Ext.button.Button} bt + * @param {Event} e + * @param {Object} eOpts + */ + onAddEvent: function(bt, e, eOpts){ + this.fireEvent('addevent', this, bt, e, eOpts); + }, + + // private + afterRender : function(){ + this.callParent(arguments); + + this.dateRangeField = this.down('#' + this.id + '-daterange'); + this.dateRangeField.setValue(this.dateRangeDefault); + this.dateRangeField.on('change', this.onFormChange, this); + this.groupByField = this.down('#' + this.id + '-groupby'); + if (this.groupByField) { + this.groupByField.setValue(this.groupBy); + this.groupByField.on('change', this.onFormChange, this); + } + this.showDetailsCheckbox = this.down('#' + this.id + '-showdetails'); + this.showDetailsCheckbox.setValue(this.showDetailsDefault); + this.showDetailsCheckbox.on('change', this.onFormChange, this); + }, + + // private + refresh : function(reloadData){ + Extensible.log('refresh (AgendaHeader)'); + if (this.isDestroyed) { + return; + } + this.callParent(arguments); + }, + + /** + * This method is called by the {@link Extensible.calendar.view.Agenda Agenda} view that hosts this header when the user chooses to + * move to a new date. The current implementation does nothing but can be overriden to update the header form if + * necessary. + * @param {Date} dt The new view start date. + */ + moveTo : function(dt){ + }, + + /** + * Returns the state to be persisted in a browser cookie. This implements function getState() + * from mixin Ext.state.Stateful. + * @return {Object} + */ + getState: function() { + var state = { + daterange: this.dateRangeField.getValue(), + showdetails: this.showDetailsCheckbox.getValue() + }; + if (this.groupByField) { + state.groupby = this.groupByField.getValue(); + } + return state; + }, + + /** + * Function is called in the constructor to restore the state. This implements function applyState() + * from mixin Ext.state.Stateful. + * @param {Object} state See function getState() for the structure of state. + */ + applyState: function(state) { + if (state) { + if (state.daterange) { + var dateRangeValues = this.getDateRangeOptions(); + for (var i = 0; i < dateRangeValues.length; i++ ) { + var option = dateRangeValues[i]; + if (option.value == state.daterange) { + this.dateRangeDefault = state.daterange; + break; + } + } + } + if (state.showdetails === true || state.showdetails === false) { + this.showDetailsDefault = state.showdetails; + } + if (state.groupby) { + var groupByValues = this.getGroupByOptions(); + for (var i = 0; i < groupByValues.length; i++ ) { + var option = groupByValues[i]; + if (option.value == state.groupby) { + this.groupBy = state.groupby; + break; + } + } + } + } + } + +}); \ No newline at end of file diff --git a/src/calendar/view/Day.js b/src/calendar/view/Day.js index 75b0a551..5efa5613 100644 --- a/src/calendar/view/Day.js +++ b/src/calendar/view/Day.js @@ -198,6 +198,9 @@ Ext.define('Extensible.calendar.view.Day', { refresh: function(reloadData) { Extensible.log('refresh (DayView)'); + if (this.isDestroyed) { + return; + } if (reloadData === undefined) { reloadData = false; } diff --git a/src/calendar/view/DayBody.js b/src/calendar/view/DayBody.js index c1ef35a4..910d5cb4 100644 --- a/src/calendar/view/DayBody.js +++ b/src/calendar/view/DayBody.js @@ -98,6 +98,9 @@ Ext.define('Extensible.calendar.view.DayBody', { refresh: function(reloadData) { Extensible.log('refresh (DayBodyView)'); + if (this.isDestroyed) { + return; + } var top = this.el.getScroll().top; this.callParent(arguments); @@ -405,11 +408,17 @@ Ext.define('Extensible.calendar.view.DayBody', { evtData._height = Math.max(((endMins - startMins) * heightFactor), this.minEventHeight) + evtOffsets.height; }, + /** + * Render events. + * The event layout is based on this article: http://stackoverflow.com/questions/11311410/ and this sample + * implementation http://jsbin.com/detefuveta/5/edit?html,js,output * + */ renderItems: function() { var day = 0, evt, - evts = []; - + evts = [], + M = Extensible.calendar.data.EventMappings; + for (; day < this.dayCount; day++) { var ev = 0, emptyCells = 0, @@ -423,7 +432,6 @@ Ext.define('Extensible.calendar.view.DayBody', { continue; } var item = evt.data || evt.event.data, - M = Extensible.calendar.data.EventMappings, ad = item[M.IsAllDay.name] === true, span = this.isEventSpanning(evt.event || evt), renderAsAllDay = ad || span; @@ -443,57 +451,116 @@ Ext.define('Extensible.calendar.view.DayBody', { } } - // overlapping event pre-processing loop + // Layout events var i = 0, j = 0, - overlapCols = [], l = evts.length, - prevDt, - evt2, - dt; - - for (; i= lastEventEnding) { + // This event does not overlap with the current event group. Start a new event group. + eventGroups.push(columns); + columns = []; + lastEventEnding = 0; + } + var placed = false; + + for (j = 0; j < columns.length; j++) { + var col = columns[ j ]; + if (!this.isOverlapping( col[col.length-1], evt ) ) { + col.push(evt); + placed = true; + break; } } + + if (!placed) { + columns.push([evt]); + } + + // Remember the last event time of the event group. + // Very short events have a minimum duration on screen (we can't see a one minute event). + var eventDuration = evt.data[M.EndDate.name].getTime() - evt.data[M.StartDate.name].getTime(); + var eventEnding; + if (eventDuration < minEventDuration) { + eventEnding = evt.data[M.StartDate.name].getTime() + minEventDuration; + } else { + eventEnding = evt.data[M.EndDate.name].getTime(); + } + if (eventEnding > lastEventEnding) { + lastEventEnding = eventEnding; + } } - // rendering loop + // Push the last event group, if there is one. + if(columns.length > 0){ + eventGroups.push(columns); + } + + // Rendering loop + l = eventGroups.length; + // Loop over all the event groups. for (i = 0; i < l; i++) { - evt = evts[i].data; - dt = evt[Extensible.calendar.data.EventMappings.StartDate.name].getDate(); + var evtGroup = eventGroups[i]; + var numColumns = evtGroup.length; + + // Loop over all the virtual columns of an event group + for (j = 0; j < numColumns; j++) { + col = evtGroup[j]; - if(evt._overlap !== undefined) { - var colWidth = 100 / (overlapCols[dt]+1), - evtWidth = 100 - (colWidth * evt._overlap); + // Loop over all the events of a virtual column + for (var k = 0; k < col.length; k++) { + evt = col[k]; - evt._width = colWidth; - evt._left = colWidth * evt._overcol; + // Check if event is rightmost of a group and can be expanded to the right + var colSpan = this.expandEvent(evt, j, evtGroup); + + evt.data._width = (100 * colSpan / numColumns); + evt.data._left = (j / numColumns) * 100; + var markup = this.getEventTemplate().apply(evt.data), + target = this.id + '-day-col-' + Ext.Date.format(evt.date, 'Ymd'); + Ext.DomHelper.append(target, markup); + } } - var markup = this.getEventTemplate().apply(evt), - target = this.id + '-day-col-' + Ext.Date.format(evts[i].date, 'Ymd'); - - Ext.DomHelper.append(target, markup); } this.fireEvent('eventsrendered', this); + }, + + /** + * Expand events at the far right to use up any remaining space. This implements step 5 in the layout + * algorithm described here: http://stackoverflow.com/questions/11311410/ + * @param {Object} evt Event to process. + * @param {int} iColumn Virtual column to where the event will be rendered. + * @param {Array} columns List of virtual colums for event group. Each column contains a list of events. + * @return {Number} + */ + expandEvent: function(evt, iColumn, columns) { + var colSpan = 1; + + // To see the output without event expansion, uncomment + // the line below. Watch column 3 in the output. + // return colSpan; + + for (var i = iColumn + 1; i < columns.length; i++) + { + var col = columns[i]; + for (var j = 0; j < col.length; j++) + { + var evt1 = col[j]; + if (this.isOverlapping(evt, evt1)) + { + return colSpan; + } + } + colSpan++; + } + return colSpan; }, getDayEl: function(dt) { diff --git a/src/calendar/view/DayHeader.js b/src/calendar/view/DayHeader.js index 76feb516..a4e1a452 100644 --- a/src/calendar/view/DayHeader.js +++ b/src/calendar/view/DayHeader.js @@ -51,6 +51,9 @@ Ext.define('Extensible.calendar.view.DayHeader', { refresh: function(reloadData) { Extensible.log('refresh (DayHeaderView)'); + if (this.isDestroyed) { + return; + } this.callParent(arguments); this.recalcHeaderBox(); }, diff --git a/src/calendar/view/Month.js b/src/calendar/view/Month.js index a6f27567..70002d05 100644 --- a/src/calendar/view/Month.js +++ b/src/calendar/view/Month.js @@ -319,6 +319,9 @@ Ext.define('Extensible.calendar.view.Month', { refresh: function(reloadData) { Extensible.log('refresh (MonthView)'); + if (this.isDestroyed) { + return; + } if(this.detailPanel) { this.detailPanel.hide(); } diff --git a/src/form/recurrence/option/Monthly.js b/src/form/recurrence/option/Monthly.js index bb2d3d92..80296d4a 100644 --- a/src/form/recurrence/option/Monthly.js +++ b/src/form/recurrence/option/Monthly.js @@ -107,11 +107,11 @@ Ext.define('Extensible.form.recurrence.option.Monthly', { dt = me.getStartDate(), // e.g. 30 (for June): - lastDayOfMonth = Ext.Date.getLastDateOfMonth(dt).getDate(), + lastDayOfMonth = Extensible.Date.getDate(dt.getLastDateOfMonth()), // e.g. "28th day": monthDayText = Ext.Date.format(dt, me.strings.monthDayDateFormat) + ' ' + me.strings.day, // e.g. 28: - dayNum = dt.getDate(), + dayNum = Extensible.Date.getDate(dt), // index in the month, e.g. 4 for the 4th Tuesday dayIndex = Math.ceil(dayNum / 7), // e.g. "TU": diff --git a/src/locale/extensible-lang-de.js b/src/locale/extensible-lang-de.js index fe61a2a2..8465af1e 100644 --- a/src/locale/extensible-lang-de.js +++ b/src/locale/extensible-lang-de.js @@ -40,6 +40,8 @@ Ext.onReady(function() { dayText: 'Tag', weekText: 'Woche', monthText: 'Monat', + agendaText: 'Agenda', + listText: 'Liste', jumpToText: 'Springe zu:', goText: 'Los', multiDayText: '{0} Tage', @@ -276,13 +278,13 @@ Ext.onReady(function() { }); } - if (Extensible.form.recurrence.FrequencyCombo) { + if (exists('Extensible.form.recurrence.FrequencyCombo')) { Ext.apply(Extensible.form.recurrence.FrequencyCombo.prototype, { fieldLabel: 'Wiederholen' }); } - if (Extensible.form.recurrence.RangeEditWindow) { + if (exists('Extensible.form.recurrence.RangeEditWindow')) { Ext.apply(Extensible.form.recurrence.RangeEditWindow.prototype, { title: 'Wiederkehrender Termin', saveButtonText: 'Speichern', @@ -290,7 +292,7 @@ Ext.onReady(function() { }); } - if (Extensible.form.recurrence.RangeEditPanel) { + if (exists('Extensible.form.recurrence.RangeEditPanel')) { Ext.apply(Extensible.form.recurrence.RangeEditPanel.prototype, { headerText: 'Auf welche Termine dieser Termin-Serie möchten Sie Ihre Änderungen anwenden?', optionSingleButtonText: 'Nur diesen', @@ -302,7 +304,7 @@ Ext.onReady(function() { }); } - if (Extensible.form.recurrence.option.Interval) { + if (exists('Extensible.form.recurrence.option.Interval')) { Ext.apply(Extensible.form.recurrence.option.Interval.prototype, { dateLabelFormat: 'l, j. F', strings: { @@ -320,7 +322,7 @@ Ext.onReady(function() { }); } - if (Extensible.form.recurrence.option.Duration) { + if (exists('Extensible.form.recurrence.option.Duration')) { Ext.apply(Extensible.form.recurrence.option.Duration.prototype, { strings: { andContinuing: 'und endet', @@ -332,7 +334,7 @@ Ext.onReady(function() { }); } - if (Extensible.form.recurrence.option.Weekly) { + if (exists('Extensible.form.recurrence.option.Weekly')) { Ext.apply(Extensible.form.recurrence.option.Weekly.prototype, { strings: { on: 'am' @@ -340,7 +342,7 @@ Ext.onReady(function() { }); } - if (Extensible.form.recurrence.option.Monthly) { + if (exists('Extensible.form.recurrence.option.Monthly')) { Ext.apply(Extensible.form.recurrence.option.Monthly.prototype, { strings: { // E.g. "on the 15th day of each month/year" @@ -359,6 +361,41 @@ Ext.onReady(function() { }); } + /* + * Strings for agenda view, added in x.x.x + */ + if (exists('Extensible.calendar.template.AgendaBody')) { + Ext.apply(Extensible.calendar.template.AgendaBody.prototype, { + dayDateFormat: 'D. j. M.', + hourFormat: 'G:i', + allDayText: 'Ganzer Tag', + locationText: 'Ort', + webLinkText: 'Web Link', + notesText: 'Bemerkung', + noEventsText: 'Für den gewählten Datumsbereich existieren keine Termine.', + prevLinkText: 'Zurück', + nextLinkText: 'Weiter', + reminderTooltip: 'Erinnerung ist aktiviert', + recurringTooltip: 'Wiederkehrender Termin' + }); + } + if (exists('Extensible.calendar.view.AgendaHeader')) { + Ext.apply(Extensible.calendar.view.AgendaHeader.prototype, { + dateRangeOneDay: 'Ein Tag', + dateRangeOneWeek: 'Eine Woche', + dateRangeOneMonth: 'Ein Monat', + dateRangeThreeMonths: 'Drei Monate', + dateRangeOneYear: 'Ein Jahr', + dateRangeText: 'Datumsbereich', + groupByMonths: 'Monat', + groupByWeek: 'Woche', + groupByNone: 'Nichts', + groupByText: 'Gruppieren nach', + showDetailsText: 'Details zeigen', + addBtnText: 'Neuer Termin', + resetBtnText: 'Zurücksetzen' + }); + } }); \ No newline at end of file diff --git a/src/locale/extensible-lang-en.js b/src/locale/extensible-lang-en.js index 31de4188..1cb746a2 100644 --- a/src/locale/extensible-lang-en.js +++ b/src/locale/extensible-lang-en.js @@ -46,6 +46,8 @@ Ext.onReady(function() { dayText: 'Day', weekText: 'Week', monthText: 'Month', + agendaText: 'Agenda', + listText: 'List', jumpToText: 'Jump to:', goText: 'Go', multiDayText: '{0} Days', // deprecated @@ -365,4 +367,41 @@ Ext.onReady(function() { }); } + /* + * Strings for agenda view. Added in x.x.x + */ + if (exists('Extensible.calendar.template.AgendaBody')) { + Ext.apply(Extensible.calendar.template.AgendaBody.prototype, { + dayDateFormat: 'D M j', + hourFormat: 'g:ia', + allDayText: 'All day', + locationText: 'Location', + webLinkText: 'Web Link', + notesText: 'Notes', + noEventsText: 'There are no events for the selected date range.', + prevLinkText: 'Previous', + nextLinkText: 'Next', + reminderTooltip: 'Reminder is activated', + recurringTooltip: 'Recurring event' + }); + } + + if (exists('Extensible.calendar.view.AgendaHeader')) { + Ext.apply(Extensible.calendar.view.AgendaHeader.prototype, { + dateRangeOneDay: 'One day', + dateRangeOneWeek: 'One week', + dateRangeOneMonth: 'One month', + dateRangeThreeMonths: 'Three months', + dateRangeOneYear: 'One year', + dateRangeText: 'Date range', + groupByMonths: 'Month', + groupByWeek: 'Week', + groupByNone: 'None', + groupByText: 'Group by', + showDetailsText: 'Show details', + addBtnText: 'Add event', + resetBtnText: 'Reset' + }); + } + }); \ No newline at end of file diff --git a/src/locale/extensible-lang-en_GB.js b/src/locale/extensible-lang-en_GB.js index caecb686..4d679ea7 100644 --- a/src/locale/extensible-lang-en_GB.js +++ b/src/locale/extensible-lang-en_GB.js @@ -277,13 +277,13 @@ Ext.onReady(function() { }); } - if (Extensible.form.recurrence.FrequencyCombo) { + if (exists('Extensible.form.recurrence.FrequencyCombo')) { Ext.apply(Extensible.form.recurrence.FrequencyCombo.prototype, { fieldLabel: 'Repeats' }); } - if (Extensible.form.recurrence.RangeEditWindow) { + if (exists('Extensible.form.recurrence.RangeEditWindow')) { Ext.apply(Extensible.form.recurrence.RangeEditWindow.prototype, { title: 'Recurring Event Options', saveButtonText: 'Save', @@ -291,7 +291,7 @@ Ext.onReady(function() { }); } - if (Extensible.form.recurrence.RangeEditPanel) { + if (exists('Extensible.form.recurrence.RangeEditPanel')) { Ext.apply(Extensible.form.recurrence.RangeEditPanel.prototype, { headerText: 'There are multiple events in this series. How would you like your changes applied?', optionSingleButtonText: 'Single', @@ -303,7 +303,7 @@ Ext.onReady(function() { }); } - if (Extensible.form.recurrence.option.Interval) { + if (exists('Extensible.form.recurrence.option.Interval')) { Ext.apply(Extensible.form.recurrence.option.Interval.prototype, { dateLabelFormat: 'l, F j', strings: { @@ -321,7 +321,7 @@ Ext.onReady(function() { }); } - if (Extensible.form.recurrence.option.Duration) { + if (exists('Extensible.form.recurrence.option.Duration')) { Ext.apply(Extensible.form.recurrence.option.Duration.prototype, { strings: { andContinuing: 'and continuing', @@ -333,7 +333,7 @@ Ext.onReady(function() { }); } - if (Extensible.form.recurrence.option.Weekly) { + if (exists('Extensible.form.recurrence.option.Weekly')) { Ext.apply(Extensible.form.recurrence.option.Weekly.prototype, { strings: { on: 'on' @@ -341,7 +341,7 @@ Ext.onReady(function() { }); } - if (Extensible.form.recurrence.option.Monthly) { + if (exists('Extensible.form.recurrence.option.Monthly')) { Ext.apply(Extensible.form.recurrence.option.Monthly.prototype, { strings: { // E.g. "on the 15th day of each month/year" @@ -359,4 +359,39 @@ Ext.onReady(function() { } }); } + + if (exists('Extensible.calendar.template.AgendaBody')) { + Ext.apply(Extensible.calendar.template.AgendaBody.prototype, { + dayDateFormat: 'D M j', + hourFormat: 'G:i', + allDayText: 'All day', + locationText: 'Location', + webLinkText: 'Web Link', + notesText: 'Notes', + noEventsText: 'There are no events for the selected date range.', + prevLinkText: 'Previous', + nextLinkText: 'Next', + reminderTooltip: 'Reminder is activated', + recurringTooltip: 'Recurring event' + }); + } + + if (exists('Extensible.calendar.view.AgendaHeader')) { + Ext.apply(Extensible.calendar.view.AgendaHeader.prototype, { + dateRangeOneDay: 'One day', + dateRangeOneWeek: 'One week', + dateRangeOneMonth: 'One month', + dateRangeThreeMonths: 'Three months', + dateRangeOneYear: 'One year', + dateRangeText: 'Date range', + groupByMonths: 'Month', + groupByWeek: 'Week', + groupByNone: 'None', + groupByText: 'Group by', + showDetailsText: 'Show details', + addBtnText: 'Add event', + resetBtnText: 'Reset' + }); + } + }); \ No newline at end of file diff --git a/src/locale/extensible-lang-fa.js b/src/locale/extensible-lang-fa.js new file mode 100644 index 00000000..8f1532aa --- /dev/null +++ b/src/locale/extensible-lang-fa.js @@ -0,0 +1,602 @@ +/*! + * Extensible 1.6.0-rc.1 + * Copyright(c) 2010-2013 Extensible, LLC + * licensing@ext.ensible.com + * http://ext.ensible.com + */ +/*! + * Extensible 1.6.0-rc.1 + * Copyright(c) 2010-2013 Extensible, LLC + * licensing@ext.ensible.com + * http://ext.ensible.com + */ +/* + * Farsi (fa) locale + * By Mehran Ziadloo + */ + +/* JalaliCalendar.js Gregorian to Jalali and inverse date convertor + * Copyright (C) 2001 Roozbeh Pournader + * Copyright (C) 2001 Mohammad Toossi + * Copyright (C) 2003,2008 Behdad Esfahbod + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * 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 + * Lesser General Public License for more details. + * + * You can receive a copy of GNU Lesser General Public License at the + * World Wide Web address . + * + * For licensing issues, contact The FarsiWeb Project Group, + * Computing Center, Sharif University of Technology, + * PO Box 11365-8515, Tehran, Iran, or contact us the + * email address . + */ + +/* Changes: + * + * 2008-Jul-32: + * Use a remainder() function to fix conversion of ancient dates + * (before 1600 gregorian). Reported by Shamim Rezaei. + * + * 2003-Mar-29: + * Ported to javascript by Behdad Esfahbod + * + * 2001-Sep-21: + * Fixed a bug with "30 Esfand" dates, reported by Mahmoud Ghandi + * + * 2001-Sep-20: + * First LGPL release, with both sides of conversions + */ + +var JalaliCalendar = { + g_days_in_month: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + , j_days_in_month: [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29] + , farsiMonthNames: ["فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند"] + + , div: function(a,b) { + return Math.floor(a/b); + } + + , remainder: function(a,b) { + return a - JalaliCalendar.div(a,b)*b; + } + + , g2j: function(date) { + var g = [ + date.getFullYear() + , date.getMonth() + 1 + , date.getDate() + ]; + + var gy, gm, gd; + var jy, jm, jd; + var g_day_no, j_day_no; + var j_np; + + var i; + + gy = g[0]-1600; + gm = g[1]-1; + gd = g[2]-1; + + g_day_no = 365*gy + JalaliCalendar.div((gy+3), 4) - JalaliCalendar.div((gy+99), 100) + JalaliCalendar.div((gy+399), 400); + for (i=0; i1 && ((gy%4==0 && gy%100!=0) || (gy%400==0))) + /* leap and after Feb */ + ++g_day_no; + g_day_no += gd; + + j_day_no = g_day_no-79; + + j_np = JalaliCalendar.div(j_day_no, 12053); + j_day_no = JalaliCalendar.remainder(j_day_no, 12053); + + jy = 979+33*j_np+4 * JalaliCalendar.div(j_day_no,1461); + j_day_no = JalaliCalendar.remainder(j_day_no, 1461); + + if (j_day_no >= 366) { + jy += JalaliCalendar.div((j_day_no-1),365); + j_day_no = JalaliCalendar.remainder((j_day_no-1), 365); + } + + for (i = 0; i < 11 && j_day_no >= JalaliCalendar.j_days_in_month[i]; ++i) { + j_day_no -= JalaliCalendar.j_days_in_month[i]; + } + jm = i+1; + jd = j_day_no+1; + + return [jy, jm - 1, jd]; + } + + , j2g: function(j /* array containing year, month-1, day*/ ) { + var gy, gm, gd; + var jy, jm, jd; + var g_day_no, j_day_no; + var leap; + + var hour = 0; + var min = 0; + var second = 0; + if (typeof j[3] !== "undefined") { + hour = j[3]; + } + if (typeof j[4] !== "undefined") { + min = j[4]; + } + if (typeof j[5] !== "undefined") { + second = j[5]; + } + + var i; + + jy = j[0]-979; + jm = j[1]; + jd = j[2]-1; + + j_day_no = 365*jy + JalaliCalendar.div(jy,33)*8 + JalaliCalendar.div((JalaliCalendar.remainder(jy, 33)+3),4); + for (i=0; i < jm; ++i) + j_day_no += JalaliCalendar.j_days_in_month[i]; + + j_day_no += jd; + + g_day_no = j_day_no+79; + + gy = 1600 + 400 * JalaliCalendar.div(g_day_no,146097); /* 146097 = 365*400 + 400/4 - 400/100 + 400/400 */ + g_day_no = JalaliCalendar.remainder(g_day_no, 146097); + + leap = 1; + if (g_day_no >= 36525) /* 36525 = 365*100 + 100/4 */ + { + g_day_no--; + gy += 100 * JalaliCalendar.div(g_day_no,36524); /* 36524 = 365*100 + 100/4 - 100/100 */ + g_day_no = JalaliCalendar.remainder(g_day_no, 36524); + + if (g_day_no >= 365) + g_day_no++; + else + leap = 0; + } + + gy += 4 * JalaliCalendar.div(g_day_no,1461); /* 1461 = 365*4 + 4/4 */ + g_day_no = JalaliCalendar.remainder(g_day_no, 1461); + + if (g_day_no >= 366) { + leap = 0; + + g_day_no--; + gy += JalaliCalendar.div(g_day_no, 365); + g_day_no = JalaliCalendar.remainder(g_day_no, 365); + } + + for (i = 0; g_day_no >= JalaliCalendar.g_days_in_month[i] + (i == 1 && leap); i++) + g_day_no -= JalaliCalendar.g_days_in_month[i] + (i == 1 && leap); + gm = i+1; + gd = g_day_no+1; + + return new Date(gy, gm-1, gd, hour, min, second, 0); + } + + , today: function() { + Today = new Date(); + var j = JalaliCalendar.g2j(Today); + return j[2]+"/"+(j[1]+1)+"/"+j[0]; + } +}; + + +Ext.onReady(function() { + var exists = Ext.Function.bind(Ext.ClassManager.get, Ext.ClassManager); + + Extensible.Date.use24HourTime = true; + + if (exists('Extensible.calendar.CalendarPanel')) { + Ext.apply(Extensible.calendar.CalendarPanel.prototype, { + startDay: 6 + }); + } + if (exists('Extensible.calendar.menu.Event')) { + Ext.apply(Extensible.calendar.menu.Event.prototype, { + startDay: 6 + }); + } + if (exists('Extensible.calendar.form.EventWindow')) { + Ext.apply(Extensible.calendar.form.EventWindow.prototype, { + startDay: 6 + }); + } + if (exists('Extensible.calendar.form.EventDetails')) { + Ext.apply(Extensible.calendar.form.EventDetails.prototype, { + startDay: 6 + }); + } + if (exists('Extensible.form.field.DateRange')) { + Ext.apply(Extensible.form.field.DateRange.prototype, { + startDay: 6 + }); + } + if (exists('Extensible.form.recurrence.AbstractOption')) { + Ext.apply(Extensible.form.recurrence.AbstractOption.prototype, { + startDay: 6 + }); + } + if (exists('Extensible.form.recurrence.option.Weekly')) { + Ext.apply(Extensible.form.recurrence.option.Weekly.prototype, { + startDay: 6 + }); + } + if (exists('Extensible.form.recurrence.Fieldset')) { + Ext.apply(Extensible.form.recurrence.Fieldset.prototype, { + startDay: 6 + }); + } + if (exists('Extensible.calendar.view.AbstractCalendar')) { + Ext.apply(Extensible.calendar.view.AbstractCalendar.prototype, { + startDay: 6, + dayCount: 7, + todayText: 'امروز', + defaultEventTitleText: '(بدون عنوان)', + ddCreateEventText: 'ایجاد یک واقعه برای {0}', + ddMoveEventText: 'انتقال واقعه به {0}', + ddResizeEventText: 'به‌روز رسانی واقعه به‌‌ {0}' + }); + } + + if (exists('Extensible.calendar.view.Month')) { + Ext.apply(Extensible.calendar.view.Month.prototype, { + moreText: '+{0} ادامه...', // deprecated + getMoreText: function(numEvents){ + return '+{0} ادامه...'; + }, + detailsTitleDateFormat: 'F j' + }); + } + + if (exists('Extensible.calendar.CalendarPanel')) { + Ext.apply(Extensible.calendar.CalendarPanel.prototype, { + todayText: 'امروز', + dayText: 'روز', + weekText: 'هفته', + monthText: 'ماه', + jumpToText: 'پرش به:', + jumpToDateFormat: 'Y/n/j', + goText: 'انتقال', + multiDayText: '{0} روز', // deprecated + multiWeekText: '{0} هفته', // deprecated + getMultiDayText: function(numDays){ + return '{0} روز'; + }, + getMultiWeekText: function(numWeeks){ + return '{0} هفته'; + } + }); + } + + if (exists('Extensible.calendar.form.EventWindow')) { + Ext.apply(Extensible.calendar.form.EventWindow.prototype, { + width: 600, + labelWidth: 65, + titleTextAdd: 'ایجاد واقعه', + titleTextEdit: 'ویرایش واقعه', + savingMessage: 'ذخیره سازی تغییرات...', + deletingMessage: 'حذف واقعه...', + detailsLinkText: 'ویرایش جزئیات...', + saveButtonText: 'ذخیره', + deleteButtonText: 'حذف', + cancelButtonText: 'لغو', + titleLabelText: 'عنوان', + datesLabelText: 'زمان', + calendarLabelText: 'تقویم' + }); + } + + if (exists('Extensible.calendar.form.EventDetails')) { + Ext.apply(Extensible.calendar.form.EventDetails.prototype, { + labelWidth: 65, + labelWidthRightCol: 65, + title: 'فرم واقعه', + titleTextAdd: 'ایجاد واقعه', + titleTextEdit: 'ویرایش واقعه', + saveButtonText: 'ذخیره', + deleteButtonText: 'حذف', + cancelButtonText: 'لغو', + titleLabelText: 'عنوان', + datesLabelText: 'زمان', + reminderLabelText: 'یادآوری', + notesLabelText: 'دست نوشته', + locationLabelText: 'مکان', + webLinkLabelText: 'لینک وب', + calendarLabelText: 'تقویم', + repeatsLabelText: 'تکرار' + }); + } + + if (exists('Extensible.form.field.DateRange')) { + Ext.apply(Extensible.form.field.DateRange.prototype, { + toText: 'تا', + allDayText: 'تمام روز' + }); + } + + if (exists('Extensible.calendar.form.field.CalendarCombo')) { + Ext.apply(Extensible.calendar.form.field.CalendarCombo.prototype, { + fieldLabel: 'تقویم' + }); + } + + if (exists('Extensible.calendar.gadget.CalendarListPanel')) { + Ext.apply(Extensible.calendar.gadget.CalendarListPanel.prototype, { + title: 'تقویم‌ها‌' + }); + } + + if (exists('Extensible.calendar.gadget.CalendarListMenu')) { + Ext.apply(Extensible.calendar.gadget.CalendarListMenu.prototype, { + displayOnlyThisCalendarText: 'تنها همین تقویم را نمایش بده' + }); + } + + if (exists('Extensible.form.recurrence.Combo')) { + Ext.apply(Extensible.form.recurrence.Combo.prototype, { + fieldLabel: 'تکرارها', + recurrenceText: { + none: 'تکرار نمی‌شود', + daily: 'روزانه', + weekly: 'هفتگی', + monthly: 'ماهانه', + yearly: 'سالانه' + } + }); + } + + if (exists('Extensible.calendar.form.field.ReminderCombo')) { + Ext.apply(Extensible.calendar.form.field.ReminderCombo.prototype, { + fieldLabel: 'یادآوری', + noneText: 'هیچ کدام', + atStartTimeText: 'در زمان شروع', + getMinutesText: function(numMinutes){ + return numMinutes === 1 ? 'دقیقه' : 'دقیقه'; + }, + getHoursText: function(numHours){ + return numHours === 1 ? 'ساعت' : 'ساعت'; + }, + getDaysText: function(numDays){ + return numDays === 1 ? 'روز' : 'روز'; + }, + getWeeksText: function(numWeeks){ + return numWeeks === 1 ? 'هفته' : 'هفته'; + }, + reminderValueFormat: '{0} {1} قبل از شروع' // e.g. "2 hours before start" + }); + } + + if (exists('Extensible.form.field.DateRange')) { + Ext.apply(Extensible.form.field.DateRange.prototype, { + dateFormat: 'Y/n/j' + }); + } + + if (exists('Extensible.calendar.menu.Event')) { + Ext.apply(Extensible.calendar.menu.Event.prototype, { + editDetailsText: 'ویرایش جزئیات', + deleteText: 'حذف', + moveToText: 'انتقال به...' + }); + } + + if (exists('Extensible.calendar.dd.DropZone')) { + Ext.apply(Extensible.calendar.dd.DropZone.prototype, { + dateRangeFormat: '{0}-{1}', + dateFormat: 'n/j' + }); + } + + if (exists('Extensible.calendar.dd.DayDropZone')) { + Ext.apply(Extensible.calendar.dd.DayDropZone.prototype, { + dateRangeFormat: '{0}-{1}', + dateFormat : 'n/j' + }); + } + + if (exists('Extensible.calendar.template.BoxLayout')) { + Ext.apply(Extensible.calendar.template.BoxLayout.prototype, { + firstWeekDateFormat: 'D j', + otherWeeksDateFormat: 'j', + singleDayDateFormat: 'l, j F Y', + multiDayFirstDayFormat: 'j F Y', + multiDayMonthStartFormat: 'j F' + }); + } + + if (exists('Extensible.calendar.template.Month')) { + Ext.apply(Extensible.calendar.template.Month.prototype, { + dayHeaderFormat: 'D', + dayHeaderTitleFormat: 'l, j F Y' + }); + } + + /* + * Recurrence strings added in v.1.6.0 + */ + if (exists('Extensible.form.recurrence.Rule')) { + Ext.apply(Extensible.form.recurrence.Rule.prototype, { + strings: { + dayNamesShort: { + SU: 'یکشنبه', + MO: 'دوشنبه', + TU: 'سه‌شنبه', + WE: 'چهارشنبه', + TH: 'پنج‌شنبه', + FR: 'جمعه', + SA: 'شنبه' + }, + + dayNamesShortByIndex: [ + 'یکشنبه', + 'دوشنبه', + 'سه‌شنبه', + 'چهارشنبه', + 'پنج‌شنبه', + 'جمعه', + 'شنبه' + ], + + dayNamesLong: { + SU: 'یکشنبه', + MO: 'دوشنبه', + TU: 'سه‌شنبه', + WE: 'چهارشنبه', + TH: 'پنج‌شنبه', + FR: 'جمعه', + SA: 'شنبه' + }, + + ordinals: { + 1: 'اول', + 2: 'دوم', + 3: 'سوم', + 4: 'چهارم', + 5: 'پنجم', + 6: 'ششم' + }, + + frequency: { + none: 'تکرار نمی‌شود', + daily: 'روزانه', + weekly: 'هفتگی', + weekdays: 'هر (شنبه-جمعه)', + monthly: 'ماهانه', + yearly: 'سالانه' + }, + + every: 'هر', // e.g. Every 2 days + days: 'روز', + weeks: 'هفته', + weekdays: 'روزهای هفته', + months: 'ماه', + years: 'سال', + time: 'بار', // e.g. Daily, 1 time + times: 'بار', // e.g. Daily, 5 times + until: 'تا', // e.g. Daily, until Dec, 31 2012 + untilFormat: 'j F Y', // e.g. Dec 10, 2012 + and: 'و', // e.g. Weekly on Tuesday and Friday + on: 'در', // e.g. Weekly on Thursday + onDay: 'در', // e.g. Monthly on day 23 + onDayPostfix: 'هر', // In some languages a postfix is need for the onDay term, + // for example in German: 'Monatlich am 23.' + // Here the postfix would be '.' + onThe: 'در', // e.g. Monthly on the first Thursday + onTheLast: 'آخر', // e.g. Monthly on the last Friday + onTheLastDay: 'آخرین روز', // e.g. Monthly on the last day + of: '', // e.g. Annually on the last day of November + monthFormat: 'F', // e.g. November + monthDayFormat: 'j F' // e.g. November 10 + } + }); + } + + if (Extensible.form.recurrence.FrequencyCombo) { + Ext.apply(Extensible.form.recurrence.FrequencyCombo.prototype, { + fieldLabel: 'تکرارها' + }); + } + + if (Extensible.form.recurrence.RangeEditWindow) { + Ext.apply(Extensible.form.recurrence.RangeEditWindow.prototype, { + title: 'Recurring Event Options', + saveButtonText: 'ذخیره سازی', + cancelButtonText: 'لغو' + }); + } + + if (Extensible.form.recurrence.RangeEditPanel) { + Ext.apply(Extensible.form.recurrence.RangeEditPanel.prototype, { + headerText: 'وقایع متعددی در این مجموعه وجود دارد. چگونه می‌بایست درخواست‌تان اعمال گردد؟', + optionSingleButtonText: 'تکی', + optionSingleDescription: 'فقط بر روی همین یک واقعه اعمال گردد. وقایع دیگر تغییری نخواهند کرد.', + optionFutureButtonText: 'آینده', + optionFutureDescription: 'بر روی این واقعه و وقایع بعدی اعمال گردد. وقایع گذشته تغییری نخواهند کرد.', + optionAllButtonText: 'تمام وقایع', + optionAllDescription: 'بر روی تمام وقایع این مجموعه اعمال گردد.' + }); + } + + if (Extensible.form.recurrence.option.Interval) { + Ext.apply(Extensible.form.recurrence.option.Interval.prototype, { + dateLabelFormat: 'l, F j', + strings: { + repeatEvery: 'موارد تکرار', + beginning: 'شروع', + day: 'روز', + days: 'روز', + week: 'هفته', + weeks: 'هفته', + month: 'ماه', + months: 'ماه', + year: 'سال', + years: 'سال' + } + }); + } + + if (Extensible.form.recurrence.option.Duration) { + Ext.apply(Extensible.form.recurrence.option.Duration.prototype, { + strings: { + andContinuing: 'و در ادامه', + occurrences: 'دفعات وقوع', + forever: 'برای همیشه', + forText: 'برای', + until: 'تا' + } + }); + } + + if (Extensible.form.recurrence.option.Weekly) { + Ext.apply(Extensible.form.recurrence.option.Weekly.prototype, { + strings: { + on: 'در' + } + }); + } + + if (Extensible.form.recurrence.option.Monthly) { + Ext.apply(Extensible.form.recurrence.option.Monthly.prototype, { + strings: { + // E.g. "on the 15th day of each month/year" + onThe: 'در', + ofEach: 'برای هر', + inText: 'در', + day: 'روز', + month: 'ماه', + year: 'سال', + last: 'آخر', + lastDay: 'آخرین روز', + monthDayDateFormat: 'jS', + nthWeekdayDateFormat: 'ام' // displays the ordinal postfix, e.g. th for 5th. + + } + }); + } + + Ext.override(Extensible.Date, { + isWeekend: function(dt){ + return dt.getDay() == 5; + } + , getMonth: function(dt) { + var j = JalaliCalendar.g2j(dt); + return j[1]; + } + , getDate: function(dt) { + var j = JalaliCalendar.g2j(dt); + return parseInt(j[2]); + } + }); + +}); \ No newline at end of file