├── src ├── img │ ├── info.png │ ├── icon-milestone.png │ ├── icon-reportable.png │ ├── icon-timesheets.png │ ├── icon-acttimesheets.png │ └── icon-deliverable.png ├── css │ ├── favicon.ico │ ├── openhand.cur │ ├── closedhand.cur │ ├── images │ │ ├── bg.jpg │ │ ├── cut.png │ │ ├── top.png │ │ ├── bottom.png │ │ ├── door.png │ │ ├── link.png │ │ ├── user.png │ │ ├── comment.png │ │ ├── comments.png │ │ ├── OneViewPPMg.png │ │ ├── arrow_left.png │ │ ├── arrow_right.png │ │ ├── properties.png │ │ ├── animated-overlay.gif │ │ ├── page_white_add.png │ │ ├── page_white_copy.png │ │ ├── page_white_edit.png │ │ ├── page_white_paste.png │ │ ├── page_white_delete.png │ │ ├── ui-icons_222222_256x240.png │ │ ├── ui-icons_228ef1_256x240.png │ │ ├── ui-icons_3572ac_256x240.png │ │ ├── ui-icons_8c291d_256x240.png │ │ ├── ui-icons_b83400_256x240.png │ │ ├── ui-icons_ef8c08_256x240.png │ │ ├── ui-icons_fbdb93_256x240.png │ │ ├── ui-icons_ffd27a_256x240.png │ │ ├── ui-icons_ffffff_256x240.png │ │ ├── ui-bg_flat_10_000000_40x100.png │ │ ├── ui-bg_glass_100_f6f6f6_1x400.png │ │ ├── ui-bg_glass_100_fdf5ce_1x400.png │ │ ├── ui-bg_glass_65_ffffff_1x400.png │ │ ├── ui-bg_fine-grain_10_eceadf_60x60.png │ │ ├── ui-bg_fine-grain_10_f8f7f6_60x60.png │ │ ├── ui-bg_fine-grain_15_eceadf_60x60.png │ │ ├── ui-bg_fine-grain_15_f7f3de_60x60.png │ │ ├── ui-bg_fine-grain_15_ffffff_60x60.png │ │ ├── ui-bg_fine-grain_65_654b24_60x60.png │ │ ├── ui-bg_fine-grain_68_b83400_60x60.png │ │ ├── ui-bg_diagonal-maze_20_6e4f1c_10x10.png │ │ ├── ui-bg_diagonal-maze_40_000000_10x10.png │ │ ├── ui-bg_gloss-wave_35_f6a828_500x100.png │ │ ├── ui-bg_highlight-soft_75_ffe45c_1x100.png │ │ ├── ui-bg_diagonals-thick_18_b81900_40x40.png │ │ ├── ui-bg_diagonals-thick_20_666666_40x40.png │ │ └── ui-bg_highlight-soft_100_eeeeee_1x100.png │ ├── TopMenu.css │ ├── reset.css │ ├── sortable.css │ ├── loader.css │ ├── jquery.contextMenu.css │ └── gantt.css ├── libs │ ├── jquery.comment │ │ ├── comment.gif │ │ ├── ajax-loader.gif │ │ ├── add_comment_ico.png │ │ ├── jquery.comment.min.css │ │ └── jquery.comment.js │ ├── images │ │ ├── ui-icons_222222_256x240.png │ │ ├── ui-icons_228ef1_256x240.png │ │ ├── ui-icons_ef8c08_256x240.png │ │ ├── ui-icons_ffd27a_256x240.png │ │ ├── ui-icons_ffffff_256x240.png │ │ ├── ui-bg_flat_10_000000_40x100.png │ │ ├── ui-bg_glass_65_ffffff_1x400.png │ │ ├── ui-bg_glass_100_f6f6f6_1x400.png │ │ ├── ui-bg_glass_100_fdf5ce_1x400.png │ │ ├── ui-bg_gloss-wave_35_f6a828_500x100.png │ │ ├── ui-bg_diagonals-thick_18_b81900_40x40.png │ │ ├── ui-bg_diagonals-thick_20_666666_40x40.png │ │ ├── ui-bg_highlight-soft_100_eeeeee_1x100.png │ │ └── ui-bg_highlight-soft_75_ffe45c_1x100.png │ ├── semantic │ │ └── themes │ │ │ ├── basic │ │ │ └── assets │ │ │ │ └── fonts │ │ │ │ ├── icons.eot │ │ │ │ ├── icons.ttf │ │ │ │ └── icons.woff │ │ │ └── default │ │ │ └── assets │ │ │ ├── fonts │ │ │ ├── icons.eot │ │ │ ├── icons.otf │ │ │ ├── icons.ttf │ │ │ └── icons.woff │ │ │ └── images │ │ │ └── flags.png │ ├── backbone.KonvaView.js │ ├── jquery.mousewheel.js │ ├── FileSaver.js │ └── xmlToJSON.js └── js │ ├── views │ ├── Notifications.js │ ├── TopMenuView │ │ ├── TopMenuView.js │ │ ├── GroupingMenuView.js │ │ ├── MiscMenuView.js │ │ ├── ZoomMenuView.js │ │ ├── FilterMenuView.js │ │ └── MSProjectMenuView.js │ ├── sideBar │ │ ├── DatePicker.js │ │ ├── ContextMenuView.js │ │ ├── NestedTask.js │ │ └── SidePanel.js │ ├── canvasChart │ │ ├── NestedTaskView.js │ │ ├── ConnectorView.js │ │ └── AloneTaskView.js │ ├── ResourcesEditor.js │ ├── CommentsView.js │ ├── GanttView.js │ └── ModalTaskEditView.js │ ├── clientConfig.js │ ├── models │ ├── ResourceReference.js │ ├── ResourceReferenceCollection.js │ ├── TaskModel.js │ └── SettingModel.js │ ├── utils │ ├── util.js │ └── xmlWorker.js │ ├── collections │ ├── ResourceReferenceCollection.js │ └── TaskCollection.js │ └── app.js ├── resources ├── ganttImage2.jpg └── jQueryGantt.png ├── .gitignore ├── gulpfile.js ├── package.json ├── data ├── config.js ├── comments.js ├── resources.js └── tasks.js ├── README.md ├── test_server.js └── .eslintrc /src/img/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/img/info.png -------------------------------------------------------------------------------- /src/css/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/favicon.ico -------------------------------------------------------------------------------- /src/css/openhand.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/openhand.cur -------------------------------------------------------------------------------- /src/css/closedhand.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/closedhand.cur -------------------------------------------------------------------------------- /src/css/images/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/bg.jpg -------------------------------------------------------------------------------- /src/css/images/cut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/cut.png -------------------------------------------------------------------------------- /src/css/images/top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/top.png -------------------------------------------------------------------------------- /resources/ganttImage2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/resources/ganttImage2.jpg -------------------------------------------------------------------------------- /resources/jQueryGantt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/resources/jQueryGantt.png -------------------------------------------------------------------------------- /src/css/images/bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/bottom.png -------------------------------------------------------------------------------- /src/css/images/door.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/door.png -------------------------------------------------------------------------------- /src/css/images/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/link.png -------------------------------------------------------------------------------- /src/css/images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/user.png -------------------------------------------------------------------------------- /src/css/images/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/comment.png -------------------------------------------------------------------------------- /src/css/images/comments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/comments.png -------------------------------------------------------------------------------- /src/img/icon-milestone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/img/icon-milestone.png -------------------------------------------------------------------------------- /src/img/icon-reportable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/img/icon-reportable.png -------------------------------------------------------------------------------- /src/img/icon-timesheets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/img/icon-timesheets.png -------------------------------------------------------------------------------- /src/css/images/OneViewPPMg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/OneViewPPMg.png -------------------------------------------------------------------------------- /src/css/images/arrow_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/arrow_left.png -------------------------------------------------------------------------------- /src/css/images/arrow_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/arrow_right.png -------------------------------------------------------------------------------- /src/css/images/properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/properties.png -------------------------------------------------------------------------------- /src/img/icon-acttimesheets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/img/icon-acttimesheets.png -------------------------------------------------------------------------------- /src/img/icon-deliverable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/img/icon-deliverable.png -------------------------------------------------------------------------------- /src/css/images/animated-overlay.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/animated-overlay.gif -------------------------------------------------------------------------------- /src/css/images/page_white_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/page_white_add.png -------------------------------------------------------------------------------- /src/css/images/page_white_copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/page_white_copy.png -------------------------------------------------------------------------------- /src/css/images/page_white_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/page_white_edit.png -------------------------------------------------------------------------------- /src/css/images/page_white_paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/page_white_paste.png -------------------------------------------------------------------------------- /src/libs/jquery.comment/comment.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/jquery.comment/comment.gif -------------------------------------------------------------------------------- /src/css/images/page_white_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/page_white_delete.png -------------------------------------------------------------------------------- /src/libs/jquery.comment/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/jquery.comment/ajax-loader.gif -------------------------------------------------------------------------------- /src/css/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /src/css/images/ui-icons_228ef1_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-icons_228ef1_256x240.png -------------------------------------------------------------------------------- /src/css/images/ui-icons_3572ac_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-icons_3572ac_256x240.png -------------------------------------------------------------------------------- /src/css/images/ui-icons_8c291d_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-icons_8c291d_256x240.png -------------------------------------------------------------------------------- /src/css/images/ui-icons_b83400_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-icons_b83400_256x240.png -------------------------------------------------------------------------------- /src/css/images/ui-icons_ef8c08_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-icons_ef8c08_256x240.png -------------------------------------------------------------------------------- /src/css/images/ui-icons_fbdb93_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-icons_fbdb93_256x240.png -------------------------------------------------------------------------------- /src/css/images/ui-icons_ffd27a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-icons_ffd27a_256x240.png -------------------------------------------------------------------------------- /src/css/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /src/libs/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /src/libs/images/ui-icons_228ef1_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/images/ui-icons_228ef1_256x240.png -------------------------------------------------------------------------------- /src/libs/images/ui-icons_ef8c08_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/images/ui-icons_ef8c08_256x240.png -------------------------------------------------------------------------------- /src/libs/images/ui-icons_ffd27a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/images/ui-icons_ffd27a_256x240.png -------------------------------------------------------------------------------- /src/libs/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /src/libs/jquery.comment/add_comment_ico.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/jquery.comment/add_comment_ico.png -------------------------------------------------------------------------------- /src/css/images/ui-bg_flat_10_000000_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-bg_flat_10_000000_40x100.png -------------------------------------------------------------------------------- /src/css/images/ui-bg_glass_100_f6f6f6_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-bg_glass_100_f6f6f6_1x400.png -------------------------------------------------------------------------------- /src/css/images/ui-bg_glass_100_fdf5ce_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-bg_glass_100_fdf5ce_1x400.png -------------------------------------------------------------------------------- /src/css/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /src/libs/images/ui-bg_flat_10_000000_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/images/ui-bg_flat_10_000000_40x100.png -------------------------------------------------------------------------------- /src/libs/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /src/libs/images/ui-bg_glass_100_f6f6f6_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/images/ui-bg_glass_100_f6f6f6_1x400.png -------------------------------------------------------------------------------- /src/libs/images/ui-bg_glass_100_fdf5ce_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/images/ui-bg_glass_100_fdf5ce_1x400.png -------------------------------------------------------------------------------- /src/css/images/ui-bg_fine-grain_10_eceadf_60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-bg_fine-grain_10_eceadf_60x60.png -------------------------------------------------------------------------------- /src/css/images/ui-bg_fine-grain_10_f8f7f6_60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-bg_fine-grain_10_f8f7f6_60x60.png -------------------------------------------------------------------------------- /src/css/images/ui-bg_fine-grain_15_eceadf_60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-bg_fine-grain_15_eceadf_60x60.png -------------------------------------------------------------------------------- /src/css/images/ui-bg_fine-grain_15_f7f3de_60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-bg_fine-grain_15_f7f3de_60x60.png -------------------------------------------------------------------------------- /src/css/images/ui-bg_fine-grain_15_ffffff_60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-bg_fine-grain_15_ffffff_60x60.png -------------------------------------------------------------------------------- /src/css/images/ui-bg_fine-grain_65_654b24_60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-bg_fine-grain_65_654b24_60x60.png -------------------------------------------------------------------------------- /src/css/images/ui-bg_fine-grain_68_b83400_60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-bg_fine-grain_68_b83400_60x60.png -------------------------------------------------------------------------------- /src/css/images/ui-bg_diagonal-maze_20_6e4f1c_10x10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-bg_diagonal-maze_20_6e4f1c_10x10.png -------------------------------------------------------------------------------- /src/css/images/ui-bg_diagonal-maze_40_000000_10x10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-bg_diagonal-maze_40_000000_10x10.png -------------------------------------------------------------------------------- /src/css/images/ui-bg_gloss-wave_35_f6a828_500x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-bg_gloss-wave_35_f6a828_500x100.png -------------------------------------------------------------------------------- /src/css/images/ui-bg_highlight-soft_75_ffe45c_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-bg_highlight-soft_75_ffe45c_1x100.png -------------------------------------------------------------------------------- /src/libs/images/ui-bg_gloss-wave_35_f6a828_500x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/images/ui-bg_gloss-wave_35_f6a828_500x100.png -------------------------------------------------------------------------------- /src/libs/semantic/themes/basic/assets/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/semantic/themes/basic/assets/fonts/icons.eot -------------------------------------------------------------------------------- /src/libs/semantic/themes/basic/assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/semantic/themes/basic/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /src/libs/semantic/themes/basic/assets/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/semantic/themes/basic/assets/fonts/icons.woff -------------------------------------------------------------------------------- /src/libs/semantic/themes/default/assets/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/semantic/themes/default/assets/fonts/icons.eot -------------------------------------------------------------------------------- /src/libs/semantic/themes/default/assets/fonts/icons.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/semantic/themes/default/assets/fonts/icons.otf -------------------------------------------------------------------------------- /src/libs/semantic/themes/default/assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/semantic/themes/default/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /src/css/images/ui-bg_diagonals-thick_18_b81900_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-bg_diagonals-thick_18_b81900_40x40.png -------------------------------------------------------------------------------- /src/css/images/ui-bg_diagonals-thick_20_666666_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-bg_diagonals-thick_20_666666_40x40.png -------------------------------------------------------------------------------- /src/css/images/ui-bg_highlight-soft_100_eeeeee_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/css/images/ui-bg_highlight-soft_100_eeeeee_1x100.png -------------------------------------------------------------------------------- /src/libs/images/ui-bg_diagonals-thick_18_b81900_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/images/ui-bg_diagonals-thick_18_b81900_40x40.png -------------------------------------------------------------------------------- /src/libs/images/ui-bg_diagonals-thick_20_666666_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/images/ui-bg_diagonals-thick_20_666666_40x40.png -------------------------------------------------------------------------------- /src/libs/images/ui-bg_highlight-soft_100_eeeeee_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/images/ui-bg_highlight-soft_100_eeeeee_1x100.png -------------------------------------------------------------------------------- /src/libs/images/ui-bg_highlight-soft_75_ffe45c_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/images/ui-bg_highlight-soft_75_ffe45c_1x100.png -------------------------------------------------------------------------------- /src/libs/semantic/themes/default/assets/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/semantic/themes/default/assets/fonts/icons.woff -------------------------------------------------------------------------------- /src/libs/semantic/themes/default/assets/images/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bitcoder/Jquery-Gantt/HEAD/src/libs/semantic/themes/default/assets/images/flags.png -------------------------------------------------------------------------------- /src/css/TopMenu.css: -------------------------------------------------------------------------------- 1 | #top-menu { 2 | font-size: 1em; 3 | } 4 | 5 | #top-menu .header { 6 | font-weight: bold; 7 | } 8 | 9 | #top-menu .action { 10 | cursor : pointer; 11 | } -------------------------------------------------------------------------------- /src/js/views/Notifications.js: -------------------------------------------------------------------------------- 1 | var Notifications = Backbone.View.extend({ 2 | initialize : function() { 3 | this.listenTo(this.collection, 'error', _.debounce(this.onError, 10)); 4 | }, 5 | onError : function() { 6 | console.error(arguments); 7 | noty({ 8 | text: 'Error while saving task, please refresh your browser, request support if this error continues.', 9 | layout : 'topRight', 10 | type : 'error' 11 | }); 12 | } 13 | }); 14 | 15 | module.exports = Notifications; 16 | -------------------------------------------------------------------------------- /src/js/clientConfig.js: -------------------------------------------------------------------------------- 1 | let util = require('./utils/util'); 2 | let params = util.getURLParams(); 3 | 4 | let tasksSubURL = ''; 5 | // detect API params from get, e.g. ?project=143&profile=17&sitekey=2b00da46b57c0395 6 | if (params.project && params.profile && params.sitekey) { 7 | tasksSubURL = '/' + params.project + '/' + params.profile + '/' + params.sitekey; 8 | } 9 | export let tasksURL = 'api/tasks/' + tasksSubURL; 10 | 11 | 12 | 13 | let configSubURL = ''; 14 | if (window.location.hostname.indexOf('localhost') === -1) { 15 | configSubURL = '/wbs/' + params.project + '/' + params.sitekey; 16 | } 17 | 18 | export let configURL = '/api/GanttConfig' + configSubURL; 19 | 20 | -------------------------------------------------------------------------------- /src/js/models/ResourceReference.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var util = require('../utils/util'); 4 | var params = util.getURLParams(); 5 | 6 | var ResourceReference = Backbone.Model.extend({ 7 | defaults: { 8 | // main params 9 | WBSID : 1, // task id 10 | ResID: 1, // resource id 11 | TSActivate: true, 12 | 13 | // some server params 14 | WBSProfileID : params.profile, 15 | WBS_ID : params.profile, 16 | PartitNo : params.sitekey, // have no idea what is that 17 | ProjectRef : params.project, 18 | sitekey: params.sitekey 19 | 20 | }, 21 | initialize : function() { 22 | 23 | } 24 | }); 25 | 26 | module.exports = ResourceReference; 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Windows image file caches 3 | Thumbs.db 4 | ehthumbs.db 5 | 6 | # Folder config file 7 | Desktop.ini 8 | 9 | # Recycle Bin used on file shares 10 | $RECYCLE.BIN/ 11 | 12 | # Windows Installer files 13 | *.cab 14 | *.msi 15 | *.msm 16 | *.msp 17 | node_modules 18 | # ========================= 19 | # Operating System Files 20 | # ========================= 21 | 22 | # OSX 23 | # ========================= 24 | 25 | .DS_Store 26 | .AppleDouble 27 | .LSOverride 28 | 29 | # Icon must ends with two \r. 30 | Icon 31 | 32 | 33 | # Thumbnails 34 | ._* 35 | 36 | # Files that might appear on external disk 37 | .Spotlight-V100 38 | .Trashes 39 | 40 | 41 | 42 | *sublime* 43 | .idea 44 | -------------------------------------------------------------------------------- /src/js/views/TopMenuView/TopMenuView.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var ZoomMenuView = require('./ZoomMenuView'); 3 | var GroupingMenuView = require('./GroupingMenuView'); 4 | var FilterMenuView = require('./FilterMenuView'); 5 | var MSProjectMenuView = require('./MSProjectMenuView'); 6 | var MiscMenuView = require('./MiscMenuView'); 7 | 8 | var TopMenuView = Backbone.View.extend({ 9 | initialize : function(params) { 10 | new ZoomMenuView(params).render(); 11 | new GroupingMenuView(params).render(); 12 | new FilterMenuView(params).render(); 13 | new MSProjectMenuView(params).render(); 14 | new MiscMenuView(params).render(); 15 | } 16 | }); 17 | 18 | module.exports = TopMenuView; 19 | -------------------------------------------------------------------------------- /src/js/views/TopMenuView/GroupingMenuView.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var GroupingMenuView = Backbone.View.extend({ 4 | el : '#grouping-menu', 5 | initialize : function(params) { 6 | this.settings = params.settings; 7 | }, 8 | events : { 9 | 'click #top-expand-all' : function() { 10 | this.collection.each(function(task) { 11 | if (task.isNested()) { 12 | task.set('collapsed', false); 13 | } 14 | }); 15 | }, 16 | 'click #top-collapse-all' : function() { 17 | this.collection.each(function(task) { 18 | if (task.isNested()) { 19 | task.set('collapsed', true); 20 | } 21 | }); 22 | } 23 | } 24 | }); 25 | 26 | module.exports = GroupingMenuView; 27 | -------------------------------------------------------------------------------- /src/js/views/TopMenuView/MiscMenuView.js: -------------------------------------------------------------------------------- 1 | var ReportsMenuView = Backbone.View.extend({ 2 | el: '#reports-menu', 3 | initialize: function(params) { 4 | this.settings = params.settings; 5 | }, 6 | events: { 7 | 'click #print': 'generatePDF', 8 | 'click #showVideo': 'showHelp' 9 | }, 10 | generatePDF: function(evt) { 11 | window.print(); 12 | evt.preventDefault(); 13 | }, 14 | showHelp: function() { 15 | $('#showVideoModal').modal({ 16 | onHidden: function() { 17 | $(document.body).removeClass('dimmable'); 18 | }, 19 | onApprove: function() { 20 | $(document.body).removeClass('dimmable'); 21 | } 22 | }).modal('show'); 23 | } 24 | }); 25 | 26 | module.exports = ReportsMenuView; 27 | -------------------------------------------------------------------------------- /src/js/views/TopMenuView/ZoomMenuView.js: -------------------------------------------------------------------------------- 1 | var ZoomMenuView = Backbone.View.extend({ 2 | el: '#zoom-menu', 3 | initialize: function(params) { 4 | this.settings = params.settings; 5 | this._hightlightSelected(); 6 | }, 7 | events: { 8 | 'click .action': 'onIntervalButtonClicked' 9 | }, 10 | onIntervalButtonClicked: function(evt) { 11 | var button = $(evt.currentTarget); 12 | var interval = button.data('interval'); 13 | this.settings.set('interval', interval); 14 | this._hightlightSelected(); 15 | }, 16 | _hightlightSelected: function() { 17 | this.$('.action').removeClass('selected'); 18 | 19 | let interval = this.settings.get('interval'); 20 | this.$('[data-interval="' + interval + '"]').addClass('selected'); 21 | } 22 | }); 23 | 24 | module.exports = ZoomMenuView; 25 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var gutil = require('gulp-util'); 3 | var browserify = require('gulp-browserify'); 4 | var livereload = require('gulp-livereload'); 5 | var rename = require('gulp-rename'); 6 | 7 | // Basic usage 8 | gulp.task('scripts', function() { 9 | // Single entry point to browserify 10 | gulp.src('src/js/app.js') 11 | .pipe(browserify({ 12 | debug: true, 13 | transform: [['babelify'], 'brfs'] 14 | }).on('error', gutil.log)) 15 | .pipe(rename('bundle.js')) 16 | .pipe(gulp.dest('./src/')) 17 | .pipe(livereload()); 18 | }); 19 | 20 | gulp.task('watch', function() { 21 | gulp.watch(['src/**/*.js', 'src/**/*.html', 'src/**/*.xml', '!src/bundle.js'], ['scripts']); 22 | }); 23 | 24 | gulp.task('server', function() { 25 | require('./test_server'); 26 | }); 27 | 28 | gulp.task('default', ['scripts', 'watch', 'server']); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GanttJS", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "test_server.js", 6 | "scripts": { 7 | "start": "gulp", 8 | "test": "node node_modules/mocha/bin/mocha --compilers js:babel/register", 9 | "debug-test": "node node_modules/mocha/bin/mocha --debug-brk --compilers js:babel/register" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git@github.com:2bitcoder/Jquery-Gantt.git" 14 | }, 15 | "license": "MIT License", 16 | "dependencies": { 17 | "backbone": "^1.1.2", 18 | "body-parser": "1.9.2", 19 | "brfs": "^1.2.0", 20 | "chai": "^1.10.0", 21 | "express": "4.10.1", 22 | "gulp": "^3.8.10", 23 | "gulp-browserify": "^0.5.0", 24 | "gulp-livereload": "^2.1.1", 25 | "gulp-util": "^3.0.1", 26 | "lodash": "^2.4.1", 27 | "morgan": "1.5.0", 28 | "serve-static": "^1.7.1", 29 | "underscore": "^1.7.0" 30 | }, 31 | "devDependencies": { 32 | "babelify": "^5.0.4", 33 | "gulp-rename": "^1.2.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/css/reset.css: -------------------------------------------------------------------------------- 1 | /* v1.0 | 20080212 */ 2 | 3 | html, body, div, span, applet, object, iframe, 4 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 5 | a, abbr, acronym, address, big, cite, code, 6 | del, dfn, em, font, img, ins, kbd, q, s, samp, 7 | small, strike, strong, sub, sup, tt, var, 8 | b, u, i, center, 9 | dl, dt, dd, ol, ul, li, 10 | fieldset, form, label, legend, 11 | table, caption, tbody, tfoot, thead, tr, th, td { 12 | margin: 0; 13 | padding: 0; 14 | border: 0; 15 | outline: 0; 16 | font-size: 100%; 17 | vertical-align: baseline; 18 | background: transparent; 19 | } 20 | body { 21 | line-height: 1; 22 | } 23 | ol, ul { 24 | list-style: none; 25 | } 26 | blockquote, q { 27 | quotes: none; 28 | } 29 | blockquote:before, blockquote:after, 30 | q:before, q:after { 31 | content: ''; 32 | content: none; 33 | } 34 | 35 | /* remember to define focus styles! */ 36 | :focus { 37 | outline: 0; 38 | } 39 | 40 | /* remember to highlight inserts somehow! */ 41 | ins { 42 | text-decoration: none; 43 | } 44 | del { 45 | text-decoration: line-through; 46 | } 47 | 48 | /* tables still need 'cellspacing="0"' in the markup */ 49 | table { 50 | border-collapse: collapse; 51 | border-spacing: 0; 52 | } -------------------------------------------------------------------------------- /data/config.js: -------------------------------------------------------------------------------- 1 | module.exports = {"cfgdata":[{"Category":"Task Health","data":[{"Alias":"","cClose":null,"cDefault":false,"cfg_item":"Green","ID":16199,"SortOrder":0,"txtValue":null},{"Alias":"","cClose":null,"cDefault":false,"cfg_item":"Amber","ID":16200,"SortOrder":0,"txtValue":null},{"Alias":"","cClose":null,"cDefault":false,"cfg_item":"Red","ID":16201,"SortOrder":0,"txtValue":null}]},{"Category":"Task Status","data":[{"Alias":"","cClose":null,"cDefault":false,"cfg_item":"In Progress","ID":23,"SortOrder":1,"txtValue":null},{"Alias":"","cClose":true,"cDefault":false,"cfg_item":"Closed","ID":24,"SortOrder":2,"txtValue":null},{"Alias":"","cClose":null,"cDefault":false,"cfg_item":"Ready","ID":218,"SortOrder":0,"txtValue":null},{"Alias":"","cClose":false,"cDefault":true,"cfg_item":"Backlog","ID":16461,"SortOrder":1,"txtValue":null}]}],"resourcedata":[{"JobTitle":null,"UserId":1,"Username":"Greg Vandeligt"},{"JobTitle":null,"UserId":58,"Username":"James Gumley"},{"JobTitle":null,"UserId":65,"Username":"Management"},{"JobTitle":null,"UserId":68,"Username":"pmo user"}],"wodata":[{"data":[{"ID":43,"SortOrder":43,"WONumber":"WO10018"},{"ID":301,"SortOrder":301,"WONumber":"WO10296"},{"ID":313,"SortOrder":313,"WONumber":"WO10315"}],"WONumber":"WorkOrders"}]}; -------------------------------------------------------------------------------- /src/js/views/sideBar/DatePicker.js: -------------------------------------------------------------------------------- 1 | var DatePicker = React.createClass({ 2 | displayName: 'DatePicker', 3 | componentDidMount: function() { 4 | $(this.getDOMNode()).datepicker({ 5 | dateFormat: this.props.dateFormat, 6 | onSelect: function() { 7 | console.log('select'); 8 | var date = this.getDOMNode().value.split('/'); 9 | var value = new Date(date[2] + '-' + date[1] + '-' + date[0]); 10 | this.props.onChange({ 11 | target: { 12 | value: value 13 | } 14 | }); 15 | }.bind(this) 16 | }); 17 | $(this.getDOMNode()).datepicker('show'); 18 | }, 19 | componentWillUnmount: function() { 20 | $(this.getDOMNode()).datepicker('destroy'); 21 | }, 22 | shouldComponentUpdate: function() { 23 | var dateStr = $.datepicker.formatDate(this.props.dateFormat, this.props.value); 24 | this.getDOMNode().value = dateStr; 25 | $(this.getDOMNode()).datepicker( "refresh" ); 26 | return false; 27 | }, 28 | render: function() { 29 | return React.createElement('input', { 30 | defaultValue: $.datepicker.formatDate(this.props.dateFormat, this.props.value) 31 | }); 32 | } 33 | }); 34 | 35 | module.exports = DatePicker; 36 | -------------------------------------------------------------------------------- /src/js/utils/util.js: -------------------------------------------------------------------------------- 1 | var monthsCode=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; 2 | var _ = require('underscore'); 3 | 4 | module.exports.correctdate = function(str) { 5 | "use strict"; 6 | if (_.last(str) !== 'Z') { 7 | return str + 'Z'; 8 | } 9 | return str; 10 | }; 11 | 12 | module.exports.formatdata = function(val, type) { 13 | "use strict"; 14 | if (type === 'm') { 15 | return monthsCode[val]; 16 | } 17 | return val; 18 | }; 19 | 20 | module.exports.hfunc = function(pos) { 21 | "use strict"; 22 | return { 23 | x: pos.x, 24 | y: this.getAbsolutePosition().y 25 | }; 26 | }; 27 | 28 | function transformToAssocArray(prmstr) { 29 | var params = {}; 30 | var prmarr = prmstr.split('&'); 31 | var i, tmparr; 32 | for (i = 0; i < prmarr.length; i++) { 33 | tmparr = prmarr[i].split('='); 34 | params[tmparr[0]] = tmparr[1]; 35 | } 36 | return params; 37 | } 38 | 39 | module.exports.getURLParams = function() { 40 | if (typeof window === "undefined") { 41 | return {}; 42 | } 43 | var prmstr = window.location.search.substr(1); 44 | return prmstr !== null && prmstr !== '' ? transformToAssocArray(prmstr) : {}; 45 | }; 46 | 47 | module.exports.convertDateToUTC = function(date) { 48 | return new Date( 49 | date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 50 | date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds() 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /data/comments.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '1897' : [ 3 | { 4 | "Id":1, 5 | "Author":"Greg Vandeligt", 6 | "Comment":"hjkhjkhjkhkj", 7 | "UserAvatar":null, 8 | "Date":"2015-02-15T17:14:00", 9 | "ParentId":null, 10 | "CanDelete":false, 11 | "CanReply":true, 12 | "PartitNo":null, 13 | "vKey":null, 14 | "dtype":null, 15 | "ProjectRef":null, 16 | "ActionID":null, 17 | "UserID":null 18 | }, 19 | { 20 | "Id":2, 21 | "Author":"Greg Vandeligt", 22 | "Comment":"hjkhkjhkjhjkhjk", 23 | "UserAvatar":null, 24 | "Date":"2015-02-15T17:15:00", 25 | "ParentId":null, 26 | "CanDelete":false, 27 | "CanReply":true, 28 | "PartitNo":null, 29 | "vKey":null, 30 | "dtype":null, 31 | "ProjectRef":null, 32 | "ActionID":null, 33 | "UserID":null 34 | }, 35 | { 36 | "Id":3, 37 | "Author":"Greg Vandeligt", 38 | "Comment":"testing", 39 | "UserAvatar":null, 40 | "Date":"2015-02-16T12:22:00", 41 | "ParentId":23, 42 | "CanDelete":false, 43 | "CanReply":true, 44 | "PartitNo":null, 45 | "vKey":null, 46 | "dtype":null, 47 | "ProjectRef":null, 48 | "ActionID":null, 49 | "UserID":null 50 | } 51 | ]}; -------------------------------------------------------------------------------- /src/css/sortable.css: -------------------------------------------------------------------------------- 1 | body.dragging, body.dragging * { 2 | cursor: move !important; 3 | } 4 | 5 | .dragged { 6 | position: absolute; 7 | opacity: 0.5; 8 | z-index: 2000; 9 | } 10 | 11 | ol.sortable li.placeholder { 12 | /*position: relative;*/ 13 | /** More li styles **/ 14 | } 15 | ol.sortable li.placeholder:before { 16 | /*position: absolute;*/ 17 | /** Define arrowhead **/ 18 | } 19 | 20 | /* line 1, /Users/jonasvonandrian/jquery-sortable/source/css/jquery-sortable.css.sass */ 21 | body.dragging, body.dragging * { 22 | cursor: move !important; } 23 | 24 | /* line 4, /Users/jonasvonandrian/jquery-sortable/source/css/jquery-sortable.css.sass */ 25 | .dragged { 26 | position: absolute; 27 | top: 0; 28 | opacity: 0.5; 29 | z-index: 2000; } 30 | 31 | 32 | ol.sortable li { 33 | /*display: block;*/ 34 | /*margin: 2px;*/ 35 | /*padding: 2px;*/ 36 | /*border: 1px solid #cccccc;*/ 37 | /*color: #0088cc;*/ 38 | /*background: #eeeeee;*/ 39 | } 40 | 41 | ol.sortable li.placeholder { 42 | /*position: relative;*/ 43 | margin: 0; 44 | padding: 0; 45 | border: none; 46 | } 47 | 48 | ol.sortable li.placeholder:before { 49 | /*position: absolute;*/ 50 | /*z-index: 20;*/ 51 | content: "__________"; 52 | width: 250px; 53 | height: 10px; 54 | /*margin-top: 5px;*/ 55 | /*left: 10px;*/ 56 | /*top: 4px;*/ 57 | border: 1px dashed lightblue; 58 | /*border-left-color: red;*/ 59 | /*border-right: none;*/ 60 | } -------------------------------------------------------------------------------- /data/resources.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | "WBSID":1832, 4 | "Resources":[ 5 | { 6 | "ID":74, 7 | "TSActivate":null, 8 | "ResID":58 9 | }, 10 | { 11 | "ID":75, 12 | "TSActivate":null, 13 | "ResID":55 14 | }, 15 | { 16 | "ID":76, 17 | "TSActivate":null, 18 | "ResID":56 19 | }, 20 | { 21 | "ID":77, 22 | "TSActivate":null, 23 | "ResID":59 24 | } 25 | ] 26 | }, 27 | { 28 | "WBSID":1894, 29 | "Resources":[ 30 | { 31 | "ID":78, 32 | "TSActivate":null, 33 | "ResID":58 34 | }, 35 | { 36 | "ID":79, 37 | "TSActivate":null, 38 | "ResID":59 39 | }, 40 | { 41 | "ID":111, 42 | "TSActivate":true, 43 | "ResID":1 44 | } 45 | ] 46 | }, 47 | { 48 | "WBSID":1897, 49 | "Resources":[ 50 | { 51 | "ID":72, 52 | "TSActivate":null, 53 | "ResID":58 54 | }, 55 | { 56 | "ID":73, 57 | "TSActivate":null, 58 | "ResID":1 59 | } 60 | ] 61 | } 62 | ]; -------------------------------------------------------------------------------- /src/js/collections/ResourceReferenceCollection.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var ResourceReferenceModel = require('../models/ResourceReference'); 4 | 5 | var util = require('../utils/util'); 6 | var params = util.getURLParams(); 7 | 8 | var Collection = Backbone.Collection.extend({ 9 | url : 'api/resources/' + (params.project || 1) + '/' + (params.profile || 1), 10 | model: ResourceReferenceModel, 11 | idAttribute : 'ID', 12 | updateResourcesForTask : function(task) { 13 | // remove old references 14 | this.toArray().forEach(function(ref) { 15 | if (ref.get('WBSID').toString() !== task.id.toString()) { 16 | return; 17 | } 18 | var isOld = task.get('resources').indexOf(ref.get('ResID')); 19 | if (isOld) { 20 | ref.destroy(); 21 | } 22 | }, this); 23 | // add new references 24 | task.get('resources').forEach(function(resId) { 25 | var isExist = this.findWhere({ResID : resId}); 26 | if (!isExist) { 27 | this.add({ 28 | ResID : resId, 29 | WBSID : task.id.toString() 30 | }).save(); 31 | } 32 | }.bind(this)); 33 | }, 34 | parse : function(res) { 35 | var result = []; 36 | res.forEach(function(item) { 37 | item.Resources.forEach(function(resItem) { 38 | var obj = resItem; 39 | obj.WBSID = item.WBSID; 40 | result.push(obj); 41 | }); 42 | }); 43 | return result; 44 | } 45 | }); 46 | 47 | module.exports = Collection; 48 | 49 | -------------------------------------------------------------------------------- /src/js/models/ResourceReferenceCollection.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var ResourceReferenceModel = require('../models/ResourceReference'); 4 | 5 | var util = require('../utils/util'); 6 | var params = util.getURLParams(); 7 | 8 | var Collection = Backbone.Collection.extend({ 9 | url : 'api/resources/' + (params.project || 1) + '/' + (params.profile || 1), 10 | model: ResourceReferenceModel, 11 | idAttribute : 'ID', 12 | updateResourcesForTask : function(task) { 13 | // remove old references 14 | this.each(function(ref) { 15 | console.log(ref); 16 | if (ref.get('WBSID').toString() !== task.id.toString()) { 17 | return; 18 | } 19 | var isOld = task.get('resources').indexOf(ref.get('ResID')); 20 | if (isOld) { 21 | ref.destroy(); 22 | } 23 | }); 24 | // add new references 25 | task.get('resources').forEach(function(resId) { 26 | var isExist = this.findWhere({ResID : resId}); 27 | if (!isExist) { 28 | this.add({ 29 | ResID : resId, 30 | WBSID : task.id.toString() 31 | }).save(); 32 | } 33 | }.bind(this)); 34 | }, 35 | parse : function(res) { 36 | var result = []; 37 | res.forEach(function(item) { 38 | item.Resources.forEach(function(resItem) { 39 | var obj = resItem; 40 | obj.WBSID = item.WBSID; 41 | result.push(obj); 42 | }); 43 | }); 44 | console.log(result); 45 | 46 | return result; 47 | } 48 | }); 49 | 50 | module.exports = Collection; 51 | 52 | -------------------------------------------------------------------------------- /src/css/loader.css: -------------------------------------------------------------------------------- 1 | #loader { 2 | position: absolute; 3 | width: 100%; 4 | height: 100%; 5 | background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iNiIgaGVpZ2h0PSI2Ij4KPHJlY3Qgd2lkdGg9IjYiIGhlaWdodD0iNiIgZmlsbD0iI2VlZWVlZSI+PC9yZWN0Pgo8ZyBpZD0iYyI+CjxyZWN0IHdpZHRoPSIzIiBoZWlnaHQ9IjMiIGZpbGw9IiNlNmU2ZTYiPjwvcmVjdD4KPHJlY3QgeT0iMSIgd2lkdGg9IjMiIGhlaWdodD0iMiIgZmlsbD0iI2Q4ZDhkOCI+PC9yZWN0Pgo8L2c+Cjx1c2UgeGxpbms6aHJlZj0iI2MiIHg9IjMiIHk9IjMiPjwvdXNlPgo8L3N2Zz4="); 6 | z-index: 20000; 7 | } 8 | 9 | .spinner { 10 | margin: 100px auto 0; 11 | width: 70px; 12 | text-align: center; 13 | 14 | position: relative; 15 | top: 50%; 16 | left: 50%; 17 | margin-top: -20px; 18 | margin-left: -20px; 19 | } 20 | 21 | .spinner > div { 22 | width: 18px; 23 | height: 18px; 24 | background-color: #333; 25 | 26 | border-radius: 100%; 27 | display: inline-block; 28 | -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; 29 | animation: sk-bouncedelay 1.4s infinite ease-in-out both; 30 | } 31 | 32 | .spinner .bounce1 { 33 | -webkit-animation-delay: -0.32s; 34 | animation-delay: -0.32s; 35 | } 36 | 37 | .spinner .bounce2 { 38 | -webkit-animation-delay: -0.16s; 39 | animation-delay: -0.16s; 40 | } 41 | 42 | @-webkit-keyframes sk-bouncedelay { 43 | 0%, 80%, 100% { -webkit-transform: scale(0) } 44 | 40% { -webkit-transform: scale(1.0) } 45 | } 46 | 47 | @keyframes sk-bouncedelay { 48 | 0%, 80%, 100% { 49 | -webkit-transform: scale(0); 50 | transform: scale(0); 51 | } 40% { 52 | -webkit-transform: scale(1.0); 53 | transform: scale(1.0); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/js/app.js: -------------------------------------------------------------------------------- 1 | // require('babel/external-helpers'); 2 | 3 | import TaskCollection from './collections/TaskCollection'; 4 | import Settings from './models/SettingModel'; 5 | 6 | import GanttView from './views/GanttView'; 7 | import {tasksURL, configURL} from './clientConfig'; 8 | 9 | 10 | function loadTasks(tasks) { 11 | var dfd = new $.Deferred(); 12 | tasks.fetch({ 13 | success: function() { 14 | dfd.resolve(); 15 | }, 16 | error: function(err) { 17 | dfd.reject(err); 18 | }, 19 | parse: true, 20 | reset: true 21 | }); 22 | return dfd.promise(); 23 | } 24 | 25 | function loadSettings(settings) { 26 | return $.getJSON(configURL) 27 | .then((data) => { 28 | settings.statuses = data; 29 | }); 30 | } 31 | 32 | 33 | $(() => { 34 | let tasks = new TaskCollection(); 35 | tasks.url = tasksURL; 36 | let settings = new Settings({}, {tasks: tasks}); 37 | 38 | window.tasks = tasks; 39 | 40 | $.when(loadTasks(tasks)) 41 | .then(() => loadSettings(settings)) 42 | .then(() => { 43 | console.log('Success loading tasks.'); 44 | new GanttView({ 45 | settings: settings, 46 | collection: tasks 47 | }).render(); 48 | }) 49 | .then(() => { 50 | tasks.setDefaultStatusId(settings.getDefaultStatusId()); 51 | tasks.setClosedStatusId(settings.getClosedStatusId()); 52 | }) 53 | .then(() => { 54 | // hide loading 55 | $('#loader').fadeOut(function() { 56 | 57 | // display head always on top 58 | $('#head').css({ 59 | position: 'fixed' 60 | }); 61 | 62 | // enable scrolling 63 | $('body').removeClass('hold-scroll'); 64 | }); 65 | }).fail((error) => { 66 | console.error('Error while loading', error); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/libs/jquery.comment/jquery.comment.min.css: -------------------------------------------------------------------------------- 1 | .commentsBlock{overflow-y:auto;padding-right:5px}.commentsBlock form{margin-top:10px;margin-bottom:5px}.commentsBlock ul{list-style:none;margin:0;padding:0}.commentsBlock .comment .commentText{word-wrap:break-word;word-break:break-all;white-space:normal;white-space:-normal;white-space:-moz-normal;white-space:-o-normal;margin:5px 0 10px 0}.commentsBlock .newComment{padding:2px}.commentsBlock .heading{border-bottom:1px solid #dcdcdc}.commentsBlock .heading h4{float:left;color:#888;margin:0 5px 5px 0;float:left}.commentsBlock .heading .loadingIndicator{background:transparent url('./ajax-loader.gif') no-repeat;height:15px;width:15px;float:left;display:none}.commentsBlock .sendComment{color:#508bc0;font-size:12px}.commentsBlock .sendComment span{padding:0 5px 0 20px;background:transparent url('./add_comment_ico.png') no-repeat 0 4px;display:block}.commentsBlock textarea{width:98%;height:30px;margin-bottom:10px;height:40px}.commentsBlock .sendComment{color:#508bc0;font-size:12px}.comments{overflow-x:hidden;overflow-y:auto;list-style:none}.comments .comment{padding:10px 0;margin:0 20px;border-bottom:#eee solid 1px}.comments .comment .commentContent .avatar{margin-right:10px;float:left}.comments .comment .commentContent .avatar img{height:64px}.comments .comment .commentContent .avatar .defaultAvatar{background-image:url("/img/userDefaultImage.png");background-repeat:no-repeat;height:64px;width:64px}.comments .comment .commentContent .content.avatarPadding{padding-left:64px}.comments .comment .info{font-size:13px;color:#505050;margin:0}.comments .comment .info .author{font-weight:bold;font-size:12px}.comments .comment .info time{font-size:10px;color:#666;line-height:20px;height:20px;margin-right:5px}.comments .comment .info a{margin-left:5px}.comments li:last-child{border:0}.comments .comment .contentBlockComment{padding:0;border:0;margin-top:5px}.comments .comment .reply_comments{margin-top:10px}.comments .comment .reply_comments .comment{padding:5px 20px 5px 20px;margin-bottom:0;border-top:1px solid #eee;border-bottom-width:0;background:transparent url('./comment.gif') no-repeat 0 10px}.commentsBlock .clearFix{clear:both} -------------------------------------------------------------------------------- /src/js/views/canvasChart/NestedTaskView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by lavrton on 17.12.2014. 3 | */ 4 | "use strict"; 5 | var BasicTaskView = require('./BasicTaskView'); 6 | 7 | var NestedTaskView = BasicTaskView.extend({ 8 | _color : '#b3d1fc', 9 | _borderSize : 6, 10 | _barHeight : 10, 11 | _completeColor : '#336699', 12 | el : function() { 13 | var group = BasicTaskView.prototype.el.call(this); 14 | var leftBorder = new Konva.Line({ 15 | fill : this._color, 16 | y : this._topPadding + this._barHeight, 17 | points : [0, 0, this._borderSize * 1.5, 0, 0, this._borderSize], 18 | closed : true, 19 | name : 'leftBorder' 20 | }); 21 | group.add(leftBorder); 22 | var rightBorder = new Konva.Line({ 23 | fill : this._color, 24 | y : this._topPadding + this._barHeight, 25 | points : [-this._borderSize * 1.5, 0, 0, 0, 0, this._borderSize], 26 | closed : true, 27 | name : 'rightBorder' 28 | }); 29 | group.add(rightBorder); 30 | return group; 31 | }, 32 | _updateDates : function() { 33 | // group is moved 34 | // so we need to detect interval 35 | var attrs = this.settings.getSetting('attr'), 36 | boundaryMin=attrs.boundaryMin, 37 | daysWidth=attrs.daysWidth; 38 | 39 | var rect = this.el.find('.mainRect')[0]; 40 | var x = this.el.x() + rect.x(); 41 | var days1 = Math.floor(x / daysWidth); 42 | var newStart = boundaryMin.clone().addDays(days1); 43 | this.model.moveToStart(newStart); 44 | }, 45 | render : function() { 46 | var x = this._calculateX(); 47 | this.el.find('.leftBorder')[0].x(0); 48 | this.el.find('.rightBorder')[0].x(x.x2 - x.x1); 49 | var completeWidth = (x.x2 - x.x1) * this.model.get('complete') / 100; 50 | if (completeWidth > this._borderSize / 2) { 51 | this.el.find('.leftBorder')[0].fill(this._completeColor); 52 | } else { 53 | this.el.find('.leftBorder')[0].fill(this._color); 54 | } 55 | if ((x.x2 - x.x1) - completeWidth < this._borderSize / 2) { 56 | this.el.find('.rightBorder')[0].fill(this._completeColor); 57 | } else { 58 | this.el.find('.rightBorder')[0].fill(this._color); 59 | } 60 | 61 | BasicTaskView.prototype.render.call(this); 62 | return this; 63 | } 64 | }); 65 | 66 | module.exports = NestedTaskView; 67 | -------------------------------------------------------------------------------- /src/js/views/ResourcesEditor.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | var ResourceEditorView = Backbone.View.extend({ 5 | initialize : function(params) { 6 | this.settings = params.settings; 7 | }, 8 | render : function(pos) { 9 | var stagePos = $('#gantt-container').offset(); 10 | var fakeEl = $('
').appendTo('body'); 11 | fakeEl.css({ 12 | position : 'absolute', 13 | top : pos.y + stagePos.top + 'px', 14 | left : pos.x + stagePos.left + 'px' 15 | }); 16 | 17 | this.popup = $('.custom.popup'); 18 | fakeEl.popup({ 19 | popup : this.popup, 20 | on : 'hover', 21 | position : 'bottom left', 22 | onHidden : function() { 23 | this._saveData(); 24 | this.popup.off('.editor'); 25 | }.bind(this) 26 | }).popup('show'); 27 | 28 | this._addResources(); 29 | this.popup.find('.button').on('click.editor', function() { 30 | this.popup.popup('hide'); 31 | this._saveData(); 32 | this.popup.off('.editor'); 33 | }.bind(this)); 34 | 35 | this._fullData(); 36 | }, 37 | _addResources : function() { 38 | this.popup.empty(); 39 | var htmlString = ''; 40 | (this.settings.statuses.resourcedata || []).forEach(function(resource) { 41 | htmlString += '
' + 42 | '' + 43 | '' + 44 | '

'; 45 | }); 46 | htmlString +='
' + 47 | 'Close' + 48 | '
'; 49 | this.popup.append(htmlString); 50 | this.popup.find('.ui.checkbox').checkbox(); 51 | }, 52 | _fullData : function() { 53 | var popup = this.popup; 54 | this.model.get('resources').forEach(function(resource) { 55 | popup.find('[name="' + resource + '"]').prop('checked', true); 56 | }); 57 | }, 58 | _saveData : function() { 59 | var resources = []; 60 | this.popup.find('input').each(function(i, input) { 61 | var $input = $(input); 62 | if ($input.prop('checked')) { 63 | resources.push($input.attr('name')); 64 | } 65 | }.bind(this)); 66 | this.model.set('resources', resources); 67 | } 68 | }); 69 | 70 | module.exports = ResourceEditorView; 71 | -------------------------------------------------------------------------------- /src/js/views/CommentsView.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var util = require('../utils/util'); 3 | var params = util.getURLParams(); 4 | 5 | var CommentsView = Backbone.View.extend({ 6 | el : '#taskCommentsModal', 7 | initialize : function(params) { 8 | this.settings = params.settings; 9 | }, 10 | render : function() { 11 | this._fillData(); 12 | 13 | // open modal 14 | this.$el.modal({ 15 | onHidden : function() { 16 | $("#taskComments").empty(); 17 | $(document.body).removeClass('dimmable'); 18 | }.bind(this), 19 | onApprove : function() { 20 | console.log('onApprove'); 21 | }.bind(this), 22 | onHide : function() { 23 | console.log('onHide'); 24 | return false; 25 | }, 26 | onDeny : function() { 27 | console.log('onDeny'); 28 | return false; 29 | } 30 | }).modal('show'); 31 | 32 | var updateCount = function() { 33 | var count = $("#taskComments").comments("count"); 34 | this.model.set('Comments', count); 35 | }.bind(this); 36 | var callback = { 37 | afterDelete : updateCount, 38 | afterCommentAdd : updateCount 39 | }; 40 | if (window.location.hostname.indexOf('localhost') === -1) { 41 | // init comments 42 | $("#taskComments").comments({ 43 | getCommentsUrl: "/api/comment/" + this.model.id + "/" + params.sitekey + "/WBS/000", 44 | postCommentUrl: "/api/comment/" + this.model.id + "/" + params.sitekey + "/WBS/" + params.project, 45 | deleteCommentUrl: "/api/comment/" + this.model.id, 46 | displayAvatar: false, 47 | callback : callback 48 | }); 49 | } else { 50 | $("#taskComments").comments({ 51 | getCommentsUrl: "/api/comment/" + this.model.id, 52 | postCommentUrl: "/api/comment/" + this.model.id, 53 | deleteCommentUrl: "/api/comment/" + this.model.id, 54 | displayAvatar: false, 55 | callback : callback 56 | }); 57 | } 58 | }, 59 | _fillData : function() { 60 | _.each(this.model.attributes, function(val, key) { 61 | var input = this.$el.find('[name="' + key + '"]'); 62 | if (!input.length) { 63 | return; 64 | } 65 | input.val(val); 66 | }, this); 67 | } 68 | }); 69 | 70 | module.exports = CommentsView; 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Gantt.js Editor 2 | ============ 3 | 4 | A Gantt Project using Backbone, React and Konva 5 | 6 | # Version 1.0 Beta 7 | 8 | The version 1.0 development is in Beta a stage, the Gantt Editor plugin allows for complex 9 | Project Gantt charts, dependency and resource assignments. Data is being read from structured 10 | json files that should allow integration to any backend technologies such as .net, php, java etc. 11 | 12 | ## Functions available: 13 | * Add, edit, delete tasks 14 | * dependencies created between items 15 | * gantt bars moved to change dates 16 | * gantt bars resized 17 | * drag and drop rows 18 | * additional task properties in a modal window 19 | * left mouse click context menu 20 | * Indent / Outdent tasks to great grouping relationships 21 | * Add tasks above and below line item 22 | * Changes are automatically saved to the server through an REST api 23 | * Edit task resources 24 | 25 | ### User Interface: 26 | * different view options, Day, Week, Month, Quarter 27 | * Expandable table section for additional line item details 28 | * Highlight rows with attributes 29 | * Filter rows with attributes 30 | 31 | ### Video Link of Gantt Editor in Action 32 | [![Alt text for your video](http://img.youtube.com/vi/2EzHS1TR2v0/0.jpg)](http://www.youtube.com/watch?v=2EzHS1TR2v0) 33 | 34 | 35 | ### Authors and Contributors 36 | This project is being developed to be used in the Cloud Based Project, Portfolio Management application OneView PPM by Level35. 37 | 38 | We are currently looking for active contribition to complete this plugin. Please contact us if you have the skills and the time to contribute. 39 | 40 | ### LICENSE 41 | 42 | This software is offered under the MIT License 43 | 44 | Permission is hereby granted, free of charge, to any person obtaining a copy 45 | of this software and associated documentation files (the "Software"), to deal 46 | in the Software without restriction, including without limitation the rights 47 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 48 | copies of the Software, and to permit persons to whom the Software is 49 | furnished to do so, subject to the following conditions: 50 | 51 | The above copyright notice and this permission notice shall be included in 52 | all copies or substantial portions of the Software. 53 | 54 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 55 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 56 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 57 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 58 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 59 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 60 | THE SOFTWARE. 61 | -------------------------------------------------------------------------------- /src/js/views/sideBar/ContextMenuView.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var ModalEdit = require('../ModalTaskEditView'); 4 | var Comments = require('../CommentsView'); 5 | 6 | function ContextMenuView(params) { 7 | this.collection = params.collection; 8 | this.settings = params.settings; 9 | } 10 | 11 | ContextMenuView.prototype.render = function() { 12 | var self = this; 13 | $('.task-container').contextMenu({ 14 | selector: 'ul', 15 | callback: function(key) { 16 | var id = $(this).attr('id') || $(this).data('id'); 17 | var model = self.collection.get(id); 18 | if(key === 'delete'){ 19 | model.destroy(); 20 | } 21 | if(key === 'properties'){ 22 | var view = new ModalEdit({ 23 | model: model, 24 | settings: self.settings 25 | }); 26 | view.render(); 27 | } 28 | if(key === 'comments'){ 29 | new Comments({ 30 | model: model, 31 | settings: self.settings 32 | }).render(); 33 | } 34 | if (key === 'rowAbove'){ 35 | var data = { 36 | reference_id: id 37 | }; 38 | self.addTask(data, 'above'); 39 | } 40 | if(key === 'rowBelow'){ 41 | self.addTask({ 42 | reference_id : id 43 | }, 'below'); 44 | } 45 | if (key === 'indent') { 46 | self.collection.indent(model); 47 | } 48 | if (key === 'outdent'){ 49 | self.collection.outdent(model); 50 | } 51 | }, 52 | items: { 53 | "rowAbove": { name: " New Row Above", icon: "above" }, 54 | "rowBelow": { name: " New Row Below", icon: "below" }, 55 | "indent": { name: " Indent Row", icon: "indent" }, 56 | "outdent": { name: " Outdent Row", icon: "outdent" }, 57 | "sep1": "---------", 58 | "properties": { name: " Properties", icon: "properties" }, 59 | "comments": { name: " Comments", icon: "comment" }, 60 | "sep2": "---------", 61 | "delete": { name: " Delete Row", icon: "delete" } 62 | } 63 | }); 64 | }; 65 | 66 | ContextMenuView.prototype.addTask = function(data, insertPos) { 67 | var sortindex = 0; 68 | var ref_model = this.collection.get(data.reference_id); 69 | if (ref_model) { 70 | sortindex = ref_model.get('sortindex') + (insertPos === 'above' ? -0.5 : 0.5); 71 | } else { 72 | sortindex = (this.collection.last().get('sortindex') + 1); 73 | } 74 | data.sortindex = sortindex; 75 | data.parentid = ref_model.get('parentid'); 76 | var task = this.collection.add(data, {parse : true}); 77 | this.collection.checkSortedIndex(); 78 | task.save(); 79 | }; 80 | 81 | module.exports = ContextMenuView; -------------------------------------------------------------------------------- /src/js/views/canvasChart/ConnectorView.js: -------------------------------------------------------------------------------- 1 | var ConnectorView = Backbone.KonvaView.extend({ 2 | _color: 'grey', 3 | _wrongColor: 'red', 4 | initialize: function (params) { 5 | this.settings = params.settings; 6 | this.beforeModel = params.beforeModel; 7 | this.afterModel = params.afterModel; 8 | this._y1 = 0; 9 | this._y2 = 0; 10 | this._initSettingsEvents(); 11 | this._initModelEvents(); 12 | }, 13 | el: function() { 14 | var line = new Konva.Line({ 15 | strokeWidth: 2, 16 | stroke: 'black', 17 | points: [0, 0, 0, 0] 18 | }); 19 | return line; 20 | }, 21 | setY1: function(y1) { 22 | this._y1 = y1; 23 | this.render(); 24 | }, 25 | setY2: function(y2) { 26 | this._y2 = y2; 27 | this.render(); 28 | }, 29 | render: function() { 30 | var x = this._calculateX(); 31 | if (x.x2 >= x.x1) { 32 | this.el.stroke(this._color); 33 | this.el.points([x.x1, this._y1, x.x1 + 10, this._y1, x.x1 + 10, this._y2, x.x2, this._y2]); 34 | } else { 35 | this.el.stroke(this._wrongColor); 36 | this.el.points([ 37 | x.x1, this._y1, 38 | x.x1 + 10, this._y1, 39 | x.x1 + 10, this._y1 + (this._y2 - this._y1) / 2, 40 | x.x2 - 10, this._y1 + (this._y2 - this._y1) / 2, 41 | x.x2 - 10, this._y2, 42 | x.x2, this._y2 43 | ]); 44 | } 45 | this.el.getLayer().batchDraw(); 46 | return this; 47 | }, 48 | _initSettingsEvents: function() { 49 | this.listenTo(this.settings, 'change:interval change:dpi', function() { 50 | this.render(); 51 | }); 52 | }, 53 | _initModelEvents: function() { 54 | this.listenTo(this.beforeModel, 'change', function() { 55 | this.render(); 56 | }); 57 | 58 | this.listenTo(this.beforeModel, 'change:hidden', function() { 59 | if (this.beforeModel.get('hidden')) { 60 | this.el.hide(); 61 | } else { 62 | this.el.show(); 63 | } 64 | }); 65 | this.listenTo(this.afterModel, 'change', function() { 66 | this.render(); 67 | }); 68 | 69 | this.listenTo(this.afterModel, 'change:hidden', function() { 70 | if (this.beforeModel.get('hidden')) { 71 | this.el.hide(); 72 | } else { 73 | this.el.show(); 74 | } 75 | }); 76 | }, 77 | _calculateX: function() { 78 | var attrs = this.settings.getSetting('attr'), 79 | boundaryMin = attrs.boundaryMin, 80 | daysWidth = attrs.daysWidth; 81 | return { 82 | x1: Date.daysdiff(boundaryMin, this.beforeModel.get('end')) * daysWidth, 83 | x2: Date.daysdiff(boundaryMin, this.afterModel.get('start')) * daysWidth 84 | }; 85 | } 86 | }); 87 | 88 | module.exports = ConnectorView; 89 | -------------------------------------------------------------------------------- /src/js/views/sideBar/NestedTask.js: -------------------------------------------------------------------------------- 1 | var TaskItem = require('./TaskItem'); 2 | 3 | var NestedTask = React.createClass({ 4 | displayName: 'NestedTask', 5 | componentDidMount: function() { 6 | this.props.model.on('change:hidden change:collapsed', function() { 7 | this.forceUpdate(); 8 | }, this); 9 | }, 10 | componentWillUnmount: function() { 11 | this.props.model.off(null, null, this); 12 | }, 13 | render: function() { 14 | var subtasks = this.props.model.children.map((task) => { 15 | if (task.get('hidden')) { 16 | return; 17 | } 18 | if (task.children.length) { 19 | return React.createElement(NestedTask, { 20 | model: task, 21 | isSubTask: true, 22 | key: task.cid, 23 | dateFormat: this.props.dateFormat, 24 | onSelectRow: this.props.onSelectRow, 25 | onEditRow: this.props.onEditRow, 26 | editedRow: this.props.editedRow, 27 | selectedRow: this.props.selectedRow, 28 | selectedModelCid: this.props.selectedModelCid, 29 | getAllStatuses: this.props.getAllStatuses, 30 | getStatusId: this.props.getStatusId 31 | }); 32 | } 33 | return React.createElement('li', { 34 | id: task.cid, 35 | key: task.cid, 36 | className: 'drag-item', 37 | 'data-id': task.cid 38 | }, 39 | React.createElement(TaskItem, { 40 | model: task, 41 | isSubTask: true, 42 | dateFormat: this.props.dateFormat, 43 | onSelectRow: this.props.onSelectRow, 44 | onEditRow: this.props.onEditRow, 45 | editedRow: (this.props.selectedModelCid === task.cid) && this.props.editedRow, 46 | selectedRow: (this.props.selectedModelCid === task.cid) && this.props.selectedRow, 47 | getAllStatuses: this.props.getAllStatuses, 48 | getStatusId: this.props.getStatusId 49 | }) 50 | ); 51 | }); 52 | return React.createElement('li', { 53 | className: 'task-list-container drag-item' + (this.props.isSubTask ? ' sub-task' : ''), 54 | id: this.props.model.cid, 55 | 'data-id': this.props.model.cid 56 | }, 57 | React.createElement('div', { 58 | id: this.props.model.cid, 59 | 'data-id': this.props.model.cid 60 | }, 61 | React.createElement(TaskItem, { 62 | model: this.props.model, 63 | dateFormat: this.props.dateFormat, 64 | onSelectRow: this.props.onSelectRow, 65 | onEditRow: this.props.onEditRow, 66 | editedRow: (this.props.selectedModelCid === this.props.model.cid) && this.props.editedRow, 67 | selectedRow: (this.props.selectedModelCid === this.props.model.cid) && this.props.selectedRow, 68 | getAllStatuses: this.props.getAllStatuses, 69 | getStatusId: this.props.getStatusId 70 | }) 71 | ), 72 | React.createElement('ol', { 73 | className: 'sub-task-list sortable' 74 | }, 75 | subtasks 76 | ) 77 | ); 78 | } 79 | }); 80 | 81 | module.exports = NestedTask; 82 | -------------------------------------------------------------------------------- /src/js/views/TopMenuView/FilterMenuView.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var FilterView = Backbone.View.extend({ 4 | el : '#filter-menu', 5 | initialize : function(params) { 6 | this.settings = params.settings; 7 | }, 8 | events : { 9 | 'change #hightlights-select' : function(e) { 10 | var hightlightTasks = this._getModelsForCriteria(e.target.value); 11 | this.collection.each(function(task) { 12 | if (hightlightTasks.indexOf(task) > -1) { 13 | task.set('hightlight', this.colors[e.target.value]); 14 | } else { 15 | task.set('hightlight', undefined); 16 | } 17 | }, this); 18 | }, 19 | 'change #filters-select' : function(e) { 20 | var criteria = e.target.value; 21 | if (criteria === 'reset') { 22 | this.collection.each(function(task) { 23 | task.show(); 24 | }); 25 | } else { 26 | var showTasks = this._getModelsForCriteria(e.target.value); 27 | this.collection.each(function(task) { 28 | if (showTasks.indexOf(task) > -1) { 29 | task.show(); 30 | // show all parents 31 | var parent = task.parent; 32 | while(parent) { 33 | parent.show(); 34 | parent = parent.parent; 35 | } 36 | } else { 37 | task.hide(); 38 | } 39 | }); 40 | } 41 | } 42 | }, 43 | colors : { 44 | 'status-backlog' : '#D2D2D9', 45 | 'status-ready' : '#B2D1F0', 46 | 'status-in progress' : '#66A3E0', 47 | 'status-complete' : '#99C299', 48 | 'late' : '#FFB2B2', 49 | 'due' : ' #FFC299', 50 | 'milestone' : '#D6C2FF', 51 | 'deliverable' : '#E0D1C2', 52 | 'financial' : '#F0E0B2', 53 | 'timesheets' : '#C2C2B2', 54 | 'reportable' : ' #E0C2C2', 55 | 'health-red' : 'red', 56 | 'health-amber' : '#FFBF00', 57 | 'health-green' : 'green' 58 | }, 59 | _getModelsForCriteria : function(creteria) { 60 | if (creteria === 'resets') { 61 | return []; 62 | } 63 | if (creteria.indexOf('status') !== -1) { 64 | var status = creteria.slice(creteria.indexOf('-') + 1); 65 | var id = (this.settings.findStatusId(status) || '').toString(); 66 | return this.collection.filter(function(task) { 67 | return task.get('status').toString() === id; 68 | }); 69 | } 70 | if (creteria === 'late') { 71 | return this.collection.filter(function(task) { 72 | return task.get('end') < new Date(); 73 | }); 74 | } 75 | if (creteria === 'due') { 76 | var lastDate = new Date(); 77 | lastDate.addWeeks(2); 78 | return this.collection.filter(function(task) { 79 | return task.get('end') > new Date() && task.get('end') < lastDate; 80 | }); 81 | } 82 | if (['milestone', 'deliverable', 'financial', 'timesheets', 'reportable'].indexOf(creteria) !== -1) { 83 | return this.collection.filter(function(task) { 84 | return task.get(creteria); 85 | }); 86 | } 87 | if (creteria.indexOf('health') !== -1) { 88 | var health = creteria.slice(creteria.indexOf('-') + 1); 89 | var healthId = (this.settings.findHealthId(health) || '').toString(); 90 | return this.collection.filter(function(task) { 91 | return task.get('health').toString() === healthId; 92 | }); 93 | } 94 | return []; 95 | } 96 | }); 97 | 98 | module.exports = FilterView; 99 | -------------------------------------------------------------------------------- /src/js/views/canvasChart/AloneTaskView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by lavrton on 17.12.2014. 3 | */ 4 | "use strict"; 5 | var BasicTaskView = require('./BasicTaskView'); 6 | 7 | var AloneTaskView = BasicTaskView.extend({ 8 | _borderWidth : 3, 9 | _color : '#E6F0FF', 10 | events : function() { 11 | return _.extend(BasicTaskView.prototype.events(), { 12 | 'dragmove .leftBorder' : '_changeSize', 13 | 'dragmove .rightBorder' : '_changeSize', 14 | 15 | 'dragend .leftBorder' : 'render', 16 | 'dragend .rightBorder' : 'render', 17 | 18 | 'mouseover .leftBorder' : '_resizePointer', 19 | 'mouseout .leftBorder' : '_defaultMouse', 20 | 21 | 'mouseover .rightBorder' : '_resizePointer', 22 | 'mouseout .rightBorder' : '_defaultMouse' 23 | }); 24 | }, 25 | el : function() { 26 | var group = BasicTaskView.prototype.el.call(this); 27 | var leftBorder = new Konva.Rect({ 28 | dragBoundFunc : function(pos) { 29 | var offset = this.el.getStage().x() + this.el.x(); 30 | var localX = pos.x - offset; 31 | return { 32 | x : Math.min(localX, this.el.find('.rightBorder')[0].x()) + offset, 33 | y : this._y + this._topPadding 34 | }; 35 | }.bind(this), 36 | width : this._borderWidth, 37 | fill : 'black', 38 | y : this._topPadding, 39 | height : this._barHeight, 40 | draggable : true, 41 | name : 'leftBorder' 42 | }); 43 | group.add(leftBorder); 44 | var rightBorder = new Konva.Rect({ 45 | dragBoundFunc : function(pos) { 46 | var offset = this.el.getStage().x() + this.el.x(); 47 | var localX = pos.x - offset; 48 | return { 49 | x : Math.max(localX, this.el.find('.leftBorder')[0].x()) + offset, 50 | y : this._y + this._topPadding 51 | }; 52 | }.bind(this), 53 | width : this._borderWidth, 54 | fill : 'black', 55 | y : this._topPadding, 56 | height : this._barHeight, 57 | draggable : true, 58 | name : 'rightBorder' 59 | }); 60 | group.add(rightBorder); 61 | return group; 62 | }, 63 | _resizePointer : function() { 64 | document.body.style.cursor = 'ew-resize'; 65 | }, 66 | _changeSize : function() { 67 | var leftX = this.el.find('.leftBorder')[0].x(); 68 | var rightX = this.el.find('.rightBorder')[0].x() + this._borderWidth; 69 | 70 | var rect = this.el.find('.mainRect')[0]; 71 | rect.width(rightX - leftX); 72 | rect.x(leftX); 73 | 74 | // update complete params 75 | var completeRect = this.el.find('.completeRect')[0]; 76 | completeRect.x(leftX); 77 | completeRect.width(this._calculateCompleteWidth()); 78 | 79 | // move tool position 80 | var tool = this.el.find('.dependencyTool')[0]; 81 | tool.x(rightX); 82 | var resources = this.el.find('.resources')[0]; 83 | resources.x(rightX + this._toolbarOffset); 84 | 85 | this._updateDates(); 86 | }, 87 | render : function() { 88 | var x = this._calculateX(); 89 | this.el.find('.leftBorder')[0].x(0); 90 | this.el.find('.rightBorder')[0].x(x.x2 - x.x1 - this._borderWidth); 91 | if (this.model.get('milestone')) { 92 | this.el.find('.diamond').show(); 93 | this.el.find('.mainRect').hide(); 94 | this.el.find('.completeRect').hide(); 95 | this.el.find('.leftBorder').hide(); 96 | this.el.find('.rightBorder').hide(); 97 | } else { 98 | this.el.find('.diamond').hide(); 99 | this.el.find('.mainRect').show(); 100 | this.el.find('.completeRect').show(); 101 | this.el.find('.leftBorder').show(); 102 | this.el.find('.rightBorder').show(); 103 | } 104 | BasicTaskView.prototype.render.call(this); 105 | return this; 106 | } 107 | }); 108 | 109 | module.exports = AloneTaskView; 110 | -------------------------------------------------------------------------------- /src/js/views/GanttView.js: -------------------------------------------------------------------------------- 1 | var ContextMenuView = require('./sideBar/ContextMenuView'); var SidePanel = require('./sideBar/SidePanel'); var GanttChartView = require('./canvasChart/GanttChartView'); var TopMenuView = require('./TopMenuView/TopMenuView'); var Notifications = require('./Notifications'); var GanttView = Backbone.View.extend({ el: '.Gantt', initialize: function(params) { this.settings = params.settings; this.$el.find('input[name="end"],input[name="start"]').on('change', this.calculateDuration); this.$menuContainer = this.$el.find('.menu-container'); new ContextMenuView({ collection: this.collection, settings: this.settings }).render(); // new task button $('.new-task').click(function() { var lastTask = params.collection.last(); var lastIndex = -1; if (lastTask) { lastIndex = lastTask.get('sortindex'); } params.collection.add({ name: 'New task', sortindex: lastIndex + 1 }); }); new Notifications({ collection: this.collection }); new TopMenuView({ settings: this.settings, collection: this.collection }).render(); this.canvasView = new GanttChartView({ collection: this.collection, settings: this.settings }); this.canvasView.render(); this._moveCanvasView(); setTimeout(function() { this.canvasView._updateStageAttrs(); // set side tasks panel height var $sidePanel = $('.menu-container'); $sidePanel.css({ 'min-height': window.innerHeight - $sidePanel.offset().top }); }.bind(this), 500); var tasksContainer = $('.tasks').get(0); React.render( React.createElement(SidePanel, { collection: this.collection, settings: this.settings, dateFormat: this.settings.getDateFormat() }), tasksContainer ); this.listenTo(this.collection, 'sort', _.debounce(function() { React.unmountComponentAtNode(tasksContainer); React.render( React.createElement(SidePanel, { collection: this.collection, settings: this.settings, dateFormat: this.settings.getDateFormat() }), tasksContainer ); }.bind(this),5)); window.addEventListener('scroll', () => { var y = Math.max(0, document.body.scrollTop || window.scrollY); $('.menu-header').css({ marginTop: (y) + 'px' }); $('.tasks').css({ marginTop: '-' + y + 'px' }); }); }, events: { 'click #tHandle': 'expand', 'click #deleteAll': 'deleteAll' }, calculateDuration: function(){ // Calculating the duration from start and end date var startdate = new Date($(document).find('input[name="start"]').val()); var enddate = new Date($(document).find('input[name="end"]').val()); var _MS_PER_DAY = 1000 * 60 * 60 * 24; if(startdate !== "" && enddate !== ""){ var utc1 = Date.UTC(startdate.getFullYear(), startdate.getMonth(), startdate.getDate()); var utc2 = Date.UTC(enddate.getFullYear(), enddate.getMonth(), enddate.getDate()); $(document).find('input[name="duration"]').val(Math.floor((utc2 - utc1) / _MS_PER_DAY)); }else{ $(document).find('input[name="duration"]').val(Math.floor(0)); } }, expand: function(evt) { var button = $(evt.target); if (button.hasClass('contract')) { this.$menuContainer.addClass('panel-collapsed'); this.$menuContainer.removeClass('panel-expanded'); } else { this.$menuContainer.addClass('panel-expanded'); this.$menuContainer.removeClass('panel-collapsed'); } setTimeout(function() { this._moveCanvasView(); }.bind(this), 600); button.toggleClass('contract'); }, _moveCanvasView: function() { var sideBarWidth = $('.menu-container').width(); this.canvasView.setLeftPadding(sideBarWidth); }, deleteAll: function() { $('#confirm').modal({ onHidden: function() { $(document.body).removeClass('dimmable'); }, onApprove: function() { while(this.collection.at(0)) { this.collection.at(0).destroy(); } }.bind(this) }).modal('show'); } }); module.exports = GanttView; -------------------------------------------------------------------------------- /src/libs/backbone.KonvaView.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | 3 | if (typeof define === 'function' && define.amd) { 4 | define(['underscore', 'backbone', 'konva'], function(_, Backbone, Konva) { 5 | Backbone.KonvaView = factory(root, _, Backbone, Konva); 6 | return Backbone; 7 | }); 8 | } else if (typeof exports !== 'undefined') { 9 | var Backbone_ = require('backbone'); 10 | Backbone_.KonvaView = factory(root, require('underscore'), Backbone_, require('konva')); 11 | module.exports = Backbone_.KonvaView; 12 | 13 | } else { 14 | root.Backbone.KonvaView = factory(root, _, Backbone, Konva); 15 | 16 | } 17 | 18 | }(this, function(root, _, Backbone, Konva) { 19 | 20 | var KonvaView = function(options) { 21 | this.cid = _.uniqueId('view'); 22 | options || (options = {}); 23 | _.extend(this, _.pick(options, viewOptions)); 24 | this._ensureElement(); 25 | this.initialize.apply(this, arguments); 26 | }; 27 | KonvaView.extend = Backbone.Model.extend; 28 | var delegateEventSplitter = /^(\S+)\s*(.*)$/; 29 | var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'events', 'nodeType']; 30 | 31 | _.extend(KonvaView.prototype, Backbone.Events, { 32 | nodeType : 'Group', 33 | 34 | initialize: function(){}, 35 | 36 | render : function(){ 37 | return this; 38 | }, 39 | 40 | remove: function() { 41 | this.undelegateEvents(); 42 | this.el.destroy(); 43 | this.stopListening(); 44 | return this; 45 | }, 46 | 47 | setElement: function(element) { 48 | this.undelegateEvents(); 49 | this.el = element; 50 | this.delegateEvents(); 51 | return this; 52 | }, 53 | 54 | _deleteEventFromNode : function(node) { 55 | var _this = this; 56 | _.each(_.keys(node.eventListeners),function(eventType){ 57 | node.off(eventType+'.delegateEvents' + _this.cid); 58 | }); 59 | }, 60 | undelegateEvents: function() { 61 | var _this = this; 62 | if (!this.el) { 63 | return this; 64 | } 65 | this._deleteEventFromNode(this.el); 66 | _.each(this.el.children, function(child){ 67 | _this._deleteEventFromNode(child); 68 | }); 69 | return this; 70 | }, 71 | undelegate: function(eventName, selector) { 72 | eventName += '.delegateEvents' + this.cid; 73 | if (selector === '' || !selector) { 74 | this.el.off(eventName); 75 | } else { 76 | this.el.find && this.el.find(selector).each(function(child){ 77 | child.off(eventName); 78 | }); 79 | } 80 | }, 81 | delegateEvents: function(events) { 82 | if (!(events || (events = _.result(this, 'events')))) return this; 83 | this.undelegateEvents(); 84 | for (var key in events) { 85 | var method = events[key]; 86 | if (!_.isFunction(method)) method = this[events[key]]; 87 | if (!method) continue; 88 | var match = key.match(delegateEventSplitter); 89 | this.delegate(match[1], match[2], _.bind(method, this)); 90 | } 91 | return this; 92 | }, 93 | delegate: function(eventName, selector, listener) { 94 | // listener = selector || listener; 95 | eventName += '.delegateEvents' + this.cid; 96 | if (selector === '') { 97 | this.el.on(eventName, listener); 98 | } else if (!listener) { 99 | this.el.on(eventName, selector); 100 | } else { 101 | this.el.find && this.el.find(selector).each(function(child){ 102 | child.on(eventName, listener); 103 | }); 104 | } 105 | }, 106 | _configure: function(options) { 107 | if (this.options) options = _.extend({}, _.result(this, 'options'), options); 108 | _.extend(this, _.pick(options, viewOptions)); 109 | this.options = options; 110 | }, 111 | _createElement: function(nodeType) { 112 | return new Konva[nodeType]; 113 | }, 114 | _ensureElement: function() { 115 | if (!this.el) { 116 | var attrs = _.extend({}, _.result(this, 'attributes')); 117 | if (this.id) attrs.id = _.result(this, 'id'); 118 | this.setElement(this._createElement(_.result(this, 'nodeType'))); 119 | this._setAttributes(attrs); 120 | } else { 121 | this.setElement(_.result(this, 'el')); 122 | } 123 | }, 124 | _setAttributes: function(attributes) { 125 | this.el.setAttrs(attributes); 126 | } 127 | }); 128 | return KonvaView; 129 | })); 130 | -------------------------------------------------------------------------------- /test_server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var bodyParser = require('body-parser'); 3 | var logger = require('morgan'); 4 | var _ = require('lodash'); 5 | var serveStatic = require('serve-static'); 6 | 7 | var tasks = require('./data/tasks'); 8 | 9 | // _.times(2, function() { 10 | // tasks = tasks.concat(JSON.parse(JSON.stringify(tasks))); 11 | // }); 12 | 13 | 14 | var resources = require('./data/resources'); 15 | var comments = require('./data/comments'); 16 | 17 | var config = require('./data/config'); 18 | 19 | var taskIdCounter = 1; 20 | var ids = []; 21 | _(tasks).each(function(task) { 22 | if (!task.id || ids.indexOf(task.id) >= 0) { 23 | task.id = Math.round(Math.random() * 100000); 24 | } 25 | ids.push(task.id); 26 | taskIdCounter = Math.max(parseInt(task.id), taskIdCounter); 27 | }); 28 | 29 | 30 | var resourceIdCounter = 0; 31 | _(resources).each(function(resource) { 32 | if (!resource.id) { 33 | resource.id = resourceIdCounter++; 34 | } 35 | resourceIdCounter = Math.max(parseInt(resource.id), resourceIdCounter); 36 | }); 37 | var commentsIdCounter = 3; 38 | 39 | var app = express(); 40 | 41 | app.use(serveStatic('./src')); 42 | app.use(bodyParser.json()); 43 | app.use(bodyParser.urlencoded({extended: true})); 44 | app.use(logger('dev')); 45 | 46 | 47 | 48 | app.get('/', function(req, res) { 49 | res.send('please select a collection, e.g., /collections/messages'); 50 | }); 51 | 52 | function generateAPI(items, baseURL) { 53 | app.get(baseURL, function(req, res) { 54 | console.log('return items ', baseURL); 55 | res.send(items); 56 | }); 57 | 58 | 59 | app.post(baseURL, function(req, res) { 60 | req.body.id = ++taskIdCounter; 61 | console.log('add item', req.body); 62 | items.push(req.body); 63 | return res.send(req.body); 64 | }); 65 | 66 | app.get(baseURL + '/:id', function(req, res) { 67 | var id = req.params.id.toString(); 68 | console.log('return item with id ' + id); 69 | 70 | var task = _.find(items, function(item) { 71 | return item.id.toString() === id.toString(); 72 | }); 73 | 74 | res.send(task || null); 75 | }); 76 | 77 | app.put(baseURL + '/:id', function(req, res) { 78 | var id = req.params.id.toString(); 79 | console.log('update item with id ' + id); 80 | var item = _(items).find(function(i) { 81 | return i.id.toString() === id; 82 | }); 83 | 84 | if (item) { 85 | _(req.body).each(function(val, key) { 86 | item[key] = val; 87 | }); 88 | _(item).each(function(val, key) { 89 | if (!req.body[key]) { 90 | item[key] = undefined; 91 | } 92 | }); 93 | } else { 94 | console.error('no such item'); 95 | } 96 | res.send(item || {}); 97 | }); 98 | 99 | app.delete(baseURL + '/:id', function(req, res) { 100 | console.log(req.params); 101 | var id = req.params.id.toString(); 102 | 103 | console.log('delete item with id ' + req.params.id); 104 | 105 | var item = _(items).find(function(i) { 106 | return i.id.toString() === id; 107 | }); 108 | 109 | if (item) { 110 | items = _.without(items, item); 111 | res.send({ 112 | msg: 'success' 113 | }); 114 | } else { 115 | console.error('no such task'); 116 | res.send({ 117 | msg: 'error' 118 | }); 119 | } 120 | }); 121 | } 122 | 123 | generateAPI(tasks, '/api/tasks'); 124 | generateAPI(resources, '/api/resources/1/1'); 125 | 126 | 127 | 128 | app.get('/api/comment/:id/', function(req, res) { 129 | var id = req.params.id.toString(); 130 | console.log('return comments with id ' + id); 131 | res.send(comments[id] || []); 132 | }); 133 | 134 | app.get('/api/GanttConfig', function(req, res) { 135 | res.send(config); 136 | }); 137 | 138 | 139 | app.post('/api/comment/:id/', function(req, res) { 140 | var id = req.params.id.toString(); 141 | console.log('add comment', req.body, 'to task', id); 142 | var comment = { 143 | Comment: req.body.comment, 144 | Id: ++commentsIdCounter, 145 | Author: 'User', 146 | Date: new Date(), 147 | UserAvatar: null, 148 | ParentId: null, 149 | CanDelete: false, 150 | CanReply: true, 151 | PartitNo: null, 152 | vKey: null, 153 | dtype: null, 154 | ProjectRef: null, 155 | ActionID: null, 156 | UserID: null 157 | }; 158 | comments[id] = comments[id] || []; 159 | comments[id].push(comment); 160 | return res.send(comment); 161 | }); 162 | 163 | 164 | app.listen(3000, function(){ 165 | console.log('Express server listening on port 3000'); 166 | }); 167 | -------------------------------------------------------------------------------- /src/css/jquery.contextMenu.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery contextMenu - Plugin for simple contextMenu handling 3 | * 4 | * Version: git-master 5 | * 6 | * Authors: Rodney Rehm, Addy Osmani (patches for FF) 7 | * Web: http://medialize.github.com/jQuery-contextMenu/ 8 | * 9 | * Licensed under 10 | * MIT License http://www.opensource.org/licenses/mit-license 11 | * GPL v3 http://opensource.org/licenses/GPL-3.0 12 | * 13 | */ 14 | 15 | .context-menu-list { 16 | margin:0; 17 | padding:0; 18 | 19 | min-width: 120px; 20 | max-width: 250px; 21 | display: inline-block; 22 | position: absolute; 23 | list-style-type: none; 24 | 25 | border: 1px solid #DDD; 26 | background: #EEE; 27 | 28 | -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); 29 | -moz-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); 30 | -ms-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); 31 | -o-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); 32 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); 33 | 34 | font-family: Verdana, Arial, Helvetica, sans-serif; 35 | font-size: 11px; 36 | } 37 | 38 | .context-menu-item { 39 | padding: 5px 2px 5px 20px; 40 | background-color: #EEE; 41 | position: relative; 42 | -webkit-user-select: none; 43 | -moz-user-select: -moz-none; 44 | -ms-user-select: none; 45 | user-select: none; 46 | } 47 | 48 | .context-menu-separator { 49 | padding-bottom:0; 50 | border-bottom: 1px solid #DDD; 51 | } 52 | 53 | .context-menu-item > label > input, 54 | .context-menu-item > label > textarea { 55 | -webkit-user-select: text; 56 | -moz-user-select: text; 57 | -ms-user-select: text; 58 | user-select: text; 59 | } 60 | 61 | .context-menu-item.hover { 62 | cursor: pointer; 63 | background-color: #39F; 64 | } 65 | 66 | .context-menu-item.disabled { 67 | color: #666; 68 | } 69 | 70 | .context-menu-input.hover, 71 | .context-menu-item.disabled.hover { 72 | cursor: default; 73 | background-color: #EEE; 74 | } 75 | 76 | .context-menu-submenu:after { 77 | content: ">"; 78 | color: #666; 79 | position: absolute; 80 | top: 0; 81 | right: 3px; 82 | z-index: 1; 83 | } 84 | 85 | /* icons 86 | #protip: 87 | In case you want to use sprites for icons (which I would suggest you do) have a look at 88 | http://css-tricks.com/13224-pseudo-spriting/ to get an idea of how to implement 89 | .context-menu-item.icon:before {} 90 | */ 91 | .context-menu-item.icon { min-height: 18px; background-repeat: no-repeat; background-position: 4px 2px; } 92 | .context-menu-item.icon-edit { background-image: url(images/page_white_edit.png); } 93 | .context-menu-item.icon-cut { background-image: url(images/cut.png); } 94 | .context-menu-item.icon-copy { background-image: url(images/page_white_copy.png); } 95 | .context-menu-item.icon-paste { background-image: url(images/page_white_paste.png); } 96 | .context-menu-item.icon-delete { background-image: url(images/page_white_delete.png); } 97 | .context-menu-item.icon-add { background-image: url(images/page_white_add.png); } 98 | .context-menu-item.icon-quit { background-image: url(images/door.png); } 99 | .context-menu-item.icon-outdent { background-image: url(images/arrow_left.png); } 100 | .context-menu-item.icon-indent { background-image: url(images/arrow_right.png); } 101 | .context-menu-item.icon-above { background-image: url(images/top.png); } 102 | .context-menu-item.icon-below { background-image: url(images/bottom.png); } 103 | .context-menu-item.icon-properties { background-image: url(images/properties.png); } 104 | .context-menu-item.icon-comment { background-image: url(images/comment.png); } 105 | 106 | /* vertically align inside labels */ 107 | .context-menu-input > label > * { vertical-align: top; } 108 | 109 | /* position checkboxes and radios as icons */ 110 | .context-menu-input > label > input[type="checkbox"], 111 | .context-menu-input > label > input[type="radio"] { 112 | margin-left: -17px; 113 | } 114 | .context-menu-input > label > span { 115 | margin-left: 5px; 116 | } 117 | 118 | .context-menu-input > label, 119 | .context-menu-input > label > input[type="text"], 120 | .context-menu-input > label > textarea, 121 | .context-menu-input > label > select { 122 | display: block; 123 | width: 100%; 124 | 125 | -webkit-box-sizing: border-box; 126 | -moz-box-sizing: border-box; 127 | -ms-box-sizing: border-box; 128 | -o-box-sizing: border-box; 129 | box-sizing: border-box; 130 | } 131 | 132 | .context-menu-input > label > textarea { 133 | height: 100px; 134 | } 135 | .context-menu-item > .context-menu-list { 136 | display: none; 137 | /* re-positioned by js */ 138 | right: -5px; 139 | top: 5px; 140 | } 141 | 142 | .context-menu-item.hover > .context-menu-list { 143 | display: block; 144 | } 145 | 146 | .context-menu-accesskey { 147 | text-decoration: underline; 148 | } 149 | -------------------------------------------------------------------------------- /src/js/utils/xmlWorker.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var xml = fs.readFileSync(__dirname + '/xmlTemplate.xml', 'utf8'); 3 | var compiled = _.template(xml); 4 | var xmlToJSON = window.xmlToJSON; 5 | 6 | function parseXMLObj(xmlString) { 7 | var obj = xmlToJSON.parseString(xmlString); 8 | var tasks = []; 9 | _.each(obj.Project[0].Tasks[0].Task, function(xmlItem) { 10 | if (!xmlItem.Name) { 11 | return; 12 | // xmlItem.Name = [{_text: 'no name ' + xmlItem.UID[0]._text}]; 13 | } 14 | // skip root project task 15 | if (xmlItem.OutlineNumber[0]._text.toString() === '0') { 16 | return; 17 | } 18 | tasks.push({ 19 | name: xmlItem.Name[0]._text, 20 | start: xmlItem.Start[0]._text, 21 | end: xmlItem.Finish[0]._text, 22 | complete: xmlItem.PercentComplete[0]._text, 23 | outline: xmlItem.OutlineNumber[0]._text.toString() 24 | }); 25 | }); 26 | return tasks; 27 | } 28 | 29 | module.exports.parseDepsFromXML = function(xmlString) { 30 | var obj = xmlToJSON.parseString(xmlString); 31 | var uids = {}; 32 | var outlines = {}; 33 | var deps = []; 34 | var parents = []; 35 | _.each(obj.Project[0].Tasks[0].Task, function(xmlItem) { 36 | if (!xmlItem.Name) { 37 | return; 38 | // xmlItem.Name = [{_text: 'no name ' + xmlItem.UID[0]._text}]; 39 | } 40 | var item = { 41 | name: xmlItem.Name[0]._text.toString(), 42 | outline: xmlItem.OutlineNumber[0]._text.toString() 43 | }; 44 | uids[xmlItem.UID[0]._text] = item; 45 | outlines[item.outline] = item; 46 | }); 47 | _.each(obj.Project[0].Tasks[0].Task, function(xmlItem) { 48 | if (!xmlItem.Name) { 49 | return; 50 | } 51 | var task = uids[xmlItem.UID[0]._text]; 52 | // var name = xmlItem.Name[0]._text; 53 | var outline = task.outline; 54 | 55 | if (xmlItem.PredecessorLink) { 56 | xmlItem.PredecessorLink.forEach((link) => { 57 | var beforeUID = link.PredecessorUID[0]._text; 58 | var before = uids[beforeUID]; 59 | 60 | deps.push({ 61 | before: before, 62 | after: task 63 | }); 64 | }); 65 | 66 | } 67 | 68 | if (outline.indexOf('.') !== -1) { 69 | var parentOutline = outline.slice(0, outline.lastIndexOf('.')); 70 | var parent = outlines[parentOutline]; 71 | if (!parent) { 72 | console.error('can not find parent'); 73 | return; 74 | } 75 | 76 | parents.push({ 77 | parent: parent, 78 | child: task 79 | }); 80 | } 81 | }); 82 | return { 83 | deps: deps, 84 | parents: parents 85 | }; 86 | }; 87 | 88 | module.exports.parseXMLObj = parseXMLObj; 89 | 90 | function cut(date) { 91 | let formated = date.toISOString(); 92 | return formated.slice(0, formated.indexOf('.')); 93 | } 94 | 95 | function getDuration(end, start) { 96 | var diff = end.getTime() - start.getTime(); 97 | const days = Math.floor(diff / 1000 / 60 / 60 / 24) + 1; 98 | // if (days >= 1) { 99 | 100 | // } 101 | var hours = days * 8; 102 | // var mins = Math.floor((diff - hours * 1000 * 60 * 60) / 1000 /60); 103 | // var secs = Math.floor((diff - hours * 1000 * 60 * 60 - mins * 1000 * 60) / 1000); 104 | return `PT${hours}H0M0S`; 105 | } 106 | 107 | module.exports.tasksToXML = function(tasks) { 108 | var start = tasks.at(0).get('start'); 109 | var end = tasks.at(0).get('end'); 110 | var data = tasks.map(function(task) { 111 | if (start > task.get('start')) { 112 | start = task.get('start'); 113 | } 114 | if (end < task.get('end')) { 115 | end = task.get('end'); 116 | } 117 | 118 | const depend = _.map(task.get('depend'), (id) => { 119 | return tasks.get(id).get('sortindex') + 1; 120 | }); 121 | 122 | return { 123 | id: task.get('sortindex') + 1, 124 | name: task.get('name'), 125 | outlineNumber: task.getOutlineNumber(), 126 | outlineLevel: task.getOutlineLevel(), 127 | start: cut(task.get('start')), 128 | finish: cut(task.get('end')), 129 | duration: getDuration(task.get('end'), task.get('start')), 130 | depend: depend[0] 131 | }; 132 | }); 133 | return compiled({ 134 | tasks: data, 135 | currentDate: cut(new Date()), 136 | startDate: cut(start), 137 | finishDate: cut(end), 138 | duration: getDuration(end, start) 139 | }); 140 | }; 141 | -------------------------------------------------------------------------------- /src/js/views/ModalTaskEditView.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | var ModalTaskEditComponent = Backbone.View.extend({ 5 | el : '#editTask', 6 | initialize : function(params) { 7 | this.settings = params.settings; 8 | }, 9 | render : function() { 10 | this.$el.find('.ui.checkbox').checkbox(); 11 | // setup values for selectors 12 | this._prepareSelects(); 13 | 14 | this.$el.find('.tabular.menu .item').tab(); 15 | 16 | 17 | this.$el.find('[name="start"], [name="end"]').datepicker({ 18 | // dateFormat: "dd/mm/yy" 19 | dateFormat : this.settings.getDateFormat() 20 | }); 21 | 22 | this._fillData(); 23 | 24 | // open modal 25 | this.$el.modal({ 26 | onHidden : function() { 27 | $(document.body).removeClass('dimmable'); 28 | this.undelegateEvents(); 29 | }.bind(this), 30 | onApprove : function() { 31 | this._saveData(); 32 | }.bind(this) 33 | }).modal('show'); 34 | this._listenInputs(); 35 | 36 | }, 37 | _listenInputs : function() { 38 | var $milestone = this.$el.find('[name="milestone"]'); 39 | var $deliverable = this.$el.find('[name="deliverable"]'); 40 | var $start = this.$el.find('[name="start"]'); 41 | var $end = this.$el.find('[name="end"]'); 42 | $milestone.on('change', function() { 43 | var val = $milestone.prop('checked'); 44 | if (val) { 45 | $start.val($end.val()); 46 | $deliverable.prop('checked', false); 47 | } 48 | }); 49 | $deliverable.on('change', function() { 50 | if ($deliverable.prop('checked')) { 51 | $milestone.prop('checked', false); 52 | } 53 | }); 54 | }, 55 | _prepareSelects : function() { 56 | var statusSelect = this.$el.find('[name="status"]'); 57 | statusSelect.children().each(function(i, child) { 58 | var id = this.settings.findStatusId(child.text); 59 | $(child).prop('value', id); 60 | }.bind(this)); 61 | 62 | var healthSelect = this.$el.find('[name="health"]'); 63 | healthSelect.children().each(function(i, child) { 64 | var id = this.settings.findHealthId(child.text); 65 | $(child).prop('value', id); 66 | }.bind(this)); 67 | 68 | var workOrderSelect = this.$el.find('[name="wo"]'); 69 | workOrderSelect.empty(); 70 | this.settings.statuses.wodata[0].data.forEach(function(data) { 71 | $('').appendTo(workOrderSelect); 72 | }); 73 | }, 74 | _fillData : function() { 75 | _.each(this.model.attributes, function(val, key) { 76 | if (key === 'status' && (!val || !this.settings.findStatusForId(val))) { 77 | val = this.settings.findDefaultStatusId(); 78 | } 79 | if (key === 'health' && (!val || !this.settings.findHealthForId(val))) { 80 | val = this.settings.findDefaultHealthId(); 81 | } 82 | if (key === 'wo' && (!val || !this.settings.findWOForId(val))) { 83 | val = this.settings.findDefaultWOId(); 84 | } 85 | var input = this.$el.find('[name="' + key + '"]'); 86 | if (!input.length) { 87 | return; 88 | } 89 | if (key === 'start' || key === 'end') { 90 | var dateStr = $.datepicker.formatDate(this.settings.getDateFormat(), val); 91 | input.get(0).value = dateStr; 92 | input.datepicker( "refresh" ); 93 | } else if (input.prop('type') === 'checkbox') { 94 | input.prop('checked', val); 95 | } else { 96 | input.val(val); 97 | } 98 | }, this); 99 | if (this.model.children.length) { 100 | this.$el.find('[name="milestone"]').parent().addClass('disabled'); 101 | } 102 | }, 103 | _saveData : function() { 104 | _.each(this.model.attributes, function(val, key) { 105 | var input = this.$el.find('[name="' + key + '"]'); 106 | if (!input.length) { 107 | return; 108 | } 109 | if (key === 'start' || key === 'end') { 110 | var date = input.val().split('/'); 111 | var value = new Date(date[2] + '-' + date[1] + '-' + date[0]); 112 | this.model.set(key, new Date(value)); 113 | } else if (input.prop('type') === 'checkbox') { 114 | this.model.set(key, input.prop('checked')); 115 | } else { 116 | this.model.set(key, input.val()); 117 | } 118 | }, this); 119 | this.model.save(); 120 | } 121 | }); 122 | 123 | module.exports = ModalTaskEditComponent; 124 | -------------------------------------------------------------------------------- /src/js/views/TopMenuView/MSProjectMenuView.js: -------------------------------------------------------------------------------- 1 | var parseXML = require('../../utils/xmlWorker').parseXMLObj; 2 | var tasksToXML = require('../../utils/xmlWorker').tasksToXML; 3 | var parseDepsFromXML = require('../../utils/xmlWorker').parseDepsFromXML; 4 | 5 | var MSProjectMenuView = Backbone.View.extend({ 6 | el: '#project-menu', 7 | 8 | initialize: function(params) { 9 | this.settings = params.settings; 10 | this.importing = false; 11 | this._setupInput(); 12 | }, 13 | _setupInput: function() { 14 | var input = $('#importFile'); 15 | var self = this; 16 | input.on('change', function(evt) { 17 | var files = evt.target.files; 18 | _.each(files, function(file) { 19 | var parts = file.name.split('.'); 20 | var extention = parts[parts.length - 1].toLowerCase(); 21 | if (extention !== 'xml') { 22 | alert('The file type "' + extention + '" is not supported. Only xml files are allowed. Please save your MS project as a xml file and try again.'); 23 | return; 24 | } 25 | var reader = new FileReader(); 26 | reader.onload = function(e) { 27 | try { 28 | self.xmlData = e.target.result; 29 | } catch (e) { 30 | alert('Error while paring file.'); 31 | throw e; 32 | } 33 | }; 34 | reader.readAsText(file); 35 | }); 36 | }); 37 | }, 38 | events: { 39 | 'click #upload-project' : function() { 40 | $('#msimport').modal({ 41 | onHidden : function() { 42 | $(document.body).removeClass('dimmable'); 43 | }, 44 | onApprove : function() { 45 | if (!this.xmlData || this.importing) { 46 | return false; 47 | } 48 | this.importing = true; 49 | $("#importProgress").show(); 50 | $("#importFile").hide(); 51 | $(document.body).removeClass('dimmable'); 52 | $('#xmlinput-form').trigger('reset'); 53 | setTimeout(this.importData.bind(this), 20); 54 | return false; 55 | }.bind(this) 56 | }).modal('show'); 57 | $("#importProgress").hide(); 58 | $("#importFile").show(); 59 | }, 60 | 'click #download-project' : function() { 61 | var data = tasksToXML(this.collection); 62 | var blob = new Blob([data], {type : 'application/json'}); 63 | saveAs(blob, 'GanttTasks.xml'); 64 | } 65 | }, 66 | progress : function(percent) { 67 | $('#importProgress').progress({ 68 | percent : percent 69 | }); 70 | }, 71 | _prepareData : function(data) { 72 | var defStatus = this.settings.findDefaultStatusId(); 73 | var defHealth = this.settings.findDefaultHealthId(); 74 | var defWO = this.settings.findDefaultWOId(); 75 | data.forEach(function(item) { 76 | item.health = defHealth; 77 | item.status = defStatus; 78 | item.wo = defWO; 79 | }); 80 | return data; 81 | }, 82 | importData : function() { 83 | var delay = 100; 84 | this.progress(0); 85 | // this is some sort of callback hell!! 86 | // we need timeouts for better user experience 87 | // I think user want to see animated progress bar 88 | // but without timeouts it is not possible, right? 89 | setTimeout(function() { 90 | this.progress(10); 91 | var col = this.collection; 92 | var data = parseXML(this.xmlData); 93 | data = this._prepareData(data); 94 | 95 | setTimeout(function() { 96 | this.progress(26); 97 | col.importTasks(data, function() { 98 | this.progress(43); 99 | setTimeout(function() { 100 | this.progress(59); 101 | var deps = parseDepsFromXML(this.xmlData); 102 | setTimeout(function() { 103 | this.progress(78); 104 | col.createDeps(deps); 105 | setTimeout(function() { 106 | this.progress(100); 107 | this.importing = false; 108 | $('#msimport').modal('hide'); 109 | }.bind(this), delay); 110 | }.bind(this), delay); 111 | }.bind(this), delay); 112 | }.bind(this), delay); 113 | }.bind(this), delay); 114 | }.bind(this), delay); 115 | } 116 | }); 117 | 118 | module.exports = MSProjectMenuView; 119 | -------------------------------------------------------------------------------- /src/libs/jquery.comment/jquery.comment.js: -------------------------------------------------------------------------------- 1 | !function (a) { var b = { init: function (c) { var d = this, e = { getCommentsUrl: null, postCommentUrl: null, deleteCommentUrl: null, localization: { headerText: "Comments", commentPlaceHolderText: "Add a comment...", sendButtonText: "Send", replyButtonText: "Reply", deleteButtonText: "Delete" }, callback: { beforeDelete: function () { return !0 }, afterDelete: function () { }, beforeCommentAdd: function () { return !0 }, afterCommentAdd: function () { }, beforeRefresh: function () { }, afterRefresh: function () { }, onGetError: function () { }, onPostError: function () { } }, displayHeader: !0, displayCount: !0, loadWhenVisible: !1, readOnly: !1, displayAvatar: !1 }; if (c = a.extend(!0, {}, e, c), this.data("data", { options: c }), this.addClass("commentsBlock"), c.displayHeader) { var f = "

" + c.localization.headerText; c.displayCount && (f += ' () '), f += "

", this.append(f) } c.readOnly || this.append('
' + '' + c.localization.sendButtonText + "" + "
" + "
"), this.append(''); var g = this.find(".newComment").find("textarea"), h = g.parent().find("[data-action='send']"); if (h.hide(), g.focusout(function () { 0 == a.trim(a(this).val()).length && (h.hide(), a(this).val("")) }), g.focus(function () { h.show() }), h.bind("click", function () { var c = a(this).parents("form"), e = g.val(); if (a.trim(e).length > 0) { var f = c.serialize(); b.postComment.call(d, f), g.val(""), h.hide() } }), c.loadWhenVisible) { var i = d.offset().top, j = i + d.height(), k = a(window).scrollTop(); j <= k + a(window).height() && i >= k ? b.refresh.call(d) : a(window).bind("scroll", function () { k = a(window).scrollTop(), j <= k + a(window).height() && i >= k && (a(window).unbind("scroll"), b.refresh.call(d)) }) } else b.refresh.call(d) }, count: function () { var a = this; return a.find(".comment").length }, refresh: function () { var c = this, d = c.find(".loadingIndicator"); d.show(); var e = this.data().data.options; e.callback.beforeRefresh(), a.getJSON(e.getCommentsUrl, function (f) { c.find(".comments").empty(), a.each(f, function (a, d) { b.bindComment.call(c, d) }), b.bindEvents.call(c), d.hide(), c.find(".heading h4 [data-action='count']").html(c.find(".comment").length), e.callback.afterRefresh() }).fail(function () { d.hide(), e.callback.onGetError() }) }, postComment: function (c) { var d = this, e = d.data().data.options; if (e.callback.beforeCommentAdd()) { var f = d.find(".loadingIndicator"); f.show(), a.post(e.postCommentUrl, c, function (a) { f.hide(), b.bindComment.call(d, a), b.bindEvents.call(d), d.find(".heading h4 [data-action='count']").html(d.find(".comment").length), e.callback.afterCommentAdd(a.Id) }).fail(function () { f.hide(), e.callback.onPostError() }) } }, bindComment: function (a) { var b, c = this.data().data.options; null != a.ParentId ? (b = this.find("[data-commentid=" + a.ParentId + "]"), 0 == b.find(".reply_comments").length && b.append("
"), b = b.children(".reply_comments").children(".comments")) : b = this.children(".comments"), null != a.Comment && (a.Comment = a.Comment.replace("", "")); var d = "
  • '; c.displayAvatar ? (d += '
    ', d += null != a.UserAvatar ? '' : '
    ', d += '
    ') : d += '
    ', d += "

    " + a.Author + "" + "

    " + "

    " + a.Comment + "

    " + "

    ", c.readOnly || ("boolean" != typeof a.CanReply || a.CanReply) && (d += "" + c.localization.replyButtonText + ""), !c.readOnly && "boolean" == typeof a.CanDelete && a.CanDelete && (d += "" + c.localization.deleteButtonText + ""), d += "

  • ", b.prepend(d) }, bindEvents: function () { var c = this, d = c.data().data.options; a('[data-action="replay"]').unbind().bind("click", function () { b.startCommentReplay.call(c, a(this)) }), a('[data-action="delete"]').unbind().bind("click", function () { if (d.callback.beforeDelete(a(this).parents("li.comment").data("commentid"))) { var b = c.find(".loadingIndicator"); b.show(), a.post(d.deleteCommentUrl, { commentId: a(this).parents("li.comment").data("commentid") }, function (e) { a("[data-commentid=" + e + "]").remove(), c.find(".heading h4 [data-action='count']").html(c.find(".comment").length), b.hide(), d.callback.afterDelete(e) }) } }) }, startCommentReplay: function (c) { var d = this, e = this.data().data.options, f = a(c).parents("li.comment"); a(c).parent().after("
    " + "" + e.localization.sendButtonText + "" + "
    " + "
    "), a(c).hide(); var g = a(f).find("textarea"); g.focus(), g.focusout(function () { a(this).removeClass("focused"), 0 == a.trim(a(this).val()).length && (f.find(".contentBlockCommentAdd").remove(), a(this).val(""), a(c.show())) }), f.find('[data-action="send"]').bind("click", function () { var e = a(this).parent(), g = e.find("textarea").val(); if (0 != a.trim(g).length) { var h = e.serialize(); h += "&parentId=" + f.data("commentid"), b.postComment.call(d, h) } a(c.show()), setTimeout(function(){f.find(".contentBlockCommentAdd").remove()},10); }) } }; a.fn.comments = function (c) { return b[c] ? b[c].apply(this, Array.prototype.slice.call(arguments, 1)) : "object" != typeof c && c ? (a.error("Method " + c + " doesn not exists in jQuery.comments"), null) : b.init.apply(this, arguments) } }(jQuery); -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "jsx": true, 4 | "modules": 2 5 | }, 6 | "parser": "espree", 7 | "env": { 8 | "browser": true, 9 | "jasmine": true, 10 | "node": true, 11 | "es6": true 12 | }, 13 | 14 | "rules": { 15 | "no-alert": 0, 16 | "no-array-constructor": 2, 17 | "no-bitwise": 2, 18 | "no-caller": 2, 19 | "no-catch-shadow": 2, 20 | "no-underscore-dangle": 0, 21 | "no-cond-assign": 2, 22 | "no-console": 0, 23 | "no-constant-condition": 0, 24 | "no-continue": 0, 25 | "no-control-regex": 2, 26 | "no-debugger": 2, 27 | "no-delete-var": 2, 28 | "no-div-regex": 0, 29 | "no-dupe-keys": 2, 30 | "no-dupe-args": 2, 31 | "no-duplicate-case": 2, 32 | "no-else-return": 2, 33 | "no-empty": 2, 34 | "no-empty-class": 0, 35 | "no-empty-character-class": 2, 36 | "no-empty-label": 2, 37 | "no-eq-null": 2, 38 | "no-eval": 2, 39 | "no-ex-assign": 2, 40 | "no-extend-native": 2, 41 | "no-extra-bind": 2, 42 | "no-extra-boolean-cast": 2, 43 | "no-extra-parens": 0, 44 | "no-extra-semi": 2, 45 | "no-fallthrough": 2, 46 | "no-floating-decimal": 0, 47 | "no-func-assign": 2, 48 | "no-implied-eval": 2, 49 | "no-inline-comments": 0, 50 | "no-inner-declarations": [2, "functions"], 51 | "no-invalid-regexp": 2, 52 | "no-irregular-whitespace": 2, 53 | "no-iterator": 2, 54 | "no-label-var": 2, 55 | "no-labels": 2, 56 | "no-lone-blocks": 2, 57 | "no-lonely-if": 0, 58 | "no-loop-func": 2, 59 | "no-mixed-requires": [1, false], 60 | "no-mixed-spaces-and-tabs": [2, false], 61 | "linebreak-style": [1, "unix"], 62 | "no-multi-spaces": 2, 63 | "no-multi-str": 2, 64 | "no-multiple-empty-lines": [1, {"max": 3}], 65 | "no-native-reassign": 2, 66 | "no-negated-in-lhs": 2, 67 | "no-nested-ternary": 0, 68 | "no-new": 2, 69 | "no-new-func": 2, 70 | "no-new-object": 2, 71 | "no-new-require": 0, 72 | "no-new-wrappers": 2, 73 | "no-obj-calls": 2, 74 | "no-octal": 2, 75 | "no-octal-escape": 2, 76 | "no-param-reassign": 0, 77 | "no-path-concat": 0, 78 | "no-plusplus": 0, 79 | "no-process-env": 0, 80 | "no-process-exit": 2, 81 | "no-proto": 2, 82 | "no-redeclare": 2, 83 | "no-regex-spaces": 2, 84 | "no-reserved-keys": 0, 85 | "no-restricted-modules": 0, 86 | "no-return-assign": 2, 87 | "no-script-url": 2, 88 | "no-self-compare": 0, 89 | "no-sequences": 2, 90 | "no-shadow": 2, 91 | "no-shadow-restricted-names": 2, 92 | "no-spaced-func": 2, 93 | "no-sparse-arrays": 2, 94 | "no-sync": 0, 95 | "no-ternary": 0, 96 | "no-trailing-spaces": 2, 97 | "no-throw-literal": 1, 98 | "no-undef": 2, 99 | "no-undef-init": 2, 100 | "no-undefined": 0, 101 | "no-unneeded-ternary": 2, 102 | "no-unreachable": 2, 103 | "no-unused-expressions": 2, 104 | "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], 105 | "no-use-before-define": 2, 106 | "no-void": 0, 107 | "no-var": 0, 108 | "prefer-const": 0, 109 | "no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], 110 | "no-with": 2, 111 | 112 | "accessor-pairs": 0, 113 | "block-scoped-var": 0, 114 | "brace-style": [0, "1tbs"], 115 | "camelcase": 2, 116 | "comma-dangle": [2, "never"], 117 | "comma-spacing": 2, 118 | "comma-style": 0, 119 | "complexity": [1, 11], 120 | "computed-property-spacing": [0, "never"], 121 | "consistent-return": 0, 122 | "consistent-this": [0, "that"], 123 | "curly": [2, "all"], 124 | "default-case": 0, 125 | "dot-location": 0, 126 | "dot-notation": [2, { "allowKeywords": true }], 127 | "eol-last": 0, 128 | "eqeqeq": 2, 129 | "func-names": 0, 130 | "func-style": [1, "declaration"], 131 | "generator-star": 0, 132 | "generator-star-spacing": 0, 133 | "guard-for-in": 1, 134 | "handle-callback-err": 2, 135 | "indent": 0, 136 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 137 | "lines-around-comment": 0, 138 | "max-depth": [2, 4], 139 | "max-len": [2, 120, 4], 140 | "max-nested-callbacks": [2, 2], 141 | "max-params": [2, 6], 142 | "max-statements": [2, 10], 143 | "new-cap": 2, 144 | "new-parens": 2, 145 | "newline-after-var": 0, 146 | "object-curly-spacing": [0, "never"], 147 | "object-shorthand": 0, 148 | "one-var": 0, 149 | "operator-assignment": [0, "always"], 150 | "operator-linebreak": 0, 151 | "padded-blocks": 0, 152 | "quote-props": 0, 153 | "quotes": [2, "single"], 154 | "radix": 0, 155 | "semi": 2, 156 | "semi-spacing": [2, {"before": false, "after": true}], 157 | "sort-vars": 0, 158 | "space-after-function-name": [0, "never"], 159 | "space-after-keywords": [0, "always"], 160 | "space-before-blocks": [0, "always"], 161 | "space-before-function-paren": [0, "always"], 162 | "space-before-function-parentheses": [0, "always"], 163 | "space-in-brackets": [0, "never"], 164 | "space-in-parens": [0, "never"], 165 | "space-infix-ops": 2, 166 | "space-return-throw-case": 2, 167 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 168 | "spaced-comment": 0, 169 | "spaced-line-comment": [0, "always"], 170 | "strict": 2, 171 | "use-isnan": 2, 172 | "valid-jsdoc": 2, 173 | "valid-typeof": 2, 174 | "vars-on-top": 0, 175 | "wrap-iife": 0, 176 | "wrap-regex": 0, 177 | "yoda": [2, "never"] 178 | }, 179 | "globals": { 180 | "$": false, 181 | "Backbone": false, 182 | "_": false, 183 | "Konva": false, 184 | "React": false 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/js/collections/TaskCollection.js: -------------------------------------------------------------------------------- 1 | var TaskModel = require('../models/TaskModel'); 2 | 3 | var TaskCollection = Backbone.Collection.extend({ 4 | url: 'api/tasks', 5 | model: TaskModel, 6 | initialize: function() { 7 | this._preventSorting = false; 8 | this.subscribe(); 9 | }, 10 | setDefaultStatusId(id){ 11 | this.defaultStatusId = id; 12 | }, 13 | setClosedStatusId(id) { 14 | this.closedStatusId = id; 15 | }, 16 | comparator: function(model) { 17 | return model.get('sortindex'); 18 | }, 19 | linkChildren: function() { 20 | this.each(function(task) { 21 | if (!task.get('parentid')) { 22 | return; 23 | } 24 | var parentTask = this.get(task.get('parentid')); 25 | if (parentTask) { 26 | if (parentTask === task) { 27 | task.set('parentid', 0); 28 | } else { 29 | parentTask.children.add(task); 30 | } 31 | } else { 32 | console.error('task has parent with id ' + task.get('parentid') + ' - but there is no such task'); 33 | task.unset('parentid'); 34 | } 35 | }.bind(this)); 36 | }, 37 | _sortChildren: function (task, sortIndex) { 38 | task.children.toArray().forEach(function(child) { 39 | child.set('sortindex', ++sortIndex); 40 | sortIndex = this._sortChildren(child, sortIndex); 41 | }.bind(this)); 42 | return sortIndex; 43 | }, 44 | checkSortedIndex: function() { 45 | var sortIndex = -1; 46 | this.toArray().forEach(function(task) { 47 | if (task.get('parentid')) { 48 | return; 49 | } 50 | task.set('sortindex', ++sortIndex); 51 | sortIndex = this._sortChildren(task, sortIndex); 52 | }.bind(this)); 53 | this.sort(); 54 | }, 55 | _resortChildren: function(data, startIndex, parentID) { 56 | var sortIndex = startIndex; 57 | data.forEach(function(taskData) { 58 | var task = this.get(taskData.id); 59 | if (task.get('parentid') !== parentID) { 60 | var newParent = this.get(parentID); 61 | if (newParent) { 62 | newParent.children.add(task); 63 | } 64 | } 65 | task.save({ 66 | sortindex: ++sortIndex, 67 | parentid: parentID 68 | }); 69 | if (taskData.children && taskData.children.length) { 70 | sortIndex = this._resortChildren(taskData.children, sortIndex, task.id); 71 | } 72 | }.bind(this)); 73 | return sortIndex; 74 | }, 75 | resort: function(data) { 76 | this._preventSorting = true; 77 | this._resortChildren(data, -1, 0); 78 | this._preventSorting = false; 79 | this.sort(); 80 | }, 81 | subscribe: function() { 82 | this.listenTo(this, 'reset', () => { 83 | // add empty task if no tasks from server 84 | if (this.length === 0) { 85 | this.reset([{ 86 | name: 'New task' 87 | }]); 88 | } 89 | }); 90 | this.listenTo(this, 'add', function(model) { 91 | if (model.get('parentid')) { 92 | var parent = this.find(function(m) { 93 | return m.id === model.get('parentid'); 94 | }); 95 | if (parent) { 96 | parent.children.add(model); 97 | model.parent = parent; 98 | } else { 99 | console.warn('can not find parent with id ' + model.get('parentid')); 100 | model.set('parentid', 0); 101 | } 102 | } 103 | if (this.defaultStatusId) { 104 | model.set('status', this.defaultStatusId); 105 | } 106 | }); 107 | this.listenTo(this, 'reset', function() { 108 | this.linkChildren(); 109 | this.checkSortedIndex(); 110 | this._checkDependencies(); 111 | }); 112 | this.listenTo(this, 'change:parentid', function(task) { 113 | if (task.parent) { 114 | task.parent.children.remove(task); 115 | task.parent = undefined; 116 | } 117 | 118 | var newParent = this.get(task.get('parentid')); 119 | if (newParent) { 120 | newParent.children.add(task); 121 | } 122 | if (!this._preventSorting) { 123 | this.checkSortedIndex(); 124 | } 125 | }); 126 | this.listenTo(this, 'change:complete', (task) => { 127 | if (task.get('complete') == 100 && this.closedStatusId !== undefined) { 128 | task.set('status', this.closedStatusId); 129 | } 130 | }); 131 | }, 132 | createDependency: function (beforeModel, afterModel) { 133 | if (this._canCreateDependence(beforeModel, afterModel)) { 134 | afterModel.dependOn(beforeModel); 135 | } 136 | }, 137 | 138 | _canCreateDependence: function(beforeModel, afterModel) { 139 | if (beforeModel.hasParent(afterModel) || afterModel.hasParent(beforeModel)) { 140 | return false; 141 | } 142 | if (beforeModel.hasInDeps(afterModel) || 143 | afterModel.hasInDeps(beforeModel)) { 144 | return false; 145 | } 146 | return true; 147 | }, 148 | removeDependency: function(afterModel) { 149 | afterModel.clearDependence(); 150 | }, 151 | _checkDependencies: function() { 152 | this.each((task) => { 153 | var ids = task.get('depend').concat([]); 154 | var hasGoodDepends = false; 155 | if (ids.length === 0) { 156 | return; 157 | } 158 | 159 | _.each(ids, (id) => { 160 | var beforeModel = this.get(id); 161 | if (beforeModel) { 162 | task.dependOn(beforeModel, true); 163 | hasGoodDepends = true; 164 | } 165 | }); 166 | if (!hasGoodDepends) { 167 | task.save('depend', []); 168 | } 169 | }); 170 | }, 171 | outdent: function(task) { 172 | var underSublings = []; 173 | if (task.parent) { 174 | task.parent.children.each(function(child) { 175 | if (child.get('sortindex') <= task.get('sortindex')) { 176 | return; 177 | } 178 | underSublings.push(child); 179 | }); 180 | } 181 | 182 | this._preventSorting = true; 183 | underSublings.forEach(function(child) { 184 | if (child.depends.get(task.id)) { 185 | child.clearDependence(); 186 | } 187 | child.save('parentid', task.id); 188 | }); 189 | this._preventSorting = false; 190 | if (task.parent && task.parent.parent) { 191 | task.save('parentid', task.parent.parent.id); 192 | } else { 193 | task.save('parentid', 0); 194 | } 195 | }, 196 | indent: function(task) { 197 | var prevTask, i, m; 198 | for (i = this.length - 1; i >= 0; i--) { 199 | m = this.at(i); 200 | if ((m.get('sortindex') < task.get('sortindex')) && (task.parent === m.parent)) { 201 | prevTask = m; 202 | break; 203 | } 204 | } 205 | if (prevTask) { 206 | task.save('parentid', prevTask.id); 207 | } 208 | }, 209 | importTasks: function(taskJSONarray, callback) { 210 | var sortindex = -1; 211 | if (this.last()) { 212 | sortindex = this.last().get('sortindex'); 213 | } 214 | taskJSONarray.forEach(function(taskItem) { 215 | taskItem.sortindex = ++sortindex; 216 | }); 217 | var length = taskJSONarray.length; 218 | var done = 0; 219 | this.add(taskJSONarray, {parse: true}).forEach((task) => { 220 | task.save({}, { 221 | success: () => { 222 | done += 1; 223 | if (done === length) { 224 | callback(); 225 | } 226 | } 227 | }); 228 | }); 229 | }, 230 | createDeps: function(data) { 231 | this._preventSorting = true; 232 | data.parents.forEach(function(item) { 233 | var parent = this.findWhere({ 234 | name: item.parent.name, 235 | outline: item.parent.outline 236 | }); 237 | var child = this.findWhere({ 238 | name: item.child.name, 239 | outline: item.child.outline 240 | }); 241 | child.save('parentid', parent.id); 242 | }.bind(this)); 243 | 244 | data.deps.forEach(function(dep) { 245 | var beforeModel = this.findWhere({ 246 | name: dep.before.name, 247 | outline: dep.before.outline 248 | }); 249 | var afterModel = this.findWhere({ 250 | name: dep.after.name, 251 | outline: dep.after.outline 252 | }); 253 | this.createDependency(beforeModel, afterModel); 254 | }.bind(this)); 255 | this._preventSorting = false; 256 | this.checkSortedIndex(); 257 | } 258 | }); 259 | 260 | module.exports = TaskCollection; 261 | -------------------------------------------------------------------------------- /src/libs/jquery.mousewheel.js: -------------------------------------------------------------------------------- 1 | /*! Copyright (c) 2013 Brandon Aaron (http://brandon.aaron.sh) 2 | * Licensed under the MIT License (LICENSE.txt). 3 | * 4 | * Version: 3.1.9 5 | * 6 | * Requires: jQuery 1.2.2+ 7 | */ 8 | 9 | (function (factory) { 10 | if ( typeof define === 'function' && define.amd ) { 11 | // AMD. Register as an anonymous module. 12 | define(['jquery'], factory); 13 | } else if (typeof exports === 'object') { 14 | // Node/CommonJS style for Browserify 15 | module.exports = factory; 16 | } else { 17 | // Browser globals 18 | factory(jQuery); 19 | } 20 | }(function ($) { 21 | 22 | var toFix = ['wheel', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll'], 23 | toBind = ( 'onwheel' in document || document.documentMode >= 9 ) ? 24 | ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'], 25 | slice = Array.prototype.slice, 26 | nullLowestDeltaTimeout, lowestDelta; 27 | 28 | if ( $.event.fixHooks ) { 29 | for ( var i = toFix.length; i; ) { 30 | $.event.fixHooks[ toFix[--i] ] = $.event.mouseHooks; 31 | } 32 | } 33 | 34 | var special = $.event.special.mousewheel = { 35 | version: '3.1.9', 36 | 37 | setup: function() { 38 | if ( this.addEventListener ) { 39 | for ( var i = toBind.length; i; ) { 40 | this.addEventListener( toBind[--i], handler, false ); 41 | } 42 | } else { 43 | this.onmousewheel = handler; 44 | } 45 | // Store the line height and page height for this particular element 46 | $.data(this, 'mousewheel-line-height', special.getLineHeight(this)); 47 | $.data(this, 'mousewheel-page-height', special.getPageHeight(this)); 48 | }, 49 | 50 | teardown: function() { 51 | if ( this.removeEventListener ) { 52 | for ( var i = toBind.length; i; ) { 53 | this.removeEventListener( toBind[--i], handler, false ); 54 | } 55 | } else { 56 | this.onmousewheel = null; 57 | } 58 | }, 59 | 60 | getLineHeight: function(elem) { 61 | return parseInt($(elem)['offsetParent' in $.fn ? 'offsetParent' : 'parent']().css('fontSize'), 10); 62 | }, 63 | 64 | getPageHeight: function(elem) { 65 | return $(elem).height(); 66 | }, 67 | 68 | settings: { 69 | adjustOldDeltas: true 70 | } 71 | }; 72 | 73 | $.fn.extend({ 74 | mousewheel: function(fn) { 75 | return fn ? this.bind('mousewheel', fn) : this.trigger('mousewheel'); 76 | }, 77 | 78 | unmousewheel: function(fn) { 79 | return this.unbind('mousewheel', fn); 80 | } 81 | }); 82 | 83 | 84 | function handler(event) { 85 | var orgEvent = event || window.event, 86 | args = slice.call(arguments, 1), 87 | delta = 0, 88 | deltaX = 0, 89 | deltaY = 0, 90 | absDelta = 0; 91 | event = $.event.fix(orgEvent); 92 | event.type = 'mousewheel'; 93 | 94 | // Old school scrollwheel delta 95 | if ( 'detail' in orgEvent ) { deltaY = orgEvent.detail * -1; } 96 | if ( 'wheelDelta' in orgEvent ) { deltaY = orgEvent.wheelDelta; } 97 | if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY; } 98 | if ( 'wheelDeltaX' in orgEvent ) { deltaX = orgEvent.wheelDeltaX * -1; } 99 | 100 | // Firefox < 17 horizontal scrolling related to DOMMouseScroll event 101 | if ( 'axis' in orgEvent && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) { 102 | deltaX = deltaY * -1; 103 | deltaY = 0; 104 | } 105 | 106 | // Set delta to be deltaY or deltaX if deltaY is 0 for backwards compatabilitiy 107 | delta = deltaY === 0 ? deltaX : deltaY; 108 | 109 | // New school wheel delta (wheel event) 110 | if ( 'deltaY' in orgEvent ) { 111 | deltaY = orgEvent.deltaY * -1; 112 | delta = deltaY; 113 | } 114 | if ( 'deltaX' in orgEvent ) { 115 | deltaX = orgEvent.deltaX; 116 | if ( deltaY === 0 ) { delta = deltaX * -1; } 117 | } 118 | 119 | // No change actually happened, no reason to go any further 120 | if ( deltaY === 0 && deltaX === 0 ) { return; } 121 | 122 | // Need to convert lines and pages to pixels if we aren't already in pixels 123 | // There are three delta modes: 124 | // * deltaMode 0 is by pixels, nothing to do 125 | // * deltaMode 1 is by lines 126 | // * deltaMode 2 is by pages 127 | if ( orgEvent.deltaMode === 1 ) { 128 | var lineHeight = $.data(this, 'mousewheel-line-height'); 129 | delta *= lineHeight; 130 | deltaY *= lineHeight; 131 | deltaX *= lineHeight; 132 | } else if ( orgEvent.deltaMode === 2 ) { 133 | var pageHeight = $.data(this, 'mousewheel-page-height'); 134 | delta *= pageHeight; 135 | deltaY *= pageHeight; 136 | deltaX *= pageHeight; 137 | } 138 | 139 | // Store lowest absolute delta to normalize the delta values 140 | absDelta = Math.max( Math.abs(deltaY), Math.abs(deltaX) ); 141 | 142 | if ( !lowestDelta || absDelta < lowestDelta ) { 143 | lowestDelta = absDelta; 144 | 145 | // Adjust older deltas if necessary 146 | if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) { 147 | lowestDelta /= 40; 148 | } 149 | } 150 | 151 | // Adjust older deltas if necessary 152 | if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) { 153 | // Divide all the things by 40! 154 | delta /= 40; 155 | deltaX /= 40; 156 | deltaY /= 40; 157 | } 158 | 159 | // Get a whole, normalized value for the deltas 160 | delta = Math[ delta >= 1 ? 'floor' : 'ceil' ](delta / lowestDelta); 161 | deltaX = Math[ deltaX >= 1 ? 'floor' : 'ceil' ](deltaX / lowestDelta); 162 | deltaY = Math[ deltaY >= 1 ? 'floor' : 'ceil' ](deltaY / lowestDelta); 163 | 164 | // Add information to the event object 165 | event.deltaX = deltaX; 166 | event.deltaY = deltaY; 167 | event.deltaFactor = lowestDelta; 168 | // Go ahead and set deltaMode to 0 since we converted to pixels 169 | // Although this is a little odd since we overwrite the deltaX/Y 170 | // properties with normalized deltas. 171 | event.deltaMode = 0; 172 | 173 | // Add event and delta to the front of the arguments 174 | args.unshift(event, delta, deltaX, deltaY); 175 | 176 | // Clearout lowestDelta after sometime to better 177 | // handle multiple device types that give different 178 | // a different lowestDelta 179 | // Ex: trackpad = 3 and mouse wheel = 120 180 | if (nullLowestDeltaTimeout) { clearTimeout(nullLowestDeltaTimeout); } 181 | nullLowestDeltaTimeout = setTimeout(nullLowestDelta, 200); 182 | 183 | return ($.event.dispatch || $.event.handle).apply(this, args); 184 | } 185 | 186 | function nullLowestDelta() { 187 | lowestDelta = null; 188 | } 189 | 190 | function shouldAdjustOldDeltas(orgEvent, absDelta) { 191 | // If this is an older event and the delta is divisable by 120, 192 | // then we are assuming that the browser is treating this as an 193 | // older mouse wheel event and that we should divide the deltas 194 | // by 40 to try and get a more usable deltaFactor. 195 | // Side note, this actually impacts the reported scroll distance 196 | // in older browsers and can cause scrolling to be slower than native. 197 | // Turn this off by setting $.event.special.mousewheel.settings.adjustOldDeltas to false. 198 | return special.settings.adjustOldDeltas && orgEvent.type === 'mousewheel' && absDelta % 120 === 0; 199 | } 200 | 201 | })); 202 | -------------------------------------------------------------------------------- /data/tasks.js: -------------------------------------------------------------------------------- 1 | module.exports = [{"acttimesheet":true,"color":"#0090d3","darkcolor":null,"deliverable":false,"depend":"","description":"","end":"2012-10-15T00:00:00","b_end":"2012-10-25T02:00:00","financial":false,"health":72,"id":1789,"name":"Deploy into Production","lightcolor":null,"milestone":true,"parentid":0,"sitekey":"2b00da46b57c0395","complete":0,"ProjectRef":43,"reportable":true,"ResID":null,"Resources":[],"sortindex":8.0,"start":"2012-10-15T00:00:00","b_start":"2012-10-25T02:00:00","status":23,"timesheet":true,"WBS_ID":177,"wbstsid":1789,"wo":43,"Comments":0},{"acttimesheet":false,"color":"#0090d3","darkcolor":null,"deliverable":true,"depend":"","description":"This is a test (Carmela)","end":"2012-07-06T02:00:00","b_end":null,"financial":true,"health":21,"id":1832,"name":"Create Project Management Plan","lightcolor":null,"milestone":false,"parentid":1897,"sitekey":"2b00da46b57c0395","complete":20,"ProjectRef":43,"reportable":true,"ResID":null,"Resources":[{"ID":127,"ResID":1,"TSActivate":true}],"sortindex":1.0,"start":"2012-05-06T20:00:00Z","b_start":null,"status":110,"timesheet":false,"WBS_ID":177,"wbstsid":100006,"wo":43,"Comments":0},{"acttimesheet":false,"color":"#0090d3","darkcolor":null,"deliverable":false,"depend":"1832","description":"","end":"2012-08-24T22:00:00","b_end":null,"financial":false,"health":72,"id":1877,"name":"Engage Vendors just another","lightcolor":null,"milestone":false,"parentid":1897,"sitekey":"2b00da46b57c0395","complete":0,"ProjectRef":43,"reportable":true,"ResID":null,"Resources":[],"sortindex":2.0,"start":"2012-07-17T22:00:00","b_start":null,"status":23,"timesheet":false,"WBS_ID":177,"wbstsid":100044,"wo":43,"Comments":0},{"acttimesheet":true,"color":"#0090d3","darkcolor":null,"deliverable":true,"depend":"1897","description":"dgfdfgdfg","end":"2015-12-18T19:00:00","b_end":null,"financial":true,"health":72,"id":1878,"name":"Build Servers yeah not bad","lightcolor":null,"milestone":false,"parentid":0,"sitekey":"2b00da46b57c0395","complete":33,"ProjectRef":43,"reportable":true,"ResID":null,"Resources":[],"sortindex":4.0,"start":"2015-11-15T10:52:00","b_start":null,"status":218,"timesheet":true,"WBS_ID":177,"wbstsid":100045,"wo":43,"Comments":0},{"acttimesheet":true,"color":"#0090d3","darkcolor":null,"deliverable":false,"depend":"","description":"yyyyyy","end":"2015-12-12T15:00:00","b_end":null,"financial":false,"health":1,"id":1894,"name":"Develop Prototype Application what the hell","lightcolor":null,"milestone":false,"parentid":1878,"sitekey":"2b00da46b57c0395","complete":0,"ProjectRef":43,"reportable":false,"ResID":null,"Resources":[],"sortindex":7.0,"start":"2015-11-15T15:00:00","b_start":null,"status":1,"timesheet":true,"WBS_ID":177,"wbstsid":100058,"wo":43,"Comments":0},{"acttimesheet":false,"color":"#0090d3","darkcolor":null,"deliverable":false,"depend":"1894","description":"","end":"2015-12-18T19:00:00","b_end":null,"financial":false,"health":72,"id":1895,"name":"Application Testing","lightcolor":null,"milestone":true,"parentid":1878,"sitekey":"2b00da46b57c0395","complete":100,"ProjectRef":43,"reportable":true,"ResID":null,"Resources":[],"sortindex":5.0,"start":"2015-12-18T19:00:00","b_start":null,"status":218,"timesheet":false,"WBS_ID":177,"wbstsid":100060,"wo":0,"Comments":0},{"acttimesheet":false,"color":"#0090d3","darkcolor":null,"deliverable":false,"depend":"","description":"","end":"2012-10-15T00:00:00","b_end":null,"financial":false,"health":72,"id":1896,"name":"Development Code Module Testing","lightcolor":null,"milestone":true,"parentid":0,"sitekey":"2b00da46b57c0395","complete":0,"ProjectRef":43,"reportable":true,"ResID":null,"Resources":[],"sortindex":10.0,"start":"2012-10-15T00:00:00","b_start":null,"status":218,"timesheet":false,"WBS_ID":177,"wbstsid":100061,"wo":0,"Comments":0},{"acttimesheet":true,"color":"#0090d3","darkcolor":null,"deliverable":false,"depend":"","description":"","end":"2015-11-15T10:52:00","b_end":null,"financial":false,"health":16201,"id":1897,"name":"Create Development Tender","lightcolor":null,"milestone":false,"parentid":0,"sitekey":"2b00da46b57c0395","complete":7,"ProjectRef":43,"reportable":true,"ResID":null,"Resources":[{"ID":125,"ResID":1,"TSActivate":true},{"ID":126,"ResID":65,"TSActivate":true}],"sortindex":0.0,"start":"2012-05-06T02:00:00","b_start":null,"status":218,"timesheet":true,"WBS_ID":177,"wbstsid":100074,"wo":43,"Comments":1},{"acttimesheet":false,"color":"#0090d3","darkcolor":null,"deliverable":false,"depend":"","description":"","end":"2015-10-22T03:00:00","b_end":null,"financial":false,"health":1,"id":3506,"name":"new test","lightcolor":null,"milestone":true,"parentid":0,"sitekey":"2b00da46b57c0395","complete":0,"ProjectRef":43,"reportable":true,"ResID":null,"Resources":[],"sortindex":11.0,"start":"2015-10-22T03:00:00","b_start":null,"status":1,"timesheet":false,"WBS_ID":177,"wbstsid":100362,"wo":43,"Comments":0},{"acttimesheet":false,"color":"#0090d3","darkcolor":null,"deliverable":false,"depend":"","description":"","end":"2012-10-28T00:00:00","b_end":null,"financial":false,"health":21,"id":3507,"name":"What the f is this","lightcolor":null,"milestone":false,"parentid":0,"sitekey":"2b00da46b57c0395","complete":0,"ProjectRef":43,"reportable":true,"ResID":null,"Resources":[],"sortindex":12.0,"start":"2012-09-14T00:00:00","b_start":null,"status":110,"timesheet":false,"WBS_ID":177,"wbstsid":100363,"wo":43,"Comments":0},{"acttimesheet":null,"color":"#0090d3","darkcolor":null,"deliverable":false,"depend":"3507","description":"ytr","end":"2012-12-26T04:00:00","b_end":"2012-12-27T13:00:00","financial":false,"health":21,"id":3508,"name":"hjhkjhkj","lightcolor":null,"milestone":false,"parentid":0,"sitekey":"2b00da46b57c0395","complete":0,"ProjectRef":43,"reportable":true,"ResID":null,"Resources":[],"sortindex":14.0,"start":"2012-11-18T04:00:00","b_start":"2012-11-19T13:00:00","status":110,"timesheet":null,"WBS_ID":177,"wbstsid":3508,"wo":43,"Comments":0},{"acttimesheet":null,"color":"#0090d3","darkcolor":null,"deliverable":false,"depend":"3508","description":"","end":"2013-03-12T17:00:00","b_end":"2013-03-14T13:00:00","financial":false,"health":21,"id":3706,"name":"hjkhjkhjkhhkj","lightcolor":null,"milestone":false,"parentid":0,"sitekey":"2b00da46b57c0395","complete":0,"ProjectRef":43,"reportable":false,"ResID":null,"Resources":[],"sortindex":9.0,"start":"2013-01-20T17:00:00","b_start":"2013-01-22T13:00:00","status":110,"timesheet":null,"WBS_ID":177,"wbstsid":3706,"wo":2,"Comments":0},{"acttimesheet":null,"color":"#0090d3","darkcolor":null,"deliverable":false,"depend":"","description":"","end":"2015-11-15T10:52:00","b_end":null,"financial":false,"health":21,"id":4780,"name":"New task","lightcolor":null,"milestone":false,"parentid":1897,"sitekey":"2b00da46b57c0395","complete":0,"ProjectRef":43,"reportable":false,"ResID":null,"Resources":[],"sortindex":3.0,"start":"2015-11-15T10:52:00","b_start":null,"status":110,"timesheet":null,"WBS_ID":177,"wbstsid":4780,"wo":2,"Comments":0},{"acttimesheet":null,"color":"#0090d3","darkcolor":null,"deliverable":false,"depend":"","description":"","end":"2015-11-15T10:52:00","b_end":null,"financial":false,"health":21,"id":4781,"name":"New task","lightcolor":null,"milestone":false,"parentid":1878,"sitekey":"2b00da46b57c0395","complete":0,"ProjectRef":43,"reportable":false,"ResID":null,"Resources":[],"sortindex":6.0,"start":"2015-11-15T10:52:00","b_start":null,"status":110,"timesheet":null,"WBS_ID":177,"wbstsid":4781,"wo":2,"Comments":0},{"acttimesheet":null,"color":"#0090d3","darkcolor":null,"deliverable":false,"depend":"","description":"","end":"2015-11-15T10:53:00","b_end":null,"financial":false,"health":21,"id":4782,"name":"New task","lightcolor":null,"milestone":false,"parentid":0,"sitekey":"2b00da46b57c0395","complete":0,"ProjectRef":43,"reportable":false,"ResID":null,"Resources":[],"sortindex":13.0,"start":"2015-11-15T10:53:00","b_start":null,"status":110,"timesheet":null,"WBS_ID":177,"wbstsid":4782,"wo":2,"Comments":0},{"acttimesheet":null,"color":"#0090d3","darkcolor":null,"deliverable":false,"depend":"","description":"","end":"2015-11-15T10:51:00","b_end":null,"financial":false,"health":21,"id":4783,"name":"New task","lightcolor":null,"milestone":false,"parentid":0,"sitekey":"2b00da46b57c0395","complete":0,"ProjectRef":43,"reportable":false,"ResID":null,"Resources":[],"sortindex":15.0,"start":"2015-11-15T10:51:00","b_start":null,"status":110,"timesheet":null,"WBS_ID":177,"wbstsid":null,"wo":2,"Comments":0},{"acttimesheet":null,"color":"#0090d3","darkcolor":null,"deliverable":false,"depend":"","description":"","end":"2015-11-15T10:51:00","b_end":null,"financial":false,"health":21,"id":4784,"name":"New what a task","lightcolor":null,"milestone":false,"parentid":0,"sitekey":"2b00da46b57c0395","complete":0,"ProjectRef":43,"reportable":false,"ResID":null,"Resources":[],"sortindex":15.0,"start":"2015-11-15T10:51:00","b_start":null,"status":110,"timesheet":null,"WBS_ID":177,"wbstsid":4784,"wo":2,"Comments":0}]; 2 | -------------------------------------------------------------------------------- /src/libs/FileSaver.js: -------------------------------------------------------------------------------- 1 | /* FileSaver.js 2 | * A saveAs() FileSaver implementation. 3 | * 2014-07-25 4 | * 5 | * By Eli Grey, http://eligrey.com 6 | * License: X11/MIT 7 | * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md 8 | */ 9 | 10 | /*global self */ 11 | /*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */ 12 | 13 | /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ 14 | 15 | var saveAs = saveAs 16 | // IE 10+ (native saveAs) 17 | || (typeof navigator !== "undefined" && 18 | navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator)) 19 | // Everyone else 20 | || (function(view) { 21 | "use strict"; 22 | // IE <10 is explicitly unsupported 23 | if (typeof navigator !== "undefined" && 24 | /MSIE [1-9]\./.test(navigator.userAgent)) { 25 | return; 26 | } 27 | var 28 | doc = view.document 29 | // only get URL when necessary in case Blob.js hasn't overridden it yet 30 | , get_URL = function() { 31 | return view.URL || view.webkitURL || view; 32 | } 33 | , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") 34 | , can_use_save_link = !view.externalHost && "download" in save_link 35 | , click = function(node) { 36 | var event = doc.createEvent("MouseEvents"); 37 | event.initMouseEvent( 38 | "click", true, false, view, 0, 0, 0, 0, 0 39 | , false, false, false, false, 0, null 40 | ); 41 | node.dispatchEvent(event); 42 | } 43 | , webkit_req_fs = view.webkitRequestFileSystem 44 | , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem 45 | , throw_outside = function(ex) { 46 | (view.setImmediate || view.setTimeout)(function() { 47 | throw ex; 48 | }, 0); 49 | } 50 | , force_saveable_type = "application/octet-stream" 51 | , fs_min_size = 0 52 | // See https://code.google.com/p/chromium/issues/detail?id=375297#c7 for 53 | // the reasoning behind the timeout and revocation flow 54 | , arbitrary_revoke_timeout = 10 55 | , revoke = function(file) { 56 | var revoker = function() { 57 | if (typeof file === "string") { // file is an object URL 58 | get_URL().revokeObjectURL(file); 59 | } else { // file is a File 60 | file.remove(); 61 | } 62 | }; 63 | if (view.chrome) { 64 | revoker(); 65 | } else { 66 | setTimeout(revoker, arbitrary_revoke_timeout); 67 | } 68 | } 69 | , dispatch = function(filesaver, event_types, event) { 70 | event_types = [].concat(event_types); 71 | var i = event_types.length; 72 | while (i--) { 73 | var listener = filesaver["on" + event_types[i]]; 74 | if (typeof listener === "function") { 75 | try { 76 | listener.call(filesaver, event || filesaver); 77 | } catch (ex) { 78 | throw_outside(ex); 79 | } 80 | } 81 | } 82 | } 83 | , FileSaver = function(blob, name) { 84 | // First try a.download, then web filesystem, then object URLs 85 | var 86 | filesaver = this 87 | , type = blob.type 88 | , blob_changed = false 89 | , object_url 90 | , target_view 91 | , dispatch_all = function() { 92 | dispatch(filesaver, "writestart progress write writeend".split(" ")); 93 | } 94 | // on any filesys errors revert to saving with object URLs 95 | , fs_error = function() { 96 | // don't create more object URLs than needed 97 | if (blob_changed || !object_url) { 98 | object_url = get_URL().createObjectURL(blob); 99 | } 100 | if (target_view) { 101 | target_view.location.href = object_url; 102 | } else { 103 | var new_tab = view.open(object_url, "_blank"); 104 | if (new_tab == undefined && typeof safari !== "undefined") { 105 | //Apple do not allow window.open, see http://bit.ly/1kZffRI 106 | view.location.href = object_url 107 | } 108 | } 109 | filesaver.readyState = filesaver.DONE; 110 | dispatch_all(); 111 | revoke(object_url); 112 | } 113 | , abortable = function(func) { 114 | return function() { 115 | if (filesaver.readyState !== filesaver.DONE) { 116 | return func.apply(this, arguments); 117 | } 118 | }; 119 | } 120 | , create_if_not_found = {create: true, exclusive: false} 121 | , slice 122 | ; 123 | filesaver.readyState = filesaver.INIT; 124 | if (!name) { 125 | name = "download"; 126 | } 127 | if (can_use_save_link) { 128 | object_url = get_URL().createObjectURL(blob); 129 | save_link.href = object_url; 130 | save_link.download = name; 131 | click(save_link); 132 | filesaver.readyState = filesaver.DONE; 133 | dispatch_all(); 134 | revoke(object_url); 135 | return; 136 | } 137 | // Object and web filesystem URLs have a problem saving in Google Chrome when 138 | // viewed in a tab, so I force save with application/octet-stream 139 | // http://code.google.com/p/chromium/issues/detail?id=91158 140 | // Update: Google errantly closed 91158, I submitted it again: 141 | // https://code.google.com/p/chromium/issues/detail?id=389642 142 | if (view.chrome && type && type !== force_saveable_type) { 143 | slice = blob.slice || blob.webkitSlice; 144 | blob = slice.call(blob, 0, blob.size, force_saveable_type); 145 | blob_changed = true; 146 | } 147 | // Since I can't be sure that the guessed media type will trigger a download 148 | // in WebKit, I append .download to the filename. 149 | // https://bugs.webkit.org/show_bug.cgi?id=65440 150 | if (webkit_req_fs && name !== "download") { 151 | name += ".download"; 152 | } 153 | if (type === force_saveable_type || webkit_req_fs) { 154 | target_view = view; 155 | } 156 | if (!req_fs) { 157 | fs_error(); 158 | return; 159 | } 160 | fs_min_size += blob.size; 161 | req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) { 162 | fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) { 163 | var save = function() { 164 | dir.getFile(name, create_if_not_found, abortable(function(file) { 165 | file.createWriter(abortable(function(writer) { 166 | writer.onwriteend = function(event) { 167 | target_view.location.href = file.toURL(); 168 | filesaver.readyState = filesaver.DONE; 169 | dispatch(filesaver, "writeend", event); 170 | revoke(file); 171 | }; 172 | writer.onerror = function() { 173 | var error = writer.error; 174 | if (error.code !== error.ABORT_ERR) { 175 | fs_error(); 176 | } 177 | }; 178 | "writestart progress write abort".split(" ").forEach(function(event) { 179 | writer["on" + event] = filesaver["on" + event]; 180 | }); 181 | writer.write(blob); 182 | filesaver.abort = function() { 183 | writer.abort(); 184 | filesaver.readyState = filesaver.DONE; 185 | }; 186 | filesaver.readyState = filesaver.WRITING; 187 | }), fs_error); 188 | }), fs_error); 189 | }; 190 | dir.getFile(name, {create: false}, abortable(function(file) { 191 | // delete file if it already exists 192 | file.remove(); 193 | save(); 194 | }), abortable(function(ex) { 195 | if (ex.code === ex.NOT_FOUND_ERR) { 196 | save(); 197 | } else { 198 | fs_error(); 199 | } 200 | })); 201 | }), fs_error); 202 | }), fs_error); 203 | } 204 | , FS_proto = FileSaver.prototype 205 | , saveAs = function(blob, name) { 206 | return new FileSaver(blob, name); 207 | } 208 | ; 209 | FS_proto.abort = function() { 210 | var filesaver = this; 211 | filesaver.readyState = filesaver.DONE; 212 | dispatch(filesaver, "abort"); 213 | }; 214 | FS_proto.readyState = FS_proto.INIT = 0; 215 | FS_proto.WRITING = 1; 216 | FS_proto.DONE = 2; 217 | 218 | FS_proto.error = 219 | FS_proto.onwritestart = 220 | FS_proto.onprogress = 221 | FS_proto.onwrite = 222 | FS_proto.onabort = 223 | FS_proto.onerror = 224 | FS_proto.onwriteend = 225 | null; 226 | 227 | return saveAs; 228 | }( 229 | typeof self !== "undefined" && self 230 | || typeof window !== "undefined" && window 231 | || this.content 232 | )); 233 | // `self` is undefined in Firefox for Android content script context 234 | // while `this` is nsIContentFrameMessageManager 235 | // with an attribute `content` that corresponds to the window 236 | 237 | if (typeof module !== "undefined" && module !== null) { 238 | module.exports = saveAs; 239 | } else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) { 240 | define([], function() { 241 | return saveAs; 242 | }); 243 | } 244 | -------------------------------------------------------------------------------- /src/css/gantt.css: -------------------------------------------------------------------------------- 1 | body { 2 | line-height: 1; 3 | } 4 | 5 | .ui.segment { 6 | border-radius: 0 !important; 7 | } 8 | 9 | ol.sortable li { 10 | box-shadow: 0 0 0 1px #E6E6E6; 11 | } 12 | 13 | body { 14 | font-family: tahoma, verdana, helvetica; 15 | font-size: 0.8em; 16 | padding: 10px; 17 | background-color: #e5e5e5; 18 | } 19 | 20 | .Gantt { 21 | height: 100%; 22 | /* width: 98%; 23 | margin-left: 1%;*/ 24 | } 25 | 26 | 27 | /* Gantt chart style */ 28 | /* author : Vineeth Krishnan */ 29 | html, 30 | body { 31 | font-size: 15px; 32 | /*height: 100%;*/ 33 | } 34 | 35 | body { 36 | font-family: "Open Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif; 37 | background: #FFFFFF; 38 | margin: 0; 39 | padding: 0; 40 | color: #555555; 41 | text-rendering: optimizeLegibility; 42 | min-width: 320px; 43 | } 44 | 45 | 46 | .ui.button { 47 | font-weight: normal; 48 | text-decoration: none; 49 | } 50 | 51 | a { 52 | color: #009FDA; 53 | text-decoration: none; 54 | -webkit-transition: color 0.3s ease; 55 | -moz-transition: color 0.3s ease; 56 | transition: color 0.3s ease; 57 | } 58 | 59 | a:hover { 60 | color: #00BAFF; 61 | } 62 | 63 | p a { 64 | font-weight: bold; 65 | } 66 | 67 | /******************************* 68 | Global 69 | *******************************/ 70 | .ui.page.grid.segment { 71 | padding: 0.5rem 0; 72 | } 73 | 74 | .hdr-title { 75 | padding-top: 1px; 76 | margin-bottom: 12px; 77 | } 78 | 79 | .task:hover,.sub-task:hover{ 80 | cursor: -webkit-grab; 81 | cursor: -moz-grab; 82 | cursor: url(openhand.cur), move; 83 | } 84 | 85 | 86 | 87 | .task:active,.sub-task:active{ 88 | cursor: -webkit-grabbing; 89 | cursor: -moz-grabbing; 90 | cursor: url(closedhand.cur), move; 91 | } 92 | div.menu-container,#gantt-container{ 93 | transition: all 0.5s ease-in-out 0s; 94 | -moz-transition: all 0.5s ease-in-out 0s; 95 | -webkit-transition: all 0.5s ease-in-out 0s; 96 | padding-left:0px; 97 | } 98 | 99 | .panel-collapsed { 100 | width: 414px; 101 | } 102 | 103 | .panel-expanded { 104 | width: 826px; 105 | } 106 | 107 | .task:hover,.sub-task:hover{ 108 | background-color: rgba(199, 138, 51, 0.5); 109 | } 110 | .item-selector{ 111 | height: 22px; 112 | position: fixed; 113 | display: none; 114 | width: 98%; 115 | background-color: rgba(199, 138, 51, 0.5); 116 | top:10px; 117 | z-index: 1; 118 | } 119 | /******************************* 120 | Responsive 121 | *******************************/ 122 | 123 | /* Mobile Only */ 124 | 125 | @media only screen and (max-width : 768px) { 126 | .ui.page.grid.segment { 127 | padding-top: 0.5rem; 128 | padding-bottom: 2rem; 129 | } 130 | #gantt-container{ 131 | /*min-width: 1300px;*/ 132 | } 133 | } 134 | @media only screen and (max-width : 760px) { 135 | /*Setting viewport for medium devices*/ 136 | .menu-container{ 137 | width: 230px !important; 138 | } 139 | } 140 | @media only screen and (max-width : 1000px) { 141 | 142 | } 143 | 144 | div.menu-container { 145 | border-right: 3px solid rgba(0, 0, 0, 0.2); 146 | overflow: hidden; 147 | /* display: inline-block; */ 148 | min-height: 100%; 149 | left: 0%; 150 | padding: 0 1px; 151 | z-index: 1; 152 | position: absolute; 153 | background: rgb(255, 255, 255); 154 | } 155 | 156 | .menu-header{ 157 | position:relative; 158 | background-color: white; 159 | width: 102%; 160 | padding-top: 4px; 161 | } 162 | 163 | .btnW{ 164 | position:absolute; 165 | top:4px; 166 | right:0; 167 | border: 1px solid #000; 168 | 169 | } 170 | .btnW:before{ 171 | content:'>' 172 | } 173 | .btnW.contract:before{ 174 | content:'<' 175 | } 176 | .menu-header .hdr-title{ 177 | border-bottom: 4px solid rgba(0, 0, 0, 0.3); 178 | height: 0px; 179 | width: 104%; 180 | } 181 | .menu-container ul{ 182 | list-style-type: none; 183 | display: block; 184 | overflow: hidden; 185 | } 186 | .menu-container ul li{ 187 | float:left; 188 | line-height: 1.5; 189 | padding: 0px 2px 0px 2px; 190 | } 191 | 192 | .task-container{ 193 | width: 850px; 194 | } 195 | .task-container-title{ 196 | width: 850px; 197 | } 198 | .task{ 199 | white-space: nowrap; 200 | max-height: 21px; 201 | } 202 | 203 | .task-list-container{ 204 | overflow: auto; 205 | } 206 | .task-list-container li{ 207 | overflow: hidden; 208 | } 209 | 210 | div.gantt-chart-container { 211 | float: right; 212 | width: 100%; 213 | 214 | display: initial; 215 | } 216 | 217 | 218 | .task .col-info { 219 | opacity:0; 220 | margin: 0; 221 | padding: 0; 222 | cursor: pointer; 223 | width: 21px; 224 | height: 21px; 225 | } 226 | 227 | .task .col-info img{ 228 | width: 21px; 229 | height: 21px; 230 | } 231 | 232 | .task:hover .col-info{ 233 | opacity:1; 234 | } 235 | 236 | .task .col-name{ 237 | width:320px; 238 | white-space: nowrap; 239 | overflow: hidden; 240 | } 241 | 242 | .nested { 243 | font-weight: bold; 244 | } 245 | 246 | .nameInput { 247 | width: 290px; 248 | } 249 | 250 | .task .col-name i{ 251 | /* margin-left: 15px; */ 252 | margin-right: 5px; 253 | /* cursor: pointer; */ 254 | } 255 | 256 | .task .col-sortindex { 257 | width:30px; 258 | } 259 | 260 | .task .col-comments { 261 | margin-left: -40px; 262 | color : blue; 263 | font-size: 12px; 264 | } 265 | 266 | .task .col-name div{ 267 | display: inline-block; 268 | } 269 | 270 | .task .col-status{ 271 | /*padding-left: 10px;*/ 272 | width: 80px; 273 | } 274 | 275 | .task .col-start{ 276 | padding-left: 10px; 277 | width: 90px; 278 | } 279 | .task .col-end{ 280 | padding-left: 10px; 281 | width: 90px; 282 | } 283 | .task .col-complete{ 284 | text-align: center; 285 | width: 40px; 286 | } 287 | 288 | .task .col-duration{ 289 | padding-left: 10px; 290 | width: 50px; 291 | } 292 | 293 | .task .col-milestone{ 294 | width: 20px; 295 | min-height: 20px; 296 | } 297 | .task .col-deliverable{ 298 | width: 20px; 299 | min-height: 20px; 300 | } 301 | .task .col-reportable{ 302 | width: 20px; 303 | min-height: 20px; 304 | } 305 | .task .col-timesheets{ 306 | width: 20px; 307 | min-height: 20px; 308 | } 309 | .task .col-acttimesheets{ 310 | width: 20px; 311 | min-height: 20px; 312 | } 313 | 314 | .task .selected { 315 | border: 1px solid lightblue; 316 | } 317 | 318 | i.icon{ 319 | margin: 0; 320 | } 321 | 322 | .ui.button > .icon{ 323 | margin-right: 0; 324 | } 325 | 326 | @media only screen and (max-width: 760px){ 327 | div.gantt-chart-container{ 328 | float: none; 329 | clear: both; 330 | width: 100%; 331 | } 332 | } 333 | 334 | /*#modalEditContainer {*/ 335 | /*position: absolute;*/ 336 | /*top : 50px;*/ 337 | /*left: 50%;*/ 338 | /*}*/ 339 | 340 | #main-view { 341 | /* margin-top: 15px; */ 342 | padding-top: 0px; 343 | } 344 | 345 | #main { 346 | margin-top: 120px; 347 | padding: 0; 348 | } 349 | 350 | #head { 351 | top: 0; 352 | z-index: 2; 353 | margin-top: 0; 354 | width: 100%; 355 | } 356 | 357 | .hold-scroll { 358 | overflow: hidden; 359 | } 360 | 361 | 362 | /*styles for modal edit*/ 363 | .ui.form input[type="text"], .ui.form input[type="email"], .ui.form input[type="date"], .ui.form input[type="password"], .ui.form input[type="number"], .ui.form input[type="url"], .ui.form input[type="tel"] { 364 | padding: 0.4em 0.5em !important; 365 | } 366 | .ui.form select { 367 | padding: 0.2em 0.5em !important; 368 | width: 160px !important; 369 | } 370 | 371 | .ui.textarea, .ui.form textarea { 372 | height: 6em !important; 373 | padding: 0.3em 0.5em !important; 374 | } 375 | .labelsolid { 376 | background-color: white; 377 | font-weight:bold; 378 | float: left; 379 | height: 100%; 380 | line-height: 1em; 381 | margin: 0; 382 | padding: 0; 383 | position: static; 384 | } 385 | .label_80 { 386 | display: block !important; 387 | float: left; 388 | padding-right: 5px; 389 | padding-top: 5px; 390 | text-align: left; 391 | width: 80px !important; 392 | } 393 | .textdiv{ 394 | padding:8px; 395 | } 396 | .ui.form .field { 397 | margin: 0 0 0.5em !important; 398 | } 399 | .noborder { 400 | border-width:0px !important; 401 | border:none !important; 402 | } 403 | .ui.checkbox { 404 | width: 130px !important; 405 | } 406 | .selectdiv{ 407 | padding:2px; 408 | } 409 | .column{ 410 | height:120px; 411 | } 412 | .right_top{ 413 | float:right; 414 | border-left: 1px solid lightgray; 415 | } 416 | /*div.menu-container {*/ 417 | /*top: 120px;*/ 418 | /*}*/ 419 | #top-menu{ 420 | z-index:1000 !important; 421 | background-color: #fafafa; 422 | } 423 | 424 | #zoom-menu .selected { 425 | background-color: lightgray; 426 | } 427 | -------------------------------------------------------------------------------- /src/js/views/sideBar/SidePanel.js: -------------------------------------------------------------------------------- 1 | var TaskItem = require('./TaskItem'); 2 | var NestedTask = require('./NestedTask'); 3 | 4 | function buildTasksOrderFromDOM(container) { 5 | var data = []; 6 | var children = $('
      ' + container.get(0).innerHTML + '
    ').children(); 7 | _.each(children, function(child) { 8 | var $child = $(child); 9 | var obj = { 10 | id: $child.data('id'), 11 | children: [] 12 | }; 13 | var sublist = $child.find('ol'); 14 | if (sublist.length) { 15 | obj.children = buildTasksOrderFromDOM(sublist); 16 | } 17 | data.push(obj); 18 | }); 19 | return data; 20 | } 21 | 22 | var SidePanel = React.createClass({ 23 | displayName: 'SidePanel', 24 | getInitialState() { 25 | return { 26 | selectedRow: null, 27 | lastSelectedRow: null, 28 | selectedModel: null, 29 | editedRow: null 30 | }; 31 | }, 32 | componentDidMount() { 33 | this.props.collection.on('add remove', function() { 34 | this.requestUpdate(); 35 | }, this); 36 | this.props.collection.on('change:hidden', function() { 37 | this.requestUpdate(); 38 | }, this); 39 | this._makeSortable(); 40 | this._setupHighlighter(); 41 | }, 42 | _makeSortable: function() { 43 | var container = $('.task-container'); 44 | container.sortable({ 45 | group: 'sortable', 46 | containerSelector: 'ol', 47 | itemSelector: '.drag-item', 48 | placeholder: '
  • ', 49 | handle: '.col-sortindex', 50 | onDragStart: ($item, position, _super, event) => { 51 | _super($item, position, _super, event); 52 | this.hightlighter.remove(); 53 | }, 54 | onDrag: ($item, position, _super, event) => { 55 | var $placeholder = $('.sort-placeholder'); 56 | var isSubTask = !$($placeholder.parent()).hasClass('task-container'); 57 | $placeholder.css({ 58 | 'padding-left': isSubTask ? '30px' : '0' 59 | }); 60 | _super($item, position, _super, event); 61 | }, 62 | onDrop: ($item, position, _super, event) => { 63 | _super($item, position, _super, event); 64 | setTimeout(() => { 65 | var data = buildTasksOrderFromDOM(container); 66 | this.props.collection.resort(data); 67 | }, 10); 68 | } 69 | }); 70 | }, 71 | _setupHighlighter() { 72 | this.hightlighter = $('
    '); 73 | this.hightlighter.css({ 74 | position: 'absolute', 75 | background: 'grey', 76 | opacity: '0.5', 77 | top: '0', 78 | width: '100%' 79 | }); 80 | 81 | var container = $('.task-container'); 82 | container.mouseenter(function() { 83 | this.hightlighter.appendTo(document.body); 84 | }.bind(this)); 85 | 86 | container.mouseover(function(e) { 87 | var $el = $(e.target); 88 | // TODO: rewrite to find closest ul 89 | if (!$el.data('id')) { 90 | $el = $el.parent(); 91 | if (!$el.data('id')) { 92 | $el = $el.parent(); 93 | } 94 | } 95 | var pos = $el.offset(); 96 | this.hightlighter.css({ 97 | top: pos.top + 'px', 98 | height: $el.height() 99 | }); 100 | }.bind(this)); 101 | 102 | container.mouseleave(function() { 103 | this.hightlighter.remove(); 104 | }.bind(this)); 105 | }, 106 | requestUpdate: (function() { 107 | var waiting = false; 108 | return function () { 109 | if (waiting) { 110 | return; 111 | } 112 | setTimeout(function() { 113 | this.forceUpdate(); 114 | waiting = false; 115 | }.bind(this), 5); 116 | waiting = true; 117 | }; 118 | }()), 119 | componentWillUnmount: function() { 120 | $('.task-container').sortable('destroy'); 121 | this.props.collection.off(null, null, this); 122 | this.hightlighter.remove(); 123 | }, 124 | onSelectRow(selectedModelCid, selectedRow) { 125 | if (this.selecteableRows.indexOf(selectedRow) === -1) { 126 | return; 127 | } 128 | this.setState({ 129 | selectedRow, 130 | selectedModelCid 131 | }); 132 | }, 133 | onEditRow(selectedModelCid, editedRow) { 134 | if (!editedRow) { 135 | var x = window.scrollX, y = window.scrollY; 136 | this.refs.container.getDOMNode().focus(); 137 | window.scrollTo(x, y); 138 | this.setState({ 139 | selectedRow: this.state.lastSelectedRow 140 | }); 141 | } 142 | this.setState({ 143 | selectedModelCid, 144 | editedRow 145 | }); 146 | }, 147 | selecteableRows: ['name', 'complete', 'status', 'start', 'end', 'duration'], 148 | onKeyDown(e) { 149 | if (e.target.tagName === 'INPUT') { 150 | return; 151 | } 152 | e.preventDefault(); 153 | const rows = this.selecteableRows; 154 | let i = rows.indexOf(this.state.selectedRow); 155 | const tasks = this.props.collection; 156 | let modelIndex = tasks.get(this.state.selectedModelCid).get('sortindex'); 157 | if (e.keyCode === 40) { // down 158 | modelIndex = (modelIndex + 1 + tasks.length) % tasks.length; 159 | } else if (e.keyCode === 39) { // right 160 | i = (i + 1 + rows.length) % rows.length; 161 | } else if (e.keyCode === 38) { // up 162 | modelIndex = (modelIndex - 1 + tasks.length) % tasks.length; 163 | } else if (e.keyCode === 37) { // left 164 | i = (i - 1 + rows.length) % rows.length; 165 | } else if (e.keyCode === 13) { // enter 166 | this.setState({ 167 | editedRow: rows[i] 168 | }); 169 | } 170 | 171 | // auto open side panel 172 | if (i >= 2) { 173 | $('.menu-container') 174 | .addClass('panel-expanded') 175 | .removeClass('panel-collapsed'); 176 | } 177 | this.setState({ 178 | selectedRow: rows[i], 179 | selectedModelCid: tasks.at(modelIndex).cid 180 | }); 181 | }, 182 | onClick() { 183 | var x = window.scrollX, y = window.scrollY; 184 | this.refs.container.getDOMNode().focus(); 185 | window.scrollTo(x, y); 186 | }, 187 | onBlur() { 188 | this.setState({ 189 | lastSelectedRow: this.state.selectedRow, 190 | selectedRow: null 191 | }); 192 | }, 193 | getAllStatuses() { 194 | // return [ 195 | // 'Backlog', 196 | // 'Ready', 197 | // 'In Progress', 198 | // 'Complete' 199 | // ]; 200 | return this.props.settings.getAllStatuses(); 201 | }, 202 | getStatusId(statusText) { 203 | return this.props.settings.findStatusId(statusText); 204 | }, 205 | render: function() { 206 | var tasks = []; 207 | this.props.collection.each((task) => { 208 | if (task.parent) { 209 | return; 210 | } 211 | if (task.get('hidden')) { 212 | return; 213 | } 214 | if (task.children.length) { 215 | tasks.push(React.createElement(NestedTask, { 216 | model: task, 217 | key: task.cid, 218 | dateFormat: this.props.dateFormat, 219 | onSelectRow: this.onSelectRow, 220 | onEditRow: this.onEditRow, 221 | editedRow: this.state.editedRow, 222 | selectedRow: this.state.selectedRow, 223 | selectedModelCid: this.state.selectedModelCid, 224 | getAllStatuses: this.getAllStatuses, 225 | getStatusId: this.getStatusId 226 | })); 227 | } else { 228 | tasks.push(React.createElement('li', { 229 | key: task.cid, 230 | className: 'drag-item', 231 | 'data-id': task.cid 232 | }, 233 | React.createElement(TaskItem, { 234 | model: task, 235 | dateFormat: this.props.dateFormat, 236 | onSelectRow: this.onSelectRow, 237 | onEditRow: this.onEditRow, 238 | editedRow: (this.state.selectedModelCid === task.cid) && this.state.editedRow, 239 | selectedRow: (this.state.selectedModelCid === task.cid) && this.state.selectedRow, 240 | getAllStatuses: this.getAllStatuses, 241 | getStatusId: this.getStatusId 242 | }) 243 | )); 244 | } 245 | }); 246 | return ( 247 |
      255 | {tasks} 256 |
    257 | ); 258 | } 259 | }); 260 | 261 | module.exports = SidePanel; 262 | -------------------------------------------------------------------------------- /src/libs/xmlToJSON.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 William Summers, metaTribal LLC 2 | * adapted from https://developer.mozilla.org/en-US/docs/JXON 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * @author William Summers 18 | * 19 | */ 20 | var xmlToJSON = (function () { 21 | 22 | this.version = "1.1"; 23 | 24 | var options = { // set up the default options 25 | mergeCDATA: true, // extract cdata and merge with text 26 | grokAttr: true, // convert truthy attributes to boolean, etc 27 | grokText: true, // convert truthy text/attr to boolean, etc 28 | normalize: true, // collapse multiple spaces to single space 29 | xmlns: true, // include namespaces as attribute in output 30 | namespaceKey: '_ns', // tag name for namespace objects 31 | textKey: '_text', // tag name for text nodes 32 | valueKey: '_value', // tag name for attribute values 33 | attrKey: '_attr', // tag for attr groups 34 | cdataKey: '_cdata', // tag for cdata nodes (ignored if mergeCDATA is true) 35 | attrsAsObject: true, // if false, key is used as prefix to name, set prefix to '' to merge children and attrs. 36 | stripAttrPrefix: true, // remove namespace prefixes from attributes 37 | stripElemPrefix: true, // for elements of same name in diff namespaces, you can enable namespaces and access the nskey property 38 | childrenAsArray: true // force children into arrays 39 | }; 40 | 41 | var prefixMatch = new RegExp(/(?!xmlns)^.*:/); 42 | var trimMatch = new RegExp(/^\s+|\s+$/g); 43 | 44 | this.grokType = function (sValue) { 45 | if (/^\s*$/.test(sValue)) { 46 | return null; 47 | } 48 | if (/^(?:true|false)$/i.test(sValue)) { 49 | return sValue.toLowerCase() === "true"; 50 | } 51 | if (isFinite(sValue)) { 52 | return parseFloat(sValue); 53 | } 54 | return sValue; 55 | }; 56 | 57 | this.parseString = function (xmlString, opt) { 58 | return this.parseXML(this.stringToXML(xmlString), opt); 59 | } 60 | 61 | this.parseXML = function (oXMLParent, opt) { 62 | 63 | // initialize options 64 | for (key in opt) { 65 | options[key] = opt[key]; 66 | } 67 | 68 | var vResult = {}, 69 | nLength = 0, 70 | sCollectedTxt = ""; 71 | 72 | // parse namespace information 73 | if (options.xmlns && oXMLParent.namespaceURI) { 74 | vResult[options.namespaceKey] = oXMLParent.namespaceURI; 75 | } 76 | 77 | // parse attributes 78 | // using attributes property instead of hasAttributes method to support older browsers 79 | if (oXMLParent.attributes && oXMLParent.attributes.length > 0) { 80 | var vAttribs = {}; 81 | 82 | for (nLength; nLength < oXMLParent.attributes.length; nLength++) { 83 | oAttrib = oXMLParent.attributes.item(nLength); 84 | vContent = {}; 85 | attribName = ''; 86 | 87 | if (options.stripAttrPrefix) { 88 | attribName = oAttrib.name.replace(prefixMatch, ''); 89 | 90 | } else { 91 | attribName = oAttrib.name; 92 | } 93 | 94 | if (options.grokAttr) { 95 | vContent[options.valueKey] = grokType(oAttrib.value.replace(trimMatch, '')); 96 | } else { 97 | vContent[options.valueKey] = oAttrib.value.replace(trimMatch, ''); 98 | } 99 | 100 | if (options.xmlns && oAttrib.namespaceURI) { 101 | vContent[options.namespaceKey] = oAttrib.namespaceURI; 102 | } 103 | 104 | if (options.attrsAsObject) { // attributes with same local name must enable prefixes 105 | vAttribs[attribName] = vContent; 106 | } else { 107 | vResult[options.attrKey + attribName] = vContent; 108 | } 109 | } 110 | 111 | if (options.attrsAsObject) { 112 | vResult[options.attrKey] = vAttribs; 113 | } else {} 114 | } 115 | 116 | // iterate over the children 117 | if (oXMLParent.hasChildNodes()) { 118 | for (var oNode, sProp, vContent, nItem = 0; nItem < oXMLParent.childNodes.length; nItem++) { 119 | oNode = oXMLParent.childNodes.item(nItem); 120 | 121 | if (oNode.nodeType === 4) { 122 | if (options.mergeCDATA) { 123 | sCollectedTxt += oNode.nodeValue; 124 | } else { 125 | if (vResult.hasOwnProperty(options.cdataKey)) { 126 | if (vResult[options.cdataKey].constructor !== Array) { 127 | vResult[options.cdataKey] = [vResult[options.cdataKey]]; 128 | } 129 | vResult[options.cdataKey].push(oNode.nodeValue); 130 | 131 | } else { 132 | if (options.childrenAsArray) { 133 | vResult[options.cdataKey] = []; 134 | vResult[options.cdataKey].push(oNode.nodeValue); 135 | } else { 136 | vResult[options.cdataKey] = oNode.nodeValue; 137 | } 138 | } 139 | } 140 | } /* nodeType is "CDATASection" (4) */ 141 | else if (oNode.nodeType === 3) { 142 | sCollectedTxt += oNode.nodeValue; 143 | } /* nodeType is "Text" (3) */ 144 | else if (oNode.nodeType === 1) { /* nodeType is "Element" (1) */ 145 | 146 | if (nLength === 0) { 147 | vResult = {}; 148 | } 149 | 150 | // using nodeName to support browser (IE) implementation with no 'localName' property 151 | if (options.stripElemPrefix) { 152 | sProp = oNode.nodeName.replace(prefixMatch, ''); 153 | } else { 154 | sProp = oNode.nodeName; 155 | } 156 | 157 | vContent = xmlToJSON.parseXML(oNode); 158 | 159 | if (vResult.hasOwnProperty(sProp)) { 160 | if (vResult[sProp].constructor !== Array) { 161 | vResult[sProp] = [vResult[sProp]]; 162 | } 163 | vResult[sProp].push(vContent); 164 | 165 | } else { 166 | if (options.childrenAsArray) { 167 | vResult[sProp] = []; 168 | vResult[sProp].push(vContent); 169 | } else { 170 | vResult[sProp] = vContent; 171 | } 172 | nLength++; 173 | } 174 | } 175 | } 176 | } else if (!sCollectedTxt) { // no children and no text, return null 177 | if (options.childrenAsArray) { 178 | vResult[options.textKey] = []; 179 | vResult[options.textKey].push(null); 180 | } else { 181 | vResult[options.textKey] = null; 182 | } 183 | } 184 | 185 | if (sCollectedTxt) { 186 | if (options.grokText) { 187 | value = this.grokType(sCollectedTxt.replace(trimMatch, '')); 188 | if (value !== null && value !== undefined) { 189 | vResult[options.textKey] = value; 190 | } 191 | } else if (options.normalize) { 192 | vResult[options.textKey] = sCollectedTxt.replace(trimMatch, '').replace(/\s+/g, " "); 193 | } else { 194 | vResult[options.textKey] = sCollectedTxt.replace(trimMatch, ''); 195 | } 196 | } 197 | 198 | return vResult; 199 | } 200 | 201 | 202 | // Convert xmlDocument to a string 203 | // Returns null on failure 204 | this.xmlToString = function (xmlDoc) { 205 | try { 206 | var xmlString = xmlDoc.xml ? xmlDoc.xml : (new XMLSerializer()).serializeToString(xmlDoc); 207 | return xmlString; 208 | } catch (err) { 209 | return null; 210 | } 211 | } 212 | 213 | // Convert a string to XML Node Structure 214 | // Returns null on failure 215 | this.stringToXML = function (xmlString) { 216 | try { 217 | var xmlDoc = null; 218 | 219 | if (window.DOMParser) { 220 | 221 | var parser = new DOMParser(); 222 | xmlDoc = parser.parseFromString(xmlString, "text/xml"); 223 | 224 | return xmlDoc; 225 | } else { 226 | xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); 227 | xmlDoc.async = false; 228 | xmlDoc.loadXML(xmlString); 229 | 230 | return xmlDoc; 231 | } 232 | } catch (e) { 233 | return null; 234 | } 235 | } 236 | 237 | return this; 238 | })(); 239 | -------------------------------------------------------------------------------- /src/js/models/TaskModel.js: -------------------------------------------------------------------------------- 1 | var ResCollection = require('../collections/ResourceReferenceCollection'); 2 | 3 | var util = require('../utils/util'); 4 | var params = util.getURLParams(); 5 | 6 | var SubTasks = Backbone.Collection.extend({ 7 | comparator: function(model) { 8 | return model.get('sortindex'); 9 | } 10 | }); 11 | 12 | var resLinks = new ResCollection(); 13 | resLinks.fetch(); 14 | 15 | var TaskModel = Backbone.Model.extend({ 16 | defaults: { 17 | // MAIN PARAMS 18 | name: 'New task', 19 | description: '', 20 | complete: 0, // 0% - 100% percents 21 | sortindex: 0, // place on side menu, starts from 0 22 | depend: [], // id of tasks 23 | status: '110', // 110 - complete, 109 - open, 108 - ready 24 | start: new Date(), 25 | end: new Date(), 26 | parentid: 0, 27 | Comments: 0, 28 | 29 | color: '#0090d3', // user color, not used for now 30 | 31 | // some additional properties 32 | resources: [], //list of id 33 | health: 21, 34 | reportable: false, 35 | wo: 2, //Select List in properties modal (configdata) 36 | milestone: false, //Check box in properties modal (true/false) 37 | deliverable: false, //Check box in properties modal (true/false) 38 | financial: false, //Check box in properties modal (true/false) 39 | timesheets: false, //Check box in properties modal (true/false) 40 | acttimesheets: false, //Check box in properties modal (true/false) 41 | 42 | // server specific params 43 | // don't use them on client side 44 | ProjectRef: params.project, 45 | WBS_ID: params.profile, 46 | sitekey: params.sitekey, 47 | 48 | 49 | // params for application views 50 | // should be removed from JSON 51 | hidden: false, 52 | collapsed: false, 53 | hightlight: '' 54 | }, 55 | initialize: function() { 56 | // self validation 57 | this.listenTo(this, 'change:resources', function() { 58 | resLinks.updateResourcesForTask(this); 59 | }); 60 | 61 | this.listenTo(this, 'change:milestone', function() { 62 | if (this.get('milestone')) { 63 | this.set('start', new Date(this.get('end'))); 64 | } 65 | }); 66 | 67 | // children references 68 | this.children = new SubTasks(); 69 | this.depends = new Backbone.Collection(); 70 | 71 | this.listenTo(this.children, 'add', function() { 72 | this.set('milestone', false); 73 | }); 74 | 75 | // removing refs 76 | this.listenTo(this.children, 'change:parentid', function(child) { 77 | if (child.get('parentid') === this.id) { 78 | return; 79 | } 80 | this.children.remove(child); 81 | }); 82 | 83 | this.listenTo(this.children, 'add', function(child) { 84 | child.parent = this; 85 | }); 86 | this.listenTo(this.children, 'change:sortindex', function() { 87 | this.children.sort(); 88 | }); 89 | this.listenTo(this.children, 'add remove change:start change:end', function() { 90 | this._checkTime(); 91 | }); 92 | 93 | this.listenTo(this, 'change:collapsed', function() { 94 | this.children.each(function(child) { 95 | if (this.get('collapsed')) { 96 | child.hide(); 97 | } else { 98 | child.show(); 99 | } 100 | }.bind(this)); 101 | }); 102 | this.listenTo(this, 'destroy', function() { 103 | this.children.toArray().forEach(function(child) { 104 | child.destroy(); 105 | }); 106 | this.stopListening(); 107 | }); 108 | 109 | // checking nested state 110 | this.listenTo(this.children, 'add remove', this._checkNested); 111 | 112 | // time checking 113 | this.listenTo(this.children, 'add remove change:complete', this._checkComplete); 114 | this._listenDependsCollection(); 115 | }, 116 | isNested: function() { 117 | return !!this.children.length; 118 | }, 119 | show: function() { 120 | this.set('hidden', false); 121 | }, 122 | hide: function() { 123 | this.set('hidden', true); 124 | this.children.forEach((child) => { 125 | child.hide(); 126 | }); 127 | }, 128 | dependOn: function(beforeModel, silent) { 129 | this.depends.add(beforeModel, {silent: silent}); 130 | if (this.get('start') < beforeModel.get('end')) { 131 | this.moveToStart(beforeModel.get('end')); 132 | } 133 | if (!silent) { 134 | this.save(); 135 | } 136 | }, 137 | hasInDeps: function (model) { 138 | return !!this.depends.get(model.id); 139 | }, 140 | hasDeps: function() { 141 | return this.depends.length !== 0; 142 | }, 143 | toJSON: function() { 144 | var json = Backbone.Model.prototype.toJSON.call(this); 145 | delete json.resources; 146 | delete json.hidden; 147 | delete json.collapsed; 148 | delete json.hightlight; 149 | json.depend = json.depend.join(','); 150 | return json; 151 | }, 152 | hasParent: function(parentForCheck) { 153 | var parent = this.parent; 154 | while(true) { 155 | if (!parent) { 156 | return false; 157 | } 158 | if (parent === parentForCheck) { 159 | return true; 160 | } 161 | parent = parent.parent; 162 | } 163 | }, 164 | clearDependence: function() { 165 | this.depends.toArray().forEach((m) => { 166 | this.depends.remove(m); 167 | }); 168 | }, 169 | _listenDependsCollection: function() { 170 | this.listenTo(this.depends, 'remove add', function() { 171 | var ids = this.depends.map((m) => m.id); 172 | this.set('depend', ids).save(); 173 | }); 174 | 175 | this.listenTo(this.depends, 'add', function(beforeModel) { 176 | this.collection.trigger('depend:add', beforeModel, this); 177 | }); 178 | 179 | this.listenTo(this.depends, 'remove', function(beforeModel) { 180 | this.collection.trigger('depend:remove', beforeModel, this); 181 | }); 182 | 183 | this.listenTo(this.depends, 'change:end', function(beforeModel) { 184 | if (this.parent && this.parent.underMoving) { 185 | return; 186 | } 187 | // check infinite depend loop 188 | var inDeps = [this]; 189 | var isInfinite = false; 190 | 191 | function checkDeps(model) { 192 | if (!model.depends.length) { 193 | return; 194 | } 195 | model.depends.each((m) => { 196 | if (inDeps.indexOf(m) > -1 || isInfinite) { 197 | isInfinite = true; 198 | return; 199 | } 200 | inDeps.push(m); 201 | checkDeps(m); 202 | }); 203 | } 204 | checkDeps(this); 205 | 206 | if (isInfinite) { 207 | return; 208 | } 209 | if (this.get('start') < beforeModel.get('end')) { 210 | this.moveToStart(beforeModel.get('end')); 211 | } 212 | }); 213 | }, 214 | _checkNested: function() { 215 | this.trigger('nestedStateChange', this); 216 | }, 217 | parse: function(response) { 218 | var start, end; 219 | if(_.isString(response.start)){ 220 | start = Date.parseExact(util.correctdate(response.start), 'dd/MM/yyyy') || 221 | new Date(util.correctdate(response.start)); 222 | } else if (_.isDate(response.start)) { 223 | start = response.start; 224 | } else { 225 | start = new Date(); 226 | } 227 | 228 | 229 | 230 | if(_.isString(response.end)){ 231 | end = Date.parseExact(util.correctdate(response.end), 'dd/MM/yyyy') || 232 | new Date(util.correctdate(response.end)); 233 | } else if (_.isDate(response.end)) { 234 | end = response.end; 235 | } else { 236 | end = new Date(); 237 | } 238 | 239 | response.start = start < end ? start : end; 240 | response.end = start < end ? end : start; 241 | 242 | response.start = util.convertDateToUTC(response.start); 243 | response.end = util.convertDateToUTC(response.end); 244 | response.parentid = parseInt(response.parentid || '0', 10); 245 | 246 | // remove null params 247 | _.each(response, function(val, key) { 248 | if (val === null) { 249 | delete response[key]; 250 | } 251 | }); 252 | 253 | // update resources as list of ID 254 | var ids = []; 255 | (response.Resources || []).forEach(function(resInfo) { 256 | ids.push(resInfo.ResID); 257 | }); 258 | response.Resources = undefined; 259 | response.resources = ids; 260 | if (response.milestone) { 261 | response.start = response.end; 262 | } 263 | 264 | 265 | // update deps for new API (array of deps) 266 | if (_.isNumber(response.depend)) { 267 | response.depend = [response.depend]; 268 | } 269 | if (_.isString(response.depend)) { 270 | response.depend = _.compact(response.depend.split(',')); 271 | } 272 | return response; 273 | }, 274 | _checkTime: function() { 275 | if (this.children.length === 0) { 276 | return; 277 | } 278 | var startTime = this.children.at(0).get('start'); 279 | var endTime = this.children.at(0).get('end'); 280 | this.children.each(function(child) { 281 | var childStartTime = child.get('start'); 282 | var childEndTime = child.get('end'); 283 | if(childStartTime < startTime) { 284 | startTime = childStartTime; 285 | } 286 | if(childEndTime > endTime){ 287 | endTime = childEndTime; 288 | } 289 | }); 290 | this.set('start', startTime); 291 | this.set('end', endTime); 292 | }, 293 | _checkComplete: function() { 294 | var complete = 0; 295 | var length = this.children.length; 296 | if (length) { 297 | this.children.each(function(child) { 298 | complete += child.get('complete') / length; 299 | }); 300 | } 301 | this.set('complete', Math.round(complete)); 302 | }, 303 | moveToStart: function(newStart) { 304 | // do nothing if new start is the same as current 305 | if (newStart.toDateString() === this.get('start').toDateString()) { 306 | return; 307 | } 308 | 309 | // calculate offset 310 | // var daysDiff = Math.floor((newStart.time() - this.get('start').time()) / 1000 / 60 / 60 / 24) 311 | var daysDiff = Date.daysdiff(newStart, this.get('start')) - 1; 312 | if (newStart < this.get('start')) { 313 | daysDiff *= -1; 314 | } 315 | 316 | // change dates 317 | this.set({ 318 | start: newStart.clone(), 319 | end: this.get('end').clone().addDays(daysDiff) 320 | }); 321 | 322 | // changes dates in all children 323 | this.underMoving = true; 324 | this._moveChildren(daysDiff); 325 | this.underMoving = false; 326 | }, 327 | _moveChildren: function(days) { 328 | this.children.each(function(child) { 329 | child.move(days); 330 | }); 331 | }, 332 | saveWithChildren: function() { 333 | this.save(); 334 | this.children.each(function(task) { 335 | task.saveWithChildren(); 336 | }); 337 | }, 338 | move: function(days) { 339 | this.set({ 340 | start: this.get('start').clone().addDays(days), 341 | end: this.get('end').clone().addDays(days) 342 | }); 343 | this._moveChildren(days); 344 | }, 345 | getOutlineLevel: function() { 346 | var level = 1; 347 | var parent = this.parent; 348 | while(true) { 349 | if (!parent) { 350 | return level; 351 | } 352 | level++; 353 | parent = parent.parent; 354 | } 355 | }, 356 | getOutlineNumber: function() { 357 | if (this.parent) { 358 | const index = this.parent.children.models.indexOf(this); 359 | return this.parent.getOutlineNumber() + '.' + (index + 1); 360 | } 361 | 362 | let number = 1; 363 | for(let i = 0; i < this.collection.length; i++) { 364 | const model = this.collection.at(i); 365 | if (model === this) { 366 | return number; 367 | } else if (!model.parent) { 368 | number += 1; 369 | } 370 | } 371 | } 372 | }); 373 | 374 | module.exports = TaskModel; 375 | -------------------------------------------------------------------------------- /src/js/models/SettingModel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var util = require('../utils/util'); 4 | 5 | var SettingModel = Backbone.Model.extend({ 6 | defaults: { 7 | interval: 'daily', 8 | //days per interval 9 | dpi: 1 10 | }, 11 | initialize: function(attrs, params) { 12 | this.statuses = undefined; 13 | this.sattr = { 14 | hData: {}, 15 | dragInterval: 1, 16 | daysWidth: 5, 17 | cellWidth: 35, 18 | minDate: new Date(2020,1,1), 19 | maxDate: new Date(0,0,0), 20 | boundaryMin: new Date(0,0,0), 21 | boundaryMax: new Date(2020,1,1), 22 | //months per cell 23 | mpc: 1 24 | }; 25 | 26 | this.sdisplay = { 27 | screenWidth: $('#gantt-container').innerWidth() + 786, 28 | tHiddenWidth: 305, 29 | tableWidth: 710 30 | }; 31 | 32 | this.collection = params.tasks; 33 | this.calculateIntervals(); 34 | this.on('change:interval change:dpi', this.calculateIntervals); 35 | this.listenTo(this.collection, 'add change:end', _.debounce(function() { 36 | this.calculateIntervals(); 37 | this.trigger('change:width'); 38 | }, 500)); 39 | }, 40 | getSetting: function(from, attr){ 41 | if(attr){ 42 | return this['s' + from][attr]; 43 | } 44 | return this['s' + from]; 45 | }, 46 | findStatusId : function(status) { 47 | for(var category in this.statuses.cfgdata) { 48 | var data = this.statuses.cfgdata[category]; 49 | if (data.Category === 'Task Status') { 50 | for (var i in data.data) { 51 | var statusItem = data.data[i]; 52 | if (statusItem.cfg_item.toLowerCase() === status.toLowerCase()) { 53 | return statusItem.ID; 54 | } 55 | } 56 | } 57 | } 58 | }, 59 | findStatusForId : function(id) { 60 | for(var category in this.statuses.cfgdata) { 61 | var data = this.statuses.cfgdata[category]; 62 | if (data.Category === 'Task Status') { 63 | for (var i in data.data) { 64 | var statusItem = data.data[i]; 65 | if (statusItem.ID.toString().toLowerCase() === id.toString().toLowerCase()) { 66 | return statusItem.ID; 67 | } 68 | } 69 | } 70 | } 71 | }, 72 | getAllStatuses() { 73 | const statuses = []; 74 | for(var category in this.statuses.cfgdata) { 75 | var data = this.statuses.cfgdata[category]; 76 | if (data.Category === 'Task Status') { 77 | for (var i in data.data) { 78 | var statusItem = data.data[i]; 79 | statuses.push(statusItem.cfg_item); 80 | } 81 | } 82 | } 83 | return statuses; 84 | }, 85 | getDefaultStatusId() { 86 | for(var category in this.statuses.cfgdata) { 87 | var data = this.statuses.cfgdata[category]; 88 | if (data.Category === 'Task Status') { 89 | for (var i in data.data) { 90 | var statusItem = data.data[i]; 91 | if (statusItem.cDefault) { 92 | return statusItem.ID; 93 | } 94 | } 95 | console.warn('no default status in config'); 96 | } 97 | } 98 | }, 99 | getClosedStatusId() { 100 | for(var category in this.statuses.cfgdata) { 101 | var data = this.statuses.cfgdata[category]; 102 | if (data.Category === 'Task Status') { 103 | for (var i in data.data) { 104 | var statusItem = data.data[i]; 105 | if (statusItem.cClose) { 106 | return statusItem.ID; 107 | } 108 | } 109 | console.warn('no closed status in config'); 110 | } 111 | } 112 | }, 113 | findDefaultStatusId: function() { 114 | for(var category in this.statuses.cfgdata) { 115 | var data = this.statuses.cfgdata[category]; 116 | if (data.Category === 'Task Status') { 117 | for (var i in data.data) { 118 | var statusItem = data.data[i]; 119 | if (statusItem.cDefault) { 120 | return statusItem.ID; 121 | } 122 | } 123 | } 124 | } 125 | }, 126 | findHealthId : function(health) { 127 | for(var category in this.statuses.cfgdata) { 128 | var data = this.statuses.cfgdata[category]; 129 | if (data.Category === 'Task Health') { 130 | for (var i in data.data) { 131 | var statusItem = data.data[i]; 132 | if (statusItem.cfg_item.toLowerCase() === health.toLowerCase()) { 133 | return statusItem.ID; 134 | } 135 | } 136 | } 137 | } 138 | }, 139 | findHealthForId : function(id) { 140 | for(var category in this.statuses.cfgdata) { 141 | var data = this.statuses.cfgdata[category]; 142 | if (data.Category === 'Task Health') { 143 | for (var i in data.data) { 144 | var statusItem = data.data[i]; 145 | if (statusItem.ID.toString().toLowerCase() === id.toString().toLowerCase()) { 146 | return statusItem.ID; 147 | } 148 | } 149 | } 150 | } 151 | }, 152 | findDefaultHealthId : function() { 153 | for(var category in this.statuses.cfgdata) { 154 | var data = this.statuses.cfgdata[category]; 155 | if (data.Category === 'Task Health') { 156 | for (var i in data.data) { 157 | var statusItem = data.data[i]; 158 | if (statusItem.cDefault) { 159 | return statusItem.ID; 160 | } 161 | } 162 | } 163 | } 164 | }, 165 | findWOId : function(wo) { 166 | for(var i in this.statuses.wodata[0].data) { 167 | var data = this.statuses.wodata[0].data[i]; 168 | if (data.WONumber.toLowerCase() === wo.toLowerCase()) { 169 | return data.ID; 170 | } 171 | } 172 | }, 173 | findWOForId : function(id) { 174 | for(var i in this.statuses.wodata[0].data) { 175 | var data = this.statuses.wodata[0].data[i]; 176 | if (data.ID.toString() === id.toString()) { 177 | return data.WONumber; 178 | } 179 | } 180 | }, 181 | findDefaultWOId : function() { 182 | return this.statuses.wodata[0].data[0].ID; 183 | }, 184 | getDateFormat : function() { 185 | return 'dd/mm/yy'; 186 | }, 187 | calcminmax: function() { 188 | var minDate = new Date(), maxDate = minDate.clone().addYears(1); 189 | 190 | this.collection.each(function(model) { 191 | if (model.get('start').compareTo(minDate) === -1) { 192 | minDate=model.get('start'); 193 | } 194 | if (model.get('end').compareTo(maxDate) === 1) { 195 | maxDate=model.get('end'); 196 | } 197 | }); 198 | this.sattr.minDate = minDate; 199 | this.sattr.maxDate = maxDate; 200 | }, 201 | setAttributes: function() { 202 | var end,sattr=this.sattr,dattr=this.sdisplay,duration,size,cellWidth,dpi,retfunc,start,last,i=0,j=0,iLen=0,next=null; 203 | 204 | var interval = this.get('interval'); 205 | 206 | if (interval === 'daily') { 207 | this.set('dpi', 1, {silent: true}); 208 | end = sattr.maxDate.clone().addDays(60); 209 | sattr.boundaryMin = sattr.minDate.clone().addDays(-1 * 20); 210 | sattr.daysWidth = 15; 211 | sattr.cellWidth = sattr.daysWidth; 212 | sattr.dragInterval = sattr.daysWidth; 213 | retfunc = function(date){ 214 | return date.clone().addDays(1); 215 | }; 216 | sattr.mpc = 1; 217 | 218 | } else if(interval === 'weekly') { 219 | this.set('dpi', 7, {silent: true}); 220 | end = sattr.maxDate.clone().addDays(20 * 7); 221 | sattr.boundaryMin = sattr.minDate.clone().addDays(-1 * 20).moveToDayOfWeek(1, -1); 222 | sattr.daysWidth = 5; 223 | sattr.cellWidth = sattr.daysWidth * 7; 224 | sattr.dragInterval = sattr.daysWidth; 225 | sattr.mpc = 1; 226 | retfunc = function(date){ 227 | return date.clone().addDays(7); 228 | }; 229 | } else if (interval === 'monthly') { 230 | this.set('dpi', 30, {silent: true}); 231 | end = sattr.maxDate.clone().addDays(12 * 30); 232 | sattr.boundaryMin = sattr.minDate.clone().addDays(-1 * 20).moveToFirstDayOfMonth(); 233 | sattr.daysWidth = 2; 234 | sattr.cellWidth = 'auto'; 235 | sattr.dragInterval = 7 * sattr.daysWidth; 236 | sattr.mpc = 1; 237 | retfunc = function(date){ 238 | return date.clone().addMonths(1); 239 | }; 240 | } else if (interval === 'quarterly') { 241 | this.set('dpi', 30, {silent: true}); 242 | end = sattr.maxDate.clone().addDays(20 * 30); 243 | sattr.boundaryMin = sattr.minDate.clone().addDays(-1 * 20); 244 | sattr.boundaryMin.moveToFirstDayOfQuarter(); 245 | sattr.daysWidth = 1; 246 | sattr.cellWidth = 'auto'; 247 | sattr.dragInterval = 30 * sattr.daysWidth; 248 | sattr.mpc = 3; 249 | retfunc = function(date){ 250 | return date.clone().addMonths(3); 251 | }; 252 | } else if (interval === 'fix') { 253 | cellWidth = 30; 254 | duration = Date.daysdiff(sattr.minDate, sattr.maxDate); 255 | size = dattr.screenWidth - dattr.tHiddenWidth - 100; 256 | sattr.daysWidth = size / duration; 257 | dpi = Math.round(cellWidth / sattr.daysWidth); 258 | this.set('dpi', dpi, {silent: true}); 259 | sattr.cellWidth = dpi * sattr.daysWidth; 260 | sattr.boundaryMin = sattr.minDate.clone().addDays(-2 * dpi); 261 | sattr.dragInterval = Math.round(0.3 * dpi) * sattr.daysWidth; 262 | end = sattr.maxDate.clone().addDays(30 * 10); 263 | sattr.mpc = Math.max(1, Math.round(dpi / 30)); 264 | retfunc = function(date){ 265 | return date.clone().addDays(dpi); 266 | }; 267 | } else if (interval==='auto') { 268 | dpi = this.get('dpi'); 269 | sattr.cellWidth = (1 + Math.log(dpi)) * 12; 270 | sattr.daysWidth = sattr.cellWidth / dpi; 271 | sattr.boundaryMin = sattr.minDate.clone().addDays(-20 * dpi); 272 | end = sattr.maxDate.clone().addDays(30 * 10); 273 | sattr.mpc = Math.max(1, Math.round(dpi / 30)); 274 | retfunc = function(date) { 275 | return date.clone().addDays(dpi); 276 | }; 277 | sattr.dragInterval = Math.round(0.3 * dpi) * sattr.daysWidth; 278 | } 279 | var hData = { 280 | '1': [], 281 | '2': [], 282 | '3': [] 283 | }; 284 | var hdata3 = []; 285 | 286 | start = sattr.boundaryMin; 287 | 288 | last = start; 289 | if (interval === 'monthly' || interval === 'quarterly') { 290 | var durfunc; 291 | if (interval==='monthly') { 292 | durfunc = function(date) { 293 | return Date.getDaysInMonth(date.getFullYear(),date.getMonth()); 294 | }; 295 | } else { 296 | durfunc = function(date) { 297 | return Date.getDaysInQuarter(date.getFullYear(), date.getQuarter()); 298 | }; 299 | } 300 | while (last.compareTo(end) === -1) { 301 | hdata3.push({ 302 | duration: durfunc(last), 303 | text: last.getDate() 304 | }); 305 | next = retfunc(last); 306 | last = next; 307 | } 308 | } else { 309 | var intervaldays = this.get('dpi'); 310 | while (last.compareTo(end) === -1) { 311 | var isHoly = last.getDay() === 6 || last.getDay() === 0; 312 | hdata3.push({ 313 | duration: intervaldays, 314 | text: last.getDate(), 315 | holy : (interval === 'daily') && isHoly 316 | }); 317 | next = retfunc(last); 318 | last = next; 319 | } 320 | } 321 | sattr.boundaryMax = end = last; 322 | hData['3'] = hdata3; 323 | 324 | //enter duration of first date to end of year 325 | var inter = Date.daysdiff(start, new Date(start.getFullYear(), 11, 31)); 326 | hData['1'].push({ 327 | duration: inter, 328 | text: start.getFullYear() 329 | }); 330 | for(i = start.getFullYear() + 1, iLen = end.getFullYear(); i < iLen; i++){ 331 | inter = Date.isLeapYear(i) ? 366 : 365; 332 | hData['1'].push({ 333 | duration: inter, 334 | text: i 335 | }); 336 | } 337 | //enter duration of last year upto end date 338 | if (start.getFullYear()!==end.getFullYear()) { 339 | inter = Date.daysdiff(new Date(end.getFullYear(), 0, 1), end); 340 | hData['1'].push({ 341 | duration: inter, 342 | text: end.getFullYear() 343 | }); 344 | } 345 | 346 | //enter duration of first month 347 | hData['2'].push({ 348 | duration: Date.daysdiff(start, start.clone().moveToLastDayOfMonth()), 349 | text: util.formatdata(start.getMonth(), 'm') 350 | }); 351 | 352 | j = start.getMonth() + 1; 353 | i = start.getFullYear(); 354 | iLen = end.getFullYear(); 355 | var endmonth = end.getMonth(); 356 | 357 | while (i <= iLen) { 358 | while(j < 12) { 359 | if (i === iLen && j === endmonth) { 360 | break; 361 | } 362 | hData['2'].push({ 363 | duration: Date.getDaysInMonth(i, j), 364 | text: util.formatdata(j, 'm') 365 | }); 366 | j += 1; 367 | } 368 | i += 1; 369 | j = 0; 370 | } 371 | if (end.getMonth() !== start.getMonth() && end.getFullYear() !== start.getFullYear()) { 372 | hData['2'].push({ 373 | duration: Date.daysdiff(end.clone().moveToFirstDayOfMonth(), end), 374 | text: util.formatdata(end.getMonth(), 'm') 375 | }); 376 | } 377 | sattr.hData = hData; 378 | }, 379 | calculateIntervals: function() { 380 | this.calcminmax(); 381 | this.setAttributes(); 382 | }, 383 | conDToT:(function(){ 384 | var dToText={ 385 | 'start':function(value){ 386 | return value.toString('dd/MM/yyyy'); 387 | }, 388 | 'end':function(value){ 389 | return value.toString('dd/MM/yyyy'); 390 | }, 391 | 'duration':function(value,model){ 392 | return Date.daysdiff(model.start,model.end)+' d'; 393 | }, 394 | 'status':function(value){ 395 | var statuses={ 396 | '110':'complete', 397 | '109':'open', 398 | '108' : 'ready' 399 | }; 400 | return statuses[value]; 401 | } 402 | 403 | }; 404 | return function(field,value,model){ 405 | return dToText[field]?dToText[field](value,model):value; 406 | }; 407 | }()) 408 | }); 409 | 410 | module.exports = SettingModel; 411 | --------------------------------------------------------------------------------