├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── TODO.md ├── config.mk ├── src ├── wtplan-web │ ├── calendar_item.js │ ├── component.js │ ├── day_list.js │ ├── day_list_day.js │ ├── day_list_item.js │ ├── dev_build_restart.sh │ ├── edit_item_view.js │ ├── flow_view.js │ ├── include_text │ │ └── includetext.go │ ├── index.html │ ├── lib │ │ ├── ZEPTO_LICENCE │ │ └── zepto.js │ ├── login_view.js │ ├── main.go │ ├── main_page.js │ ├── remote_commands.js │ └── utility_functions.js └── wtplan │ ├── generate_version_str │ └── generate_version_str.go │ └── main.go └── wtplan.1 /.gitignore: -------------------------------------------------------------------------------- 1 | .#* 2 | *~ 3 | /wtplan 4 | /wtplan-web 5 | /bin 6 | src/wtplan-web/textfiles.go 7 | src/wtplan-web/wtplan-web 8 | src/wtplan/wtplan 9 | src/wtplan/version.go -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Kjell Winblad (kjellwinblad@gmail.com, http://winsh.me) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # wtplan 2 | # See LICENSE file for copyright and license details. 3 | 4 | include config.mk 5 | 6 | 7 | export GOPATH=$(shell pwd) 8 | export VERSIONSTR=${VERSION} 9 | 10 | all: wtplan wtplan-web 11 | 12 | wtplan: src/wtplan/main.go 13 | ${GO} generate ./src/wtplan/main.go 14 | ${GO} build ./src/wtplan/main.go ./src/wtplan/version.go 15 | @mv main wtplan 16 | 17 | wtplan-web: $(wildcard src/wtplan-web/*.go src/wtplan-web/*.js) 18 | ${GO} generate ./src/wtplan-web/main.go 19 | ${GO} build ./src/wtplan-web/main.go ./src/wtplan-web/textfiles.go 20 | @mv main wtplan-web 21 | 22 | clean: 23 | @echo cleaning 24 | @rm -f wtplan 25 | @rm -f wtplan-web 26 | 27 | dist: clean 28 | @echo creating dist tarball 29 | @mkdir -p wtplan-${VERSION} 30 | @cp -R src LICENSE Makefile config.mk README.md wtplan.1 wtplan-${VERSION} 31 | @tar -cf wtplan-${VERSION}.tar wtplan-${VERSION} 32 | @gzip wtplan-${VERSION}.tar 33 | @rm -rf wtplan-${VERSION} 34 | 35 | install: all 36 | @echo installing executable files wtplan and wtplan-web to ${DESTDIR}${PREFIX}/bin 37 | @mkdir -p ${DESTDIR}${PREFIX}/bin 38 | @cp -f wtplan ${DESTDIR}${PREFIX}/bin 39 | @chmod 755 ${DESTDIR}${PREFIX}/bin/wtplan 40 | @cp -f wtplan-web ${DESTDIR}${PREFIX}/bin 41 | @chmod 755 ${DESTDIR}${PREFIX}/bin/wtplan-web 42 | @echo installing manual page to ${DESTDIR}${MANPREFIX}/man1 43 | @mkdir -p ${DESTDIR}${MANPREFIX}/man1 44 | @sed "s/VERSION/${VERSION}/g" < wtplan.1 > ${DESTDIR}${MANPREFIX}/man1/wtplan.1 45 | @chmod 644 ${DESTDIR}${MANPREFIX}/man1/wtplan.1 46 | 47 | uninstall: 48 | @echo removing executable files from ${DESTDIR}${PREFIX}/bin 49 | @rm -f ${DESTDIR}${PREFIX}/bin/wtplan 50 | @rm -f ${DESTDIR}${PREFIX}/bin/wtplan-web 51 | @echo removing manual page from ${DESTDIR}${MANPREFIX}/man1 52 | @rm -f ${DESTDIR}${MANPREFIX}/man1/wtplan.1 53 | 54 | .PHONY: all clean dist install uninstall 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | wtplan - web terminal planner 2 | ============================= 3 | 4 | wtplan helps you manage a calendar. The calendar can be managed from a 5 | command line interface and/or a web interface. The calendar data is 6 | stored in a simple text file (located in `~/.wtplan` by 7 | default). wtplan integrates with git to make backup and 8 | synchronization between multiple computers convenient. 9 | 10 | 11 | See the man page ([wtplan.1](http://wtplan.winsh.me/man_page.html)) for more details 12 | 13 | * [Website](http://wtplan.winsh.me/) 14 | * [Screenshots](http://wtplan.winsh.me/screenshots.html) 15 | 16 | Features 17 | -------- 18 | 19 | * command line interface 20 | * web interface 21 | * optional password authentication 22 | * git integration 23 | * simple json based data format 24 | 25 | Requirements 26 | ------------ 27 | 28 | * golang is required to compile wtplan. 29 | * (optional) git is required if you want to use wtplan's git integration. 30 | * (optional) make makes building and installing more convenient but is not 31 | required. See the Makefile for how to build without make. 32 | 33 | Install 34 | ------- 35 | 36 | 1. Edit the paths in config.mk to fit your system if needed. (wtplan is 37 | installed into `/usr/local/{bin,share/man/man1}` by default.) 38 | 2. Run `make install` as root. (This will build and install wtplan) 39 | 40 | License 41 | ------- 42 | 43 | The MIT/X Consortium License. See the LICENSE file for details. -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * Make the web interface usable even when there is no connection to 2 | the server. This can be done by adding a command queue to the web 3 | interface that can be flushed when the server is accessible again. 4 | 5 | * Make it possible to read calendar data from multiple config 6 | folders. This can be useful if one wants to share a kid's calendar 7 | with the other parent. This could be implemented by letting one 8 | specify additional calendar data files when starting the web 9 | interface. The calendar data from the additional calendar data files 10 | would then be viewable in a read-only way in the web interface. 11 | 12 | * Support for importing calendar data from an iCal formatted calendar 13 | data file. -------------------------------------------------------------------------------- /config.mk: -------------------------------------------------------------------------------- 1 | # wtplan version 2 | VERSION = 0.1 3 | 4 | # Customize below to fit your system 5 | 6 | # paths 7 | PREFIX = /usr/local 8 | MANPREFIX = ${PREFIX}/share/man 9 | 10 | # Compiler and build tool 11 | GO = go 12 | -------------------------------------------------------------------------------- /src/wtplan-web/calendar_item.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Kjell Winblad (kjellwinblad@gmail.com, http://winsh.me) 2 | // License: MIT License, see the LICENSE file 3 | 4 | var WTPLAN = WTPLAN || {}; 5 | 6 | (function() { 7 | 8 | //Constructor 9 | WTPLAN.CalendarItem = function(calendarItemData, id) { 10 | this.date = calendarItemData.date; 11 | this.duration = calendarItemData.duration; 12 | this.description = calendarItemData.description; 13 | this.id = id; 14 | }; 15 | 16 | WTPLAN.CalendarItem.prototype.startDate = function() { 17 | return new Date(new Date(this.date).valueOf() + 1); 18 | // (One ms is added to avoid being bwtween days) 19 | }; 20 | 21 | WTPLAN.CalendarItem.prototype.endDate = function() { 22 | var result = WTPLAN.durationRegExp.exec(this.duration); 23 | if (result[1] === "NA") { 24 | return this.startDate(); 25 | } 26 | var hours = 0; 27 | var minutes = 0; 28 | if (result[2] != undefined) { 29 | hours = parseInt(result[2], 10); 30 | } 31 | if (result[3] != undefined) { 32 | minutes = parseInt(result[3], 10); 33 | } 34 | var endDate = new Date(this.startDate()); 35 | endDate.setHours(endDate.getHours() + hours); 36 | endDate.setMinutes(endDate.getMinutes() + minutes); 37 | return endDate; 38 | }; 39 | 40 | WTPLAN.CalendarItem.prototype.toPlainObject = function() { 41 | return { 42 | date: this.date, 43 | duration: this.duration, 44 | description: this.description 45 | }; 46 | }; 47 | 48 | WTPLAN.CalendarItem.prototype.toString = function() { 49 | return this.date; 50 | }; 51 | 52 | })(); 53 | -------------------------------------------------------------------------------- /src/wtplan-web/component.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Kjell Winblad (kjellwinblad@gmail.com, http://winsh.me) 2 | // License: MIT License, see the LICENSE file 3 | 4 | var WTPLAN = WTPLAN || {}; 5 | 6 | (function() { 7 | var componentId = 0; 8 | 9 | WTPLAN.Component = function(props) { 10 | if (props === undefined) { 11 | this.state = {}; 12 | } else { 13 | this.state = props; 14 | } 15 | this.componentId = componentId; 16 | componentId = componentId + 1; 17 | } 18 | 19 | WTPLAN.Component.prototype.renderAt = function(element) { 20 | var newElement = $(this.render()); 21 | newElement.attr("data-wtplanid", "" + this.componentId); 22 | element.replaceWith(newElement); 23 | var outerThis = this; 24 | outerThis.componentDidMount(newElement); 25 | }; 26 | 27 | WTPLAN.Component.prototype.setState = function(newState) { 28 | this.state = newState; 29 | var element = this.getRenderedComponent(); 30 | var renderedHtml = this.render(); 31 | var newElement = $(renderedHtml); 32 | newElement.attr("data-wtplanid", "" + this.componentId); 33 | element.replaceWith(newElement); 34 | var outerThis = this; 35 | outerThis.componentDidMount(newElement); 36 | }; 37 | 38 | WTPLAN.Component.prototype.render = function() { 39 | console.log("WARNING: render has not been implemented for component"); 40 | } 41 | 42 | WTPLAN.Component.prototype.componentDidMount = function(element) { 43 | 44 | } 45 | 46 | WTPLAN.Component.prototype.getRenderedComponent = function() { 47 | return $('[data-wtplanid="' + this.componentId + '"]'); 48 | } 49 | 50 | // Object.create() polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create 51 | 52 | if (typeof Object.create != 'function') { 53 | Object.create = (function(undefined) { 54 | var Temp = function() {}; 55 | return function(prototype, propertiesObject) { 56 | if (prototype !== Object(prototype) && prototype !== null) { 57 | throw TypeError('Argument must be an object, or null'); 58 | } 59 | Temp.prototype = prototype || {}; 60 | var result = new Temp(); 61 | Temp.prototype = null; 62 | if (propertiesObject !== undefined) { 63 | Object.defineProperties(result, propertiesObject); 64 | } 65 | 66 | // to imitate the case of Object.create(null) 67 | if (prototype === null) { 68 | result.__proto__ = null; 69 | } 70 | return result; 71 | }; 72 | })(); 73 | } 74 | 75 | })(); 76 | -------------------------------------------------------------------------------- /src/wtplan-web/day_list.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Kjell Winblad (kjellwinblad@gmail.com, http://winsh.me) 2 | // License: MIT License, see the LICENSE file 3 | 4 | var WTPLAN = WTPLAN || {}; 5 | 6 | (function() { 7 | 8 | //Constructor 9 | WTPLAN.DayList = function(params) { 10 | WTPLAN.Component.call(this); 11 | this.state = params; 12 | } 13 | //Extending Component 14 | WTPLAN.DayList.prototype = Object.create(WTPLAN.Component.prototype); 15 | WTPLAN.DayList.prototype.constructor = WTPLAN.DayList; 16 | 17 | //Methods 18 | WTPLAN.DayList.prototype.render = function() { 19 | var dayDivs = ""; 20 | var i = 0; 21 | for (var i = 0; i <= this.state.numberOfDaysAfterToday; i++) { 22 | dayDivs = dayDivs + '
'; 23 | } 24 | return '
' + dayDivs + '
'; 25 | }; 26 | 27 | WTPLAN.DayList.prototype.componentDidMount = function(component) { 28 | var currentDay = WTPLAN.getLocalStartOfDay(this.state.currentDay); 29 | var i = 0; 30 | for (; i <= this.state.numberOfDaysAfterToday; i++) { 31 | new WTPLAN.DayListDay({ 32 | 'currentDay': new Date(currentDay.getTime()), 33 | 'calendarItems': this.state.calendarItems, 34 | 'openAddDialog': this.state.openAddDialog, 35 | 'openEditDialog': this.state.openEditDialog, 36 | 'removeItemAction': this.state.removeItemAction 37 | }).renderAt($('#day' + i)); 38 | currentDay.setDate(currentDay.getDate() + 1);; 39 | } 40 | }; 41 | 42 | })() 43 | -------------------------------------------------------------------------------- /src/wtplan-web/day_list_day.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Kjell Winblad (kjellwinblad@gmail.com, http://winsh.me) 2 | // License: MIT License, see the LICENSE file 3 | 4 | var WTPLAN = WTPLAN || {}; 5 | 6 | (function() { 7 | 8 | //Constructor 9 | WTPLAN.DayListDay = function(state) { 10 | WTPLAN.Component.call(this); 11 | this.state = state; 12 | } 13 | //Extending Component 14 | WTPLAN.DayListDay.prototype = Object.create(WTPLAN.Component.prototype); 15 | WTPLAN.DayListDay.prototype.constructor = WTPLAN.DayListDay; 16 | 17 | //Methods 18 | WTPLAN.DayListDay.prototype.render = function() { 19 | var currentDay = this.state.currentDay; 20 | var nextDay = new Date(currentDay.getTime()); 21 | nextDay.setDate(currentDay.getDate() + 1); 22 | var itemsToday = this.state.calendarItems.filter(function(item) { 23 | var itemStartDate = item.startDate(); 24 | var itemEndDate = item.endDate(); 25 | return (itemStartDate.getTime() >= currentDay.getTime() && 26 | itemStartDate < nextDay.getTime()) || 27 | (itemEndDate.getTime() >= currentDay.getTime() && 28 | itemEndDate < nextDay.getTime()) || 29 | (itemStartDate.getTime() < currentDay.getTime() && 30 | itemEndDate >= nextDay.getTime()); 31 | }); 32 | this.tmpItemItemDivIdList = itemsToday.map(function(item, index) { 33 | return { 34 | 'item': item, 35 | itemId: "item" + index 36 | }; 37 | }); 38 | var itemDivs = this.tmpItemItemDivIdList.reduce(function(acc, curr) { 39 | return acc + '
'; 40 | }, ""); 41 | var dayHeader = WTPLAN.template( 42 | '
\ 43 | <%localDateString%> <%localDayString%>\ 44 |
\ 45 |
', 46 | {localDateString: WTPLAN.getLocalDayString(currentDay), 47 | localDayString: WTPLAN.dayStringFromDate(currentDay), 48 | componentId: this.componentId}); 49 | return WTPLAN.template( 50 | '
\ 51 | <%dayHeader%>\ 52 |
<%itemDivs%>
\ 53 |
', 54 | {dayHeader: dayHeader, 55 | itemDivs: itemDivs}); 56 | }; 57 | 58 | WTPLAN.DayListDay.prototype.componentDidMount = function(component) { 59 | var outerThis = this; 60 | var currentDay = this.state.currentDay; 61 | var nextDay = new Date(currentDay.getTime()); 62 | nextDay.setDate(currentDay.getDate() + 1); 63 | this.tmpItemItemDivIdList.forEach(function(tuple) { 64 | new WTPLAN.DayListItem({ 65 | calendarItem: tuple.item, 66 | 'currentDay': currentDay, 67 | 'nextDay': nextDay, 68 | 'openEditDialog': outerThis.state.openEditDialog, 69 | 'removeItemAction': outerThis.state.removeItemAction 70 | }).renderAt(component.find('[data-itemId="' + tuple.itemId +'"]')); 71 | }); 72 | $('#addButton' + outerThis.componentId).click(function() { 73 | outerThis.state.openAddDialog(WTPLAN.dateToRfc3339String( 74 | outerThis.state.currentDay)); 75 | }); 76 | }; 77 | 78 | })() 79 | -------------------------------------------------------------------------------- /src/wtplan-web/day_list_item.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Kjell Winblad (kjellwinblad@gmail.com, http://winsh.me) 2 | // License: MIT License, see the LICENSE file 3 | 4 | var WTPLAN = WTPLAN || {}; 5 | 6 | (function() { 7 | 8 | var dayListItemIdCounter = 0; 9 | 10 | //Constructor 11 | WTPLAN.DayListItem = function(state) { 12 | WTPLAN.Component.call(this); 13 | this.dayListItemId = dayListItemIdCounter; 14 | dayListItemIdCounter = dayListItemIdCounter + 1; 15 | this.state = state; 16 | } 17 | //Extending Component 18 | WTPLAN.DayListItem.prototype = Object.create(WTPLAN.Component.prototype); 19 | WTPLAN.DayListItem.prototype.constructor = WTPLAN.DayListItem; 20 | 21 | //Methods 22 | WTPLAN.DayListItem.prototype.render = function() { 23 | var currentDayTime = this.state.currentDay.getTime(); 24 | var nextDayTime = this.state.nextDay.getTime(); 25 | var startDate = this.state.calendarItem.startDate(); 26 | var startDateTime = startDate.getTime(); 27 | var endDate = this.state.calendarItem.endDate(); 28 | var endDateTime = endDate.getTime(); 29 | var hoursStart = startDate.getHours(); 30 | var minutesStart = startDate.getMinutes(); 31 | var hoursEnd = endDate.getHours(); 32 | var minutesEnd = endDate.getMinutes(); 33 | 34 | function pad(i) { 35 | var str = "" + i; 36 | return ('00' + str).substring(str.length); 37 | } 38 | var timeString = undefined; 39 | if ((startDateTime >= currentDayTime && startDateTime < nextDayTime) && 40 | (endDateTime >= currentDayTime && endDateTime < nextDayTime)) { 41 | //Both end and start date are inside day 42 | timeString = pad(hoursStart) + ":" + pad(minutesStart); 43 | if (startDate.getTime() != endDate.getTime()) { 44 | timeString = timeString + "-" + pad(hoursEnd) + ":" + pad(minutesEnd); 45 | } 46 | } else if (startDateTime >= currentDayTime && startDateTime < nextDayTime) { 47 | // Start date in day 48 | timeString = pad(hoursStart) + ":" + pad(minutesStart) + "-(->)"; 49 | } else if (endDateTime >= currentDayTime && endDateTime < nextDayTime) { 50 | // End date in day 51 | timeString = "(<-)-" + pad(hoursEnd) + ":" + pad(minutesEnd); 52 | } else if (startDateTime < currentDayTime && endDateTime >= nextDayTime) { 53 | //Start date before day and end date after day 54 | timeString = "(<-)-(->)"; 55 | } 56 | return WTPLAN.template( 57 | '
\ 58 |
\ 59 |
<%timeString%>
\ 60 | \ 61 |
\ 62 |
\ 63 | <%description%>\ 64 |
', 65 | {timeString: timeString, 66 | dayListItemId: this.dayListItemId, 67 | description: this.state.calendarItem.description}); 68 | 69 | }; 70 | 71 | WTPLAN.DayListItem.prototype.componentDidMount = function(component) { 72 | var outerThis = this; 73 | $('#editButton' +outerThis.dayListItemId).click(function() { 74 | outerThis.state.openEditDialog(outerThis.state.calendarItem); 75 | }); 76 | $('#removeButton' + outerThis.dayListItemId).click(function() { 77 | outerThis.state.removeItemAction(outerThis.state.calendarItem.id); 78 | }); 79 | }; 80 | 81 | })(); 82 | -------------------------------------------------------------------------------- /src/wtplan-web/dev_build_restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sigint_handler() 4 | { 5 | kill $PID 6 | exit 7 | } 8 | 9 | trap sigint_handler SIGINT 10 | 11 | while true; do 12 | go generate 13 | go build 14 | ./wtplan-web & 15 | PID=$! 16 | inotifywait -e modify -e move -e create -e delete -e attrib -r `pwd` 17 | kill $PID 18 | done 19 | -------------------------------------------------------------------------------- /src/wtplan-web/edit_item_view.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Kjell Winblad (kjellwinblad@gmail.com, http://winsh.me) 2 | // License: MIT License, see the LICENSE file 3 | 4 | var WTPLAN = WTPLAN || {}; 5 | 6 | (function() { 7 | 8 | function validateDurationFiled() { 9 | var str = $('#durationField').val(); 10 | var result = WTPLAN.durationRegExp.exec(str); 11 | return result != null && str == result[0]; 12 | } 13 | 14 | function validateDateFiled() { 15 | var str = $('#dateField').val(); 16 | var result = WTPLAN.rfc3339regexp.exec(str); 17 | return result != null && str == result[0]; 18 | } 19 | 20 | function validateDescriptionFiled() { 21 | var str = $('#descriptionField').val(); 22 | return str != ""; 23 | } 24 | 25 | //Constructor 26 | WTPLAN.EditItemView = function(calendarItem, saveCallback, cancelCallback) { 27 | WTPLAN.Component.call(this); 28 | this.state.calendarItem = calendarItem; 29 | this.saveCallback = saveCallback; 30 | this.cancelCallback = cancelCallback; 31 | } 32 | //Extending Component 33 | WTPLAN.EditItemView.prototype = Object.create(WTPLAN.Component.prototype); 34 | WTPLAN.EditItemView.prototype.constructor = WTPLAN.EditItemView; 35 | 36 | //Methods 37 | WTPLAN.EditItemView.prototype.render = function() { 38 | return WTPLAN.template( 39 | '\
\ 40 | \ 41 | \ 42 |
\ 43 | Date (RFC3339):\ 44 | \ 45 |
\ 46 | Duration (e.g. NA, 2h, 2h20m):\ 47 | \ 48 |
\ 49 | Description:\ 50 | \ 51 |
', 52 | {date: this.state.calendarItem.date, 53 | duration: this.state.calendarItem.duration, 54 | description: this.state.calendarItem.description}); 55 | 56 | }; 57 | 58 | WTPLAN.EditItemView.prototype.componentDidMount = function(component) { 59 | var outerThis = this; 60 | $("#saveButton").click(function() { 61 | if (!validateDateFiled()) { 62 | alert("The date filed contains an invalid date."); 63 | return; 64 | } 65 | if (!validateDurationFiled()) { 66 | alert("The duration filed contains an invalid date."); 67 | return; 68 | } 69 | if (!validateDescriptionFiled()) { 70 | alert("The description filed is empty."); 71 | return; 72 | } 73 | outerThis.saveCallback(new WTPLAN.CalendarItem({ 74 | date: $("#dateField").val(), 75 | duration: $("#durationField").val(), 76 | description: $("#descriptionField").val() 77 | })); 78 | }); 79 | $("#cancelButton").click(function() { 80 | outerThis.cancelCallback(); 81 | }); 82 | $("#dateField").on('change textInput input', function() { 83 | if (!validateDateFiled()) { 84 | $(this).css({ 85 | backgroundColor: 'red' 86 | }); 87 | } else { 88 | $(this).css({ 89 | backgroundColor: '' 90 | }); 91 | } 92 | }); 93 | $("#durationField").on('change textInput input', function() { 94 | if (!validateDurationFiled()) { 95 | $(this).css({ 96 | backgroundColor: 'red' 97 | }); 98 | } else { 99 | $(this).css({ 100 | backgroundColor: '' 101 | }); 102 | } 103 | }); 104 | }; 105 | 106 | })() 107 | -------------------------------------------------------------------------------- /src/wtplan-web/flow_view.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Kjell Winblad (kjellwinblad@gmail.com, http://winsh.me) 2 | // License: MIT License, see the LICENSE file 3 | 4 | var WTPLAN = WTPLAN || {}; 5 | 6 | (function() { 7 | 8 | //Constructor 9 | WTPLAN.FlowView = function(openAddDialog, openEditDialog, removeItemAction) { 10 | WTPLAN.Component.call(this); 11 | this.state = { 12 | currentDay: WTPLAN.getLocalDayString(), 13 | numberOfDaysAfterToday: 9, 14 | calendarItems: [], 15 | 'openAddDialog': openAddDialog, 16 | 'openEditDialog': openEditDialog, 17 | 'removeItemAction': removeItemAction 18 | } 19 | 20 | this._dayList = new WTPLAN.DayList(this.state); 21 | } 22 | 23 | //Extending Component 24 | WTPLAN.FlowView.prototype = Object.create(WTPLAN.Component.prototype); 25 | WTPLAN.FlowView.prototype.constructor = WTPLAN.FlowView; 26 | 27 | //Mathods 28 | WTPLAN.FlowView.prototype.render = function() { 29 | return WTPLAN.template( 30 | '
\ 31 | \ 32 | and the next day(s):\ 33 |
\ 34 |
\ 35 |
\ 36 |
', 37 | {currentDay: this.state.currentDay, 38 | numberOfDaysAfterToday: this.state.numberOfDaysAfterToday} 39 | ); 40 | }; 41 | 42 | WTPLAN.FlowView.prototype.componentDidMount = function(component) { 43 | var outerThis = this; 44 | $(component).find("#dateInput").on('change textInput input', function() { 45 | var date = new Date($(this).val()); 46 | if (isNaN(date.getTime())) { 47 | $(this).css({ 48 | backgroundColor: 'red' 49 | }); 50 | } else { 51 | $(this).css({ 52 | backgroundColor: '' 53 | }); 54 | var newCurrentDay = WTPLAN.getLocalDayString(date); 55 | if (newCurrentDay != outerThis.state.currentDay) { 56 | outerThis.state.currentDay = newCurrentDay; 57 | outerThis._dayList.setState(outerThis.state); 58 | } 59 | } 60 | 61 | }); 62 | $(component).find("#daysInput").on('change textInput input', function() { 63 | var days = parseInt($(this).val(), 10); 64 | if (isNaN(days)) { 65 | $(this).css({ 66 | backgroundColor: 'red' 67 | }); 68 | } else { 69 | $(this).css({ 70 | backgroundColor: '' 71 | }); 72 | if (days != outerThis.state.numberOfDaysAfterToday) { 73 | outerThis.state.numberOfDaysAfterToday = days; 74 | outerThis._dayList.setState(outerThis.state); 75 | } 76 | } 77 | 78 | }); 79 | this._dayList.renderAt($('#dayListDiv')); 80 | }; 81 | 82 | })() 83 | -------------------------------------------------------------------------------- /src/wtplan-web/include_text/includetext.go: -------------------------------------------------------------------------------- 1 | //Copyright 2017 Kjell Winblad (kjellwinblad@gmail.com, http://winsh.me) 2 | //License: MIT License, see the LICENSE file 3 | 4 | // Code created with the help of Stack Overflow question 5 | // http://stackoverflow.com/questions/17796043/golang-embedding-text-file-into-compiled-executable 6 | // Question by Zvika: 7 | // http://stackoverflow.com/users/1543290/zvika 8 | // Answer by Johan Wikström: 9 | // http://stackoverflow.com/users/702065/johan-wikstr%c3%b6m 10 | 11 | 12 | package main 13 | 14 | import ( 15 | "bytes" 16 | "fmt" 17 | "io" 18 | "io/ioutil" 19 | "os" 20 | "path/filepath" 21 | "strings" 22 | ) 23 | 24 | // Reads all .html and .js files in the current folder 25 | // and encodes them as strings literals in textfiles.go 26 | func main() { 27 | fs1, _ := ioutil.ReadDir(".") 28 | fs2, _ := ioutil.ReadDir("./lib") 29 | var fs []string 30 | for i := range fs1 { 31 | fs = append(fs, fs1[i].Name()) 32 | } 33 | for i := range fs2 { 34 | fs = append(fs, filepath.Join("./lib", fs2[i].Name())) 35 | } 36 | out, _ := os.Create("textfiles.go") 37 | out.Write([]byte("package main \n\nconst (\n")) 38 | for _, f := range fs { 39 | if strings.HasSuffix(f, ".html") || strings.HasSuffix(f, ".js") { 40 | stringsToReplace := []string{"/", "_", "-", "."} 41 | fieldName := f 42 | for i := range stringsToReplace { 43 | fieldName = strings.Replace(fieldName, stringsToReplace[i], "D", -1) 44 | } 45 | out.Write([]byte(fieldName + " = `")) 46 | fileContent, err := ioutil.ReadFile(f) 47 | _, err = io.Copy(out, bytes.NewBufferString(strings.Replace(string(fileContent), "`", "`+\"`\"+`", -1))) 48 | if err != nil { 49 | fmt.Println(err) 50 | } 51 | out.Write([]byte("`\n")) 52 | } 53 | } 54 | out.Write([]byte(")\n")) 55 | } 56 | -------------------------------------------------------------------------------- /src/wtplan-web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 |
Loading...
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/wtplan-web/lib/ZEPTO_LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2017 Thomas Fuchs 2 | http://zeptojs.com/ 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/wtplan-web/lib/zepto.js: -------------------------------------------------------------------------------- 1 | /* Zepto v1.2.0 - zepto event ajax form ie - zeptojs.com/license */ 2 | (function(global, factory) { 3 | if (typeof define === 'function' && define.amd) 4 | define(function() { return factory(global) }) 5 | else 6 | factory(global) 7 | }(this, function(window) { 8 | var Zepto = (function() { 9 | var undefined, key, $, classList, emptyArray = [], concat = emptyArray.concat, filter = emptyArray.filter, slice = emptyArray.slice, 10 | document = window.document, 11 | elementDisplay = {}, classCache = {}, 12 | cssNumber = { 'column-count': 1, 'columns': 1, 'font-weight': 1, 'line-height': 1,'opacity': 1, 'z-index': 1, 'zoom': 1 }, 13 | fragmentRE = /^\s*<(\w+|!)[^>]*>/, 14 | singleTagRE = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, 15 | tagExpanderRE = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, 16 | rootNodeRE = /^(?:body|html)$/i, 17 | capitalRE = /([A-Z])/g, 18 | 19 | // special attributes that should be get/set via method calls 20 | methodAttributes = ['val', 'css', 'html', 'text', 'data', 'width', 'height', 'offset'], 21 | 22 | adjacencyOperators = [ 'after', 'prepend', 'before', 'append' ], 23 | table = document.createElement('table'), 24 | tableRow = document.createElement('tr'), 25 | containers = { 26 | 'tr': document.createElement('tbody'), 27 | 'tbody': table, 'thead': table, 'tfoot': table, 28 | 'td': tableRow, 'th': tableRow, 29 | '*': document.createElement('div') 30 | }, 31 | readyRE = /complete|loaded|interactive/, 32 | simpleSelectorRE = /^[\w-]*$/, 33 | class2type = {}, 34 | toString = class2type.toString, 35 | zepto = {}, 36 | camelize, uniq, 37 | tempParent = document.createElement('div'), 38 | propMap = { 39 | 'tabindex': 'tabIndex', 40 | 'readonly': 'readOnly', 41 | 'for': 'htmlFor', 42 | 'class': 'className', 43 | 'maxlength': 'maxLength', 44 | 'cellspacing': 'cellSpacing', 45 | 'cellpadding': 'cellPadding', 46 | 'rowspan': 'rowSpan', 47 | 'colspan': 'colSpan', 48 | 'usemap': 'useMap', 49 | 'frameborder': 'frameBorder', 50 | 'contenteditable': 'contentEditable' 51 | }, 52 | isArray = Array.isArray || 53 | function(object){ return object instanceof Array } 54 | 55 | zepto.matches = function(element, selector) { 56 | if (!selector || !element || element.nodeType !== 1) return false 57 | var matchesSelector = element.matches || element.webkitMatchesSelector || 58 | element.mozMatchesSelector || element.oMatchesSelector || 59 | element.matchesSelector 60 | if (matchesSelector) return matchesSelector.call(element, selector) 61 | // fall back to performing a selector: 62 | var match, parent = element.parentNode, temp = !parent 63 | if (temp) (parent = tempParent).appendChild(element) 64 | match = ~zepto.qsa(parent, selector).indexOf(element) 65 | temp && tempParent.removeChild(element) 66 | return match 67 | } 68 | 69 | function type(obj) { 70 | return obj == null ? String(obj) : 71 | class2type[toString.call(obj)] || "object" 72 | } 73 | 74 | function isFunction(value) { return type(value) == "function" } 75 | function isWindow(obj) { return obj != null && obj == obj.window } 76 | function isDocument(obj) { return obj != null && obj.nodeType == obj.DOCUMENT_NODE } 77 | function isObject(obj) { return type(obj) == "object" } 78 | function isPlainObject(obj) { 79 | return isObject(obj) && !isWindow(obj) && Object.getPrototypeOf(obj) == Object.prototype 80 | } 81 | 82 | function likeArray(obj) { 83 | var length = !!obj && 'length' in obj && obj.length, 84 | type = $.type(obj) 85 | 86 | return 'function' != type && !isWindow(obj) && ( 87 | 'array' == type || length === 0 || 88 | (typeof length == 'number' && length > 0 && (length - 1) in obj) 89 | ) 90 | } 91 | 92 | function compact(array) { return filter.call(array, function(item){ return item != null }) } 93 | function flatten(array) { return array.length > 0 ? $.fn.concat.apply([], array) : array } 94 | camelize = function(str){ return str.replace(/-+(.)?/g, function(match, chr){ return chr ? chr.toUpperCase() : '' }) } 95 | function dasherize(str) { 96 | return str.replace(/::/g, '/') 97 | .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') 98 | .replace(/([a-z\d])([A-Z])/g, '$1_$2') 99 | .replace(/_/g, '-') 100 | .toLowerCase() 101 | } 102 | uniq = function(array){ return filter.call(array, function(item, idx){ return array.indexOf(item) == idx }) } 103 | 104 | function classRE(name) { 105 | return name in classCache ? 106 | classCache[name] : (classCache[name] = new RegExp('(^|\\s)' + name + '(\\s|$)')) 107 | } 108 | 109 | function maybeAddPx(name, value) { 110 | return (typeof value == "number" && !cssNumber[dasherize(name)]) ? value + "px" : value 111 | } 112 | 113 | function defaultDisplay(nodeName) { 114 | var element, display 115 | if (!elementDisplay[nodeName]) { 116 | element = document.createElement(nodeName) 117 | document.body.appendChild(element) 118 | display = getComputedStyle(element, '').getPropertyValue("display") 119 | element.parentNode.removeChild(element) 120 | display == "none" && (display = "block") 121 | elementDisplay[nodeName] = display 122 | } 123 | return elementDisplay[nodeName] 124 | } 125 | 126 | function children(element) { 127 | return 'children' in element ? 128 | slice.call(element.children) : 129 | $.map(element.childNodes, function(node){ if (node.nodeType == 1) return node }) 130 | } 131 | 132 | function Z(dom, selector) { 133 | var i, len = dom ? dom.length : 0 134 | for (i = 0; i < len; i++) this[i] = dom[i] 135 | this.length = len 136 | this.selector = selector || '' 137 | } 138 | 139 | // `$.zepto.fragment` takes a html string and an optional tag name 140 | // to generate DOM nodes from the given html string. 141 | // The generated DOM nodes are returned as an array. 142 | // This function can be overridden in plugins for example to make 143 | // it compatible with browsers that don't support the DOM fully. 144 | zepto.fragment = function(html, name, properties) { 145 | var dom, nodes, container 146 | 147 | // A special case optimization for a single tag 148 | if (singleTagRE.test(html)) dom = $(document.createElement(RegExp.$1)) 149 | 150 | if (!dom) { 151 | if (html.replace) html = html.replace(tagExpanderRE, "<$1>") 152 | if (name === undefined) name = fragmentRE.test(html) && RegExp.$1 153 | if (!(name in containers)) name = '*' 154 | 155 | container = containers[name] 156 | container.innerHTML = '' + html 157 | dom = $.each(slice.call(container.childNodes), function(){ 158 | container.removeChild(this) 159 | }) 160 | } 161 | 162 | if (isPlainObject(properties)) { 163 | nodes = $(dom) 164 | $.each(properties, function(key, value) { 165 | if (methodAttributes.indexOf(key) > -1) nodes[key](value) 166 | else nodes.attr(key, value) 167 | }) 168 | } 169 | 170 | return dom 171 | } 172 | 173 | // `$.zepto.Z` swaps out the prototype of the given `dom` array 174 | // of nodes with `$.fn` and thus supplying all the Zepto functions 175 | // to the array. This method can be overridden in plugins. 176 | zepto.Z = function(dom, selector) { 177 | return new Z(dom, selector) 178 | } 179 | 180 | // `$.zepto.isZ` should return `true` if the given object is a Zepto 181 | // collection. This method can be overridden in plugins. 182 | zepto.isZ = function(object) { 183 | return object instanceof zepto.Z 184 | } 185 | 186 | // `$.zepto.init` is Zepto's counterpart to jQuery's `$.fn.init` and 187 | // takes a CSS selector and an optional context (and handles various 188 | // special cases). 189 | // This method can be overridden in plugins. 190 | zepto.init = function(selector, context) { 191 | var dom 192 | // If nothing given, return an empty Zepto collection 193 | if (!selector) return zepto.Z() 194 | // Optimize for string selectors 195 | else if (typeof selector == 'string') { 196 | selector = selector.trim() 197 | // If it's a html fragment, create nodes from it 198 | // Note: In both Chrome 21 and Firefox 15, DOM error 12 199 | // is thrown if the fragment doesn't begin with < 200 | if (selector[0] == '<' && fragmentRE.test(selector)) 201 | dom = zepto.fragment(selector, RegExp.$1, context), selector = null 202 | // If there's a context, create a collection on that context first, and select 203 | // nodes from there 204 | else if (context !== undefined) return $(context).find(selector) 205 | // If it's a CSS selector, use it to select nodes. 206 | else dom = zepto.qsa(document, selector) 207 | } 208 | // If a function is given, call it when the DOM is ready 209 | else if (isFunction(selector)) return $(document).ready(selector) 210 | // If a Zepto collection is given, just return it 211 | else if (zepto.isZ(selector)) return selector 212 | else { 213 | // normalize array if an array of nodes is given 214 | if (isArray(selector)) dom = compact(selector) 215 | // Wrap DOM nodes. 216 | else if (isObject(selector)) 217 | dom = [selector], selector = null 218 | // If it's a html fragment, create nodes from it 219 | else if (fragmentRE.test(selector)) 220 | dom = zepto.fragment(selector.trim(), RegExp.$1, context), selector = null 221 | // If there's a context, create a collection on that context first, and select 222 | // nodes from there 223 | else if (context !== undefined) return $(context).find(selector) 224 | // And last but no least, if it's a CSS selector, use it to select nodes. 225 | else dom = zepto.qsa(document, selector) 226 | } 227 | // create a new Zepto collection from the nodes found 228 | return zepto.Z(dom, selector) 229 | } 230 | 231 | // `$` will be the base `Zepto` object. When calling this 232 | // function just call `$.zepto.init, which makes the implementation 233 | // details of selecting nodes and creating Zepto collections 234 | // patchable in plugins. 235 | $ = function(selector, context){ 236 | return zepto.init(selector, context) 237 | } 238 | 239 | function extend(target, source, deep) { 240 | for (key in source) 241 | if (deep && (isPlainObject(source[key]) || isArray(source[key]))) { 242 | if (isPlainObject(source[key]) && !isPlainObject(target[key])) 243 | target[key] = {} 244 | if (isArray(source[key]) && !isArray(target[key])) 245 | target[key] = [] 246 | extend(target[key], source[key], deep) 247 | } 248 | else if (source[key] !== undefined) target[key] = source[key] 249 | } 250 | 251 | // Copy all but undefined properties from one or more 252 | // objects to the `target` object. 253 | $.extend = function(target){ 254 | var deep, args = slice.call(arguments, 1) 255 | if (typeof target == 'boolean') { 256 | deep = target 257 | target = args.shift() 258 | } 259 | args.forEach(function(arg){ extend(target, arg, deep) }) 260 | return target 261 | } 262 | 263 | // `$.zepto.qsa` is Zepto's CSS selector implementation which 264 | // uses `document.querySelectorAll` and optimizes for some special cases, like `#id`. 265 | // This method can be overridden in plugins. 266 | zepto.qsa = function(element, selector){ 267 | var found, 268 | maybeID = selector[0] == '#', 269 | maybeClass = !maybeID && selector[0] == '.', 270 | nameOnly = maybeID || maybeClass ? selector.slice(1) : selector, // Ensure that a 1 char tag name still gets checked 271 | isSimple = simpleSelectorRE.test(nameOnly) 272 | return (element.getElementById && isSimple && maybeID) ? // Safari DocumentFragment doesn't have getElementById 273 | ( (found = element.getElementById(nameOnly)) ? [found] : [] ) : 274 | (element.nodeType !== 1 && element.nodeType !== 9 && element.nodeType !== 11) ? [] : 275 | slice.call( 276 | isSimple && !maybeID && element.getElementsByClassName ? // DocumentFragment doesn't have getElementsByClassName/TagName 277 | maybeClass ? element.getElementsByClassName(nameOnly) : // If it's simple, it could be a class 278 | element.getElementsByTagName(selector) : // Or a tag 279 | element.querySelectorAll(selector) // Or it's not simple, and we need to query all 280 | ) 281 | } 282 | 283 | function filtered(nodes, selector) { 284 | return selector == null ? $(nodes) : $(nodes).filter(selector) 285 | } 286 | 287 | $.contains = document.documentElement.contains ? 288 | function(parent, node) { 289 | return parent !== node && parent.contains(node) 290 | } : 291 | function(parent, node) { 292 | while (node && (node = node.parentNode)) 293 | if (node === parent) return true 294 | return false 295 | } 296 | 297 | function funcArg(context, arg, idx, payload) { 298 | return isFunction(arg) ? arg.call(context, idx, payload) : arg 299 | } 300 | 301 | function setAttribute(node, name, value) { 302 | value == null ? node.removeAttribute(name) : node.setAttribute(name, value) 303 | } 304 | 305 | // access className property while respecting SVGAnimatedString 306 | function className(node, value){ 307 | var klass = node.className || '', 308 | svg = klass && klass.baseVal !== undefined 309 | 310 | if (value === undefined) return svg ? klass.baseVal : klass 311 | svg ? (klass.baseVal = value) : (node.className = value) 312 | } 313 | 314 | // "true" => true 315 | // "false" => false 316 | // "null" => null 317 | // "42" => 42 318 | // "42.5" => 42.5 319 | // "08" => "08" 320 | // JSON => parse if valid 321 | // String => self 322 | function deserializeValue(value) { 323 | try { 324 | return value ? 325 | value == "true" || 326 | ( value == "false" ? false : 327 | value == "null" ? null : 328 | +value + "" == value ? +value : 329 | /^[\[\{]/.test(value) ? $.parseJSON(value) : 330 | value ) 331 | : value 332 | } catch(e) { 333 | return value 334 | } 335 | } 336 | 337 | $.type = type 338 | $.isFunction = isFunction 339 | $.isWindow = isWindow 340 | $.isArray = isArray 341 | $.isPlainObject = isPlainObject 342 | 343 | $.isEmptyObject = function(obj) { 344 | var name 345 | for (name in obj) return false 346 | return true 347 | } 348 | 349 | $.isNumeric = function(val) { 350 | var num = Number(val), type = typeof val 351 | return val != null && type != 'boolean' && 352 | (type != 'string' || val.length) && 353 | !isNaN(num) && isFinite(num) || false 354 | } 355 | 356 | $.inArray = function(elem, array, i){ 357 | return emptyArray.indexOf.call(array, elem, i) 358 | } 359 | 360 | $.camelCase = camelize 361 | $.trim = function(str) { 362 | return str == null ? "" : String.prototype.trim.call(str) 363 | } 364 | 365 | // plugin compatibility 366 | $.uuid = 0 367 | $.support = { } 368 | $.expr = { } 369 | $.noop = function() {} 370 | 371 | $.map = function(elements, callback){ 372 | var value, values = [], i, key 373 | if (likeArray(elements)) 374 | for (i = 0; i < elements.length; i++) { 375 | value = callback(elements[i], i) 376 | if (value != null) values.push(value) 377 | } 378 | else 379 | for (key in elements) { 380 | value = callback(elements[key], key) 381 | if (value != null) values.push(value) 382 | } 383 | return flatten(values) 384 | } 385 | 386 | $.each = function(elements, callback){ 387 | var i, key 388 | if (likeArray(elements)) { 389 | for (i = 0; i < elements.length; i++) 390 | if (callback.call(elements[i], i, elements[i]) === false) return elements 391 | } else { 392 | for (key in elements) 393 | if (callback.call(elements[key], key, elements[key]) === false) return elements 394 | } 395 | 396 | return elements 397 | } 398 | 399 | $.grep = function(elements, callback){ 400 | return filter.call(elements, callback) 401 | } 402 | 403 | if (window.JSON) $.parseJSON = JSON.parse 404 | 405 | // Populate the class2type map 406 | $.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { 407 | class2type[ "[object " + name + "]" ] = name.toLowerCase() 408 | }) 409 | 410 | // Define methods that will be available on all 411 | // Zepto collections 412 | $.fn = { 413 | constructor: zepto.Z, 414 | length: 0, 415 | 416 | // Because a collection acts like an array 417 | // copy over these useful array functions. 418 | forEach: emptyArray.forEach, 419 | reduce: emptyArray.reduce, 420 | push: emptyArray.push, 421 | sort: emptyArray.sort, 422 | splice: emptyArray.splice, 423 | indexOf: emptyArray.indexOf, 424 | concat: function(){ 425 | var i, value, args = [] 426 | for (i = 0; i < arguments.length; i++) { 427 | value = arguments[i] 428 | args[i] = zepto.isZ(value) ? value.toArray() : value 429 | } 430 | return concat.apply(zepto.isZ(this) ? this.toArray() : this, args) 431 | }, 432 | 433 | // `map` and `slice` in the jQuery API work differently 434 | // from their array counterparts 435 | map: function(fn){ 436 | return $($.map(this, function(el, i){ return fn.call(el, i, el) })) 437 | }, 438 | slice: function(){ 439 | return $(slice.apply(this, arguments)) 440 | }, 441 | 442 | ready: function(callback){ 443 | // need to check if document.body exists for IE as that browser reports 444 | // document ready when it hasn't yet created the body element 445 | if (readyRE.test(document.readyState) && document.body) callback($) 446 | else document.addEventListener('DOMContentLoaded', function(){ callback($) }, false) 447 | return this 448 | }, 449 | get: function(idx){ 450 | return idx === undefined ? slice.call(this) : this[idx >= 0 ? idx : idx + this.length] 451 | }, 452 | toArray: function(){ return this.get() }, 453 | size: function(){ 454 | return this.length 455 | }, 456 | remove: function(){ 457 | return this.each(function(){ 458 | if (this.parentNode != null) 459 | this.parentNode.removeChild(this) 460 | }) 461 | }, 462 | each: function(callback){ 463 | emptyArray.every.call(this, function(el, idx){ 464 | return callback.call(el, idx, el) !== false 465 | }) 466 | return this 467 | }, 468 | filter: function(selector){ 469 | if (isFunction(selector)) return this.not(this.not(selector)) 470 | return $(filter.call(this, function(element){ 471 | return zepto.matches(element, selector) 472 | })) 473 | }, 474 | add: function(selector,context){ 475 | return $(uniq(this.concat($(selector,context)))) 476 | }, 477 | is: function(selector){ 478 | return this.length > 0 && zepto.matches(this[0], selector) 479 | }, 480 | not: function(selector){ 481 | var nodes=[] 482 | if (isFunction(selector) && selector.call !== undefined) 483 | this.each(function(idx){ 484 | if (!selector.call(this,idx)) nodes.push(this) 485 | }) 486 | else { 487 | var excludes = typeof selector == 'string' ? this.filter(selector) : 488 | (likeArray(selector) && isFunction(selector.item)) ? slice.call(selector) : $(selector) 489 | this.forEach(function(el){ 490 | if (excludes.indexOf(el) < 0) nodes.push(el) 491 | }) 492 | } 493 | return $(nodes) 494 | }, 495 | has: function(selector){ 496 | return this.filter(function(){ 497 | return isObject(selector) ? 498 | $.contains(this, selector) : 499 | $(this).find(selector).size() 500 | }) 501 | }, 502 | eq: function(idx){ 503 | return idx === -1 ? this.slice(idx) : this.slice(idx, + idx + 1) 504 | }, 505 | first: function(){ 506 | var el = this[0] 507 | return el && !isObject(el) ? el : $(el) 508 | }, 509 | last: function(){ 510 | var el = this[this.length - 1] 511 | return el && !isObject(el) ? el : $(el) 512 | }, 513 | find: function(selector){ 514 | var result, $this = this 515 | if (!selector) result = $() 516 | else if (typeof selector == 'object') 517 | result = $(selector).filter(function(){ 518 | var node = this 519 | return emptyArray.some.call($this, function(parent){ 520 | return $.contains(parent, node) 521 | }) 522 | }) 523 | else if (this.length == 1) result = $(zepto.qsa(this[0], selector)) 524 | else result = this.map(function(){ return zepto.qsa(this, selector) }) 525 | return result 526 | }, 527 | closest: function(selector, context){ 528 | var nodes = [], collection = typeof selector == 'object' && $(selector) 529 | this.each(function(_, node){ 530 | while (node && !(collection ? collection.indexOf(node) >= 0 : zepto.matches(node, selector))) 531 | node = node !== context && !isDocument(node) && node.parentNode 532 | if (node && nodes.indexOf(node) < 0) nodes.push(node) 533 | }) 534 | return $(nodes) 535 | }, 536 | parents: function(selector){ 537 | var ancestors = [], nodes = this 538 | while (nodes.length > 0) 539 | nodes = $.map(nodes, function(node){ 540 | if ((node = node.parentNode) && !isDocument(node) && ancestors.indexOf(node) < 0) { 541 | ancestors.push(node) 542 | return node 543 | } 544 | }) 545 | return filtered(ancestors, selector) 546 | }, 547 | parent: function(selector){ 548 | return filtered(uniq(this.pluck('parentNode')), selector) 549 | }, 550 | children: function(selector){ 551 | return filtered(this.map(function(){ return children(this) }), selector) 552 | }, 553 | contents: function() { 554 | return this.map(function() { return this.contentDocument || slice.call(this.childNodes) }) 555 | }, 556 | siblings: function(selector){ 557 | return filtered(this.map(function(i, el){ 558 | return filter.call(children(el.parentNode), function(child){ return child!==el }) 559 | }), selector) 560 | }, 561 | empty: function(){ 562 | return this.each(function(){ this.innerHTML = '' }) 563 | }, 564 | // `pluck` is borrowed from Prototype.js 565 | pluck: function(property){ 566 | return $.map(this, function(el){ return el[property] }) 567 | }, 568 | show: function(){ 569 | return this.each(function(){ 570 | this.style.display == "none" && (this.style.display = '') 571 | if (getComputedStyle(this, '').getPropertyValue("display") == "none") 572 | this.style.display = defaultDisplay(this.nodeName) 573 | }) 574 | }, 575 | replaceWith: function(newContent){ 576 | return this.before(newContent).remove() 577 | }, 578 | wrap: function(structure){ 579 | var func = isFunction(structure) 580 | if (this[0] && !func) 581 | var dom = $(structure).get(0), 582 | clone = dom.parentNode || this.length > 1 583 | 584 | return this.each(function(index){ 585 | $(this).wrapAll( 586 | func ? structure.call(this, index) : 587 | clone ? dom.cloneNode(true) : dom 588 | ) 589 | }) 590 | }, 591 | wrapAll: function(structure){ 592 | if (this[0]) { 593 | $(this[0]).before(structure = $(structure)) 594 | var children 595 | // drill down to the inmost element 596 | while ((children = structure.children()).length) structure = children.first() 597 | $(structure).append(this) 598 | } 599 | return this 600 | }, 601 | wrapInner: function(structure){ 602 | var func = isFunction(structure) 603 | return this.each(function(index){ 604 | var self = $(this), contents = self.contents(), 605 | dom = func ? structure.call(this, index) : structure 606 | contents.length ? contents.wrapAll(dom) : self.append(dom) 607 | }) 608 | }, 609 | unwrap: function(){ 610 | this.parent().each(function(){ 611 | $(this).replaceWith($(this).children()) 612 | }) 613 | return this 614 | }, 615 | clone: function(){ 616 | return this.map(function(){ return this.cloneNode(true) }) 617 | }, 618 | hide: function(){ 619 | return this.css("display", "none") 620 | }, 621 | toggle: function(setting){ 622 | return this.each(function(){ 623 | var el = $(this) 624 | ;(setting === undefined ? el.css("display") == "none" : setting) ? el.show() : el.hide() 625 | }) 626 | }, 627 | prev: function(selector){ return $(this.pluck('previousElementSibling')).filter(selector || '*') }, 628 | next: function(selector){ return $(this.pluck('nextElementSibling')).filter(selector || '*') }, 629 | html: function(html){ 630 | return 0 in arguments ? 631 | this.each(function(idx){ 632 | var originHtml = this.innerHTML 633 | $(this).empty().append( funcArg(this, html, idx, originHtml) ) 634 | }) : 635 | (0 in this ? this[0].innerHTML : null) 636 | }, 637 | text: function(text){ 638 | return 0 in arguments ? 639 | this.each(function(idx){ 640 | var newText = funcArg(this, text, idx, this.textContent) 641 | this.textContent = newText == null ? '' : ''+newText 642 | }) : 643 | (0 in this ? this.pluck('textContent').join("") : null) 644 | }, 645 | attr: function(name, value){ 646 | var result 647 | return (typeof name == 'string' && !(1 in arguments)) ? 648 | (0 in this && this[0].nodeType == 1 && (result = this[0].getAttribute(name)) != null ? result : undefined) : 649 | this.each(function(idx){ 650 | if (this.nodeType !== 1) return 651 | if (isObject(name)) for (key in name) setAttribute(this, key, name[key]) 652 | else setAttribute(this, name, funcArg(this, value, idx, this.getAttribute(name))) 653 | }) 654 | }, 655 | removeAttr: function(name){ 656 | return this.each(function(){ this.nodeType === 1 && name.split(' ').forEach(function(attribute){ 657 | setAttribute(this, attribute) 658 | }, this)}) 659 | }, 660 | prop: function(name, value){ 661 | name = propMap[name] || name 662 | return (1 in arguments) ? 663 | this.each(function(idx){ 664 | this[name] = funcArg(this, value, idx, this[name]) 665 | }) : 666 | (this[0] && this[0][name]) 667 | }, 668 | removeProp: function(name){ 669 | name = propMap[name] || name 670 | return this.each(function(){ delete this[name] }) 671 | }, 672 | data: function(name, value){ 673 | var attrName = 'data-' + name.replace(capitalRE, '-$1').toLowerCase() 674 | 675 | var data = (1 in arguments) ? 676 | this.attr(attrName, value) : 677 | this.attr(attrName) 678 | 679 | return data !== null ? deserializeValue(data) : undefined 680 | }, 681 | val: function(value){ 682 | if (0 in arguments) { 683 | if (value == null) value = "" 684 | return this.each(function(idx){ 685 | this.value = funcArg(this, value, idx, this.value) 686 | }) 687 | } else { 688 | return this[0] && (this[0].multiple ? 689 | $(this[0]).find('option').filter(function(){ return this.selected }).pluck('value') : 690 | this[0].value) 691 | } 692 | }, 693 | offset: function(coordinates){ 694 | if (coordinates) return this.each(function(index){ 695 | var $this = $(this), 696 | coords = funcArg(this, coordinates, index, $this.offset()), 697 | parentOffset = $this.offsetParent().offset(), 698 | props = { 699 | top: coords.top - parentOffset.top, 700 | left: coords.left - parentOffset.left 701 | } 702 | 703 | if ($this.css('position') == 'static') props['position'] = 'relative' 704 | $this.css(props) 705 | }) 706 | if (!this.length) return null 707 | if (document.documentElement !== this[0] && !$.contains(document.documentElement, this[0])) 708 | return {top: 0, left: 0} 709 | var obj = this[0].getBoundingClientRect() 710 | return { 711 | left: obj.left + window.pageXOffset, 712 | top: obj.top + window.pageYOffset, 713 | width: Math.round(obj.width), 714 | height: Math.round(obj.height) 715 | } 716 | }, 717 | css: function(property, value){ 718 | if (arguments.length < 2) { 719 | var element = this[0] 720 | if (typeof property == 'string') { 721 | if (!element) return 722 | return element.style[camelize(property)] || getComputedStyle(element, '').getPropertyValue(property) 723 | } else if (isArray(property)) { 724 | if (!element) return 725 | var props = {} 726 | var computedStyle = getComputedStyle(element, '') 727 | $.each(property, function(_, prop){ 728 | props[prop] = (element.style[camelize(prop)] || computedStyle.getPropertyValue(prop)) 729 | }) 730 | return props 731 | } 732 | } 733 | 734 | var css = '' 735 | if (type(property) == 'string') { 736 | if (!value && value !== 0) 737 | this.each(function(){ this.style.removeProperty(dasherize(property)) }) 738 | else 739 | css = dasherize(property) + ":" + maybeAddPx(property, value) 740 | } else { 741 | for (key in property) 742 | if (!property[key] && property[key] !== 0) 743 | this.each(function(){ this.style.removeProperty(dasherize(key)) }) 744 | else 745 | css += dasherize(key) + ':' + maybeAddPx(key, property[key]) + ';' 746 | } 747 | 748 | return this.each(function(){ this.style.cssText += ';' + css }) 749 | }, 750 | index: function(element){ 751 | return element ? this.indexOf($(element)[0]) : this.parent().children().indexOf(this[0]) 752 | }, 753 | hasClass: function(name){ 754 | if (!name) return false 755 | return emptyArray.some.call(this, function(el){ 756 | return this.test(className(el)) 757 | }, classRE(name)) 758 | }, 759 | addClass: function(name){ 760 | if (!name) return this 761 | return this.each(function(idx){ 762 | if (!('className' in this)) return 763 | classList = [] 764 | var cls = className(this), newName = funcArg(this, name, idx, cls) 765 | newName.split(/\s+/g).forEach(function(klass){ 766 | if (!$(this).hasClass(klass)) classList.push(klass) 767 | }, this) 768 | classList.length && className(this, cls + (cls ? " " : "") + classList.join(" ")) 769 | }) 770 | }, 771 | removeClass: function(name){ 772 | return this.each(function(idx){ 773 | if (!('className' in this)) return 774 | if (name === undefined) return className(this, '') 775 | classList = className(this) 776 | funcArg(this, name, idx, classList).split(/\s+/g).forEach(function(klass){ 777 | classList = classList.replace(classRE(klass), " ") 778 | }) 779 | className(this, classList.trim()) 780 | }) 781 | }, 782 | toggleClass: function(name, when){ 783 | if (!name) return this 784 | return this.each(function(idx){ 785 | var $this = $(this), names = funcArg(this, name, idx, className(this)) 786 | names.split(/\s+/g).forEach(function(klass){ 787 | (when === undefined ? !$this.hasClass(klass) : when) ? 788 | $this.addClass(klass) : $this.removeClass(klass) 789 | }) 790 | }) 791 | }, 792 | scrollTop: function(value){ 793 | if (!this.length) return 794 | var hasScrollTop = 'scrollTop' in this[0] 795 | if (value === undefined) return hasScrollTop ? this[0].scrollTop : this[0].pageYOffset 796 | return this.each(hasScrollTop ? 797 | function(){ this.scrollTop = value } : 798 | function(){ this.scrollTo(this.scrollX, value) }) 799 | }, 800 | scrollLeft: function(value){ 801 | if (!this.length) return 802 | var hasScrollLeft = 'scrollLeft' in this[0] 803 | if (value === undefined) return hasScrollLeft ? this[0].scrollLeft : this[0].pageXOffset 804 | return this.each(hasScrollLeft ? 805 | function(){ this.scrollLeft = value } : 806 | function(){ this.scrollTo(value, this.scrollY) }) 807 | }, 808 | position: function() { 809 | if (!this.length) return 810 | 811 | var elem = this[0], 812 | // Get *real* offsetParent 813 | offsetParent = this.offsetParent(), 814 | // Get correct offsets 815 | offset = this.offset(), 816 | parentOffset = rootNodeRE.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset() 817 | 818 | // Subtract element margins 819 | // note: when an element has margin: auto the offsetLeft and marginLeft 820 | // are the same in Safari causing offset.left to incorrectly be 0 821 | offset.top -= parseFloat( $(elem).css('margin-top') ) || 0 822 | offset.left -= parseFloat( $(elem).css('margin-left') ) || 0 823 | 824 | // Add offsetParent borders 825 | parentOffset.top += parseFloat( $(offsetParent[0]).css('border-top-width') ) || 0 826 | parentOffset.left += parseFloat( $(offsetParent[0]).css('border-left-width') ) || 0 827 | 828 | // Subtract the two offsets 829 | return { 830 | top: offset.top - parentOffset.top, 831 | left: offset.left - parentOffset.left 832 | } 833 | }, 834 | offsetParent: function() { 835 | return this.map(function(){ 836 | var parent = this.offsetParent || document.body 837 | while (parent && !rootNodeRE.test(parent.nodeName) && $(parent).css("position") == "static") 838 | parent = parent.offsetParent 839 | return parent 840 | }) 841 | } 842 | } 843 | 844 | // for now 845 | $.fn.detach = $.fn.remove 846 | 847 | // Generate the `width` and `height` functions 848 | ;['width', 'height'].forEach(function(dimension){ 849 | var dimensionProperty = 850 | dimension.replace(/./, function(m){ return m[0].toUpperCase() }) 851 | 852 | $.fn[dimension] = function(value){ 853 | var offset, el = this[0] 854 | if (value === undefined) return isWindow(el) ? el['inner' + dimensionProperty] : 855 | isDocument(el) ? el.documentElement['scroll' + dimensionProperty] : 856 | (offset = this.offset()) && offset[dimension] 857 | else return this.each(function(idx){ 858 | el = $(this) 859 | el.css(dimension, funcArg(this, value, idx, el[dimension]())) 860 | }) 861 | } 862 | }) 863 | 864 | function traverseNode(node, fun) { 865 | fun(node) 866 | for (var i = 0, len = node.childNodes.length; i < len; i++) 867 | traverseNode(node.childNodes[i], fun) 868 | } 869 | 870 | // Generate the `after`, `prepend`, `before`, `append`, 871 | // `insertAfter`, `insertBefore`, `appendTo`, and `prependTo` methods. 872 | adjacencyOperators.forEach(function(operator, operatorIndex) { 873 | var inside = operatorIndex % 2 //=> prepend, append 874 | 875 | $.fn[operator] = function(){ 876 | // arguments can be nodes, arrays of nodes, Zepto objects and HTML strings 877 | var argType, nodes = $.map(arguments, function(arg) { 878 | var arr = [] 879 | argType = type(arg) 880 | if (argType == "array") { 881 | arg.forEach(function(el) { 882 | if (el.nodeType !== undefined) return arr.push(el) 883 | else if ($.zepto.isZ(el)) return arr = arr.concat(el.get()) 884 | arr = arr.concat(zepto.fragment(el)) 885 | }) 886 | return arr 887 | } 888 | return argType == "object" || arg == null ? 889 | arg : zepto.fragment(arg) 890 | }), 891 | parent, copyByClone = this.length > 1 892 | if (nodes.length < 1) return this 893 | 894 | return this.each(function(_, target){ 895 | parent = inside ? target : target.parentNode 896 | 897 | // convert all methods to a "before" operation 898 | target = operatorIndex == 0 ? target.nextSibling : 899 | operatorIndex == 1 ? target.firstChild : 900 | operatorIndex == 2 ? target : 901 | null 902 | 903 | var parentInDocument = $.contains(document.documentElement, parent) 904 | 905 | nodes.forEach(function(node){ 906 | if (copyByClone) node = node.cloneNode(true) 907 | else if (!parent) return $(node).remove() 908 | 909 | parent.insertBefore(node, target) 910 | if (parentInDocument) traverseNode(node, function(el){ 911 | if (el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT' && 912 | (!el.type || el.type === 'text/javascript') && !el.src){ 913 | var target = el.ownerDocument ? el.ownerDocument.defaultView : window 914 | target['eval'].call(target, el.innerHTML) 915 | } 916 | }) 917 | }) 918 | }) 919 | } 920 | 921 | // after => insertAfter 922 | // prepend => prependTo 923 | // before => insertBefore 924 | // append => appendTo 925 | $.fn[inside ? operator+'To' : 'insert'+(operatorIndex ? 'Before' : 'After')] = function(html){ 926 | $(html)[operator](this) 927 | return this 928 | } 929 | }) 930 | 931 | zepto.Z.prototype = Z.prototype = $.fn 932 | 933 | // Export internal API functions in the `$.zepto` namespace 934 | zepto.uniq = uniq 935 | zepto.deserializeValue = deserializeValue 936 | $.zepto = zepto 937 | 938 | return $ 939 | })() 940 | 941 | window.Zepto = Zepto 942 | window.$ === undefined && (window.$ = Zepto) 943 | 944 | ;(function($){ 945 | var _zid = 1, undefined, 946 | slice = Array.prototype.slice, 947 | isFunction = $.isFunction, 948 | isString = function(obj){ return typeof obj == 'string' }, 949 | handlers = {}, 950 | specialEvents={}, 951 | focusinSupported = 'onfocusin' in window, 952 | focus = { focus: 'focusin', blur: 'focusout' }, 953 | hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' } 954 | 955 | specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents' 956 | 957 | function zid(element) { 958 | return element._zid || (element._zid = _zid++) 959 | } 960 | function findHandlers(element, event, fn, selector) { 961 | event = parse(event) 962 | if (event.ns) var matcher = matcherFor(event.ns) 963 | return (handlers[zid(element)] || []).filter(function(handler) { 964 | return handler 965 | && (!event.e || handler.e == event.e) 966 | && (!event.ns || matcher.test(handler.ns)) 967 | && (!fn || zid(handler.fn) === zid(fn)) 968 | && (!selector || handler.sel == selector) 969 | }) 970 | } 971 | function parse(event) { 972 | var parts = ('' + event).split('.') 973 | return {e: parts[0], ns: parts.slice(1).sort().join(' ')} 974 | } 975 | function matcherFor(ns) { 976 | return new RegExp('(?:^| )' + ns.replace(' ', ' .* ?') + '(?: |$)') 977 | } 978 | 979 | function eventCapture(handler, captureSetting) { 980 | return handler.del && 981 | (!focusinSupported && (handler.e in focus)) || 982 | !!captureSetting 983 | } 984 | 985 | function realEvent(type) { 986 | return hover[type] || (focusinSupported && focus[type]) || type 987 | } 988 | 989 | function add(element, events, fn, data, selector, delegator, capture){ 990 | var id = zid(element), set = (handlers[id] || (handlers[id] = [])) 991 | events.split(/\s/).forEach(function(event){ 992 | if (event == 'ready') return $(document).ready(fn) 993 | var handler = parse(event) 994 | handler.fn = fn 995 | handler.sel = selector 996 | // emulate mouseenter, mouseleave 997 | if (handler.e in hover) fn = function(e){ 998 | var related = e.relatedTarget 999 | if (!related || (related !== this && !$.contains(this, related))) 1000 | return handler.fn.apply(this, arguments) 1001 | } 1002 | handler.del = delegator 1003 | var callback = delegator || fn 1004 | handler.proxy = function(e){ 1005 | e = compatible(e) 1006 | if (e.isImmediatePropagationStopped()) return 1007 | e.data = data 1008 | var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args)) 1009 | if (result === false) e.preventDefault(), e.stopPropagation() 1010 | return result 1011 | } 1012 | handler.i = set.length 1013 | set.push(handler) 1014 | if ('addEventListener' in element) 1015 | element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture)) 1016 | }) 1017 | } 1018 | function remove(element, events, fn, selector, capture){ 1019 | var id = zid(element) 1020 | ;(events || '').split(/\s/).forEach(function(event){ 1021 | findHandlers(element, event, fn, selector).forEach(function(handler){ 1022 | delete handlers[id][handler.i] 1023 | if ('removeEventListener' in element) 1024 | element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture)) 1025 | }) 1026 | }) 1027 | } 1028 | 1029 | $.event = { add: add, remove: remove } 1030 | 1031 | $.proxy = function(fn, context) { 1032 | var args = (2 in arguments) && slice.call(arguments, 2) 1033 | if (isFunction(fn)) { 1034 | var proxyFn = function(){ return fn.apply(context, args ? args.concat(slice.call(arguments)) : arguments) } 1035 | proxyFn._zid = zid(fn) 1036 | return proxyFn 1037 | } else if (isString(context)) { 1038 | if (args) { 1039 | args.unshift(fn[context], fn) 1040 | return $.proxy.apply(null, args) 1041 | } else { 1042 | return $.proxy(fn[context], fn) 1043 | } 1044 | } else { 1045 | throw new TypeError("expected function") 1046 | } 1047 | } 1048 | 1049 | $.fn.bind = function(event, data, callback){ 1050 | return this.on(event, data, callback) 1051 | } 1052 | $.fn.unbind = function(event, callback){ 1053 | return this.off(event, callback) 1054 | } 1055 | $.fn.one = function(event, selector, data, callback){ 1056 | return this.on(event, selector, data, callback, 1) 1057 | } 1058 | 1059 | var returnTrue = function(){return true}, 1060 | returnFalse = function(){return false}, 1061 | ignoreProperties = /^([A-Z]|returnValue$|layer[XY]$|webkitMovement[XY]$)/, 1062 | eventMethods = { 1063 | preventDefault: 'isDefaultPrevented', 1064 | stopImmediatePropagation: 'isImmediatePropagationStopped', 1065 | stopPropagation: 'isPropagationStopped' 1066 | } 1067 | 1068 | function compatible(event, source) { 1069 | if (source || !event.isDefaultPrevented) { 1070 | source || (source = event) 1071 | 1072 | $.each(eventMethods, function(name, predicate) { 1073 | var sourceMethod = source[name] 1074 | event[name] = function(){ 1075 | this[predicate] = returnTrue 1076 | return sourceMethod && sourceMethod.apply(source, arguments) 1077 | } 1078 | event[predicate] = returnFalse 1079 | }) 1080 | 1081 | event.timeStamp || (event.timeStamp = Date.now()) 1082 | 1083 | if (source.defaultPrevented !== undefined ? source.defaultPrevented : 1084 | 'returnValue' in source ? source.returnValue === false : 1085 | source.getPreventDefault && source.getPreventDefault()) 1086 | event.isDefaultPrevented = returnTrue 1087 | } 1088 | return event 1089 | } 1090 | 1091 | function createProxy(event) { 1092 | var key, proxy = { originalEvent: event } 1093 | for (key in event) 1094 | if (!ignoreProperties.test(key) && event[key] !== undefined) proxy[key] = event[key] 1095 | 1096 | return compatible(proxy, event) 1097 | } 1098 | 1099 | $.fn.delegate = function(selector, event, callback){ 1100 | return this.on(event, selector, callback) 1101 | } 1102 | $.fn.undelegate = function(selector, event, callback){ 1103 | return this.off(event, selector, callback) 1104 | } 1105 | 1106 | $.fn.live = function(event, callback){ 1107 | $(document.body).delegate(this.selector, event, callback) 1108 | return this 1109 | } 1110 | $.fn.die = function(event, callback){ 1111 | $(document.body).undelegate(this.selector, event, callback) 1112 | return this 1113 | } 1114 | 1115 | $.fn.on = function(event, selector, data, callback, one){ 1116 | var autoRemove, delegator, $this = this 1117 | if (event && !isString(event)) { 1118 | $.each(event, function(type, fn){ 1119 | $this.on(type, selector, data, fn, one) 1120 | }) 1121 | return $this 1122 | } 1123 | 1124 | if (!isString(selector) && !isFunction(callback) && callback !== false) 1125 | callback = data, data = selector, selector = undefined 1126 | if (callback === undefined || data === false) 1127 | callback = data, data = undefined 1128 | 1129 | if (callback === false) callback = returnFalse 1130 | 1131 | return $this.each(function(_, element){ 1132 | if (one) autoRemove = function(e){ 1133 | remove(element, e.type, callback) 1134 | return callback.apply(this, arguments) 1135 | } 1136 | 1137 | if (selector) delegator = function(e){ 1138 | var evt, match = $(e.target).closest(selector, element).get(0) 1139 | if (match && match !== element) { 1140 | evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element}) 1141 | return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1))) 1142 | } 1143 | } 1144 | 1145 | add(element, event, callback, data, selector, delegator || autoRemove) 1146 | }) 1147 | } 1148 | $.fn.off = function(event, selector, callback){ 1149 | var $this = this 1150 | if (event && !isString(event)) { 1151 | $.each(event, function(type, fn){ 1152 | $this.off(type, selector, fn) 1153 | }) 1154 | return $this 1155 | } 1156 | 1157 | if (!isString(selector) && !isFunction(callback) && callback !== false) 1158 | callback = selector, selector = undefined 1159 | 1160 | if (callback === false) callback = returnFalse 1161 | 1162 | return $this.each(function(){ 1163 | remove(this, event, callback, selector) 1164 | }) 1165 | } 1166 | 1167 | $.fn.trigger = function(event, args){ 1168 | event = (isString(event) || $.isPlainObject(event)) ? $.Event(event) : compatible(event) 1169 | event._args = args 1170 | return this.each(function(){ 1171 | // handle focus(), blur() by calling them directly 1172 | if (event.type in focus && typeof this[event.type] == "function") this[event.type]() 1173 | // items in the collection might not be DOM elements 1174 | else if ('dispatchEvent' in this) this.dispatchEvent(event) 1175 | else $(this).triggerHandler(event, args) 1176 | }) 1177 | } 1178 | 1179 | // triggers event handlers on current element just as if an event occurred, 1180 | // doesn't trigger an actual event, doesn't bubble 1181 | $.fn.triggerHandler = function(event, args){ 1182 | var e, result 1183 | this.each(function(i, element){ 1184 | e = createProxy(isString(event) ? $.Event(event) : event) 1185 | e._args = args 1186 | e.target = element 1187 | $.each(findHandlers(element, event.type || event), function(i, handler){ 1188 | result = handler.proxy(e) 1189 | if (e.isImmediatePropagationStopped()) return false 1190 | }) 1191 | }) 1192 | return result 1193 | } 1194 | 1195 | // shortcut methods for `.bind(event, fn)` for each event type 1196 | ;('focusin focusout focus blur load resize scroll unload click dblclick '+ 1197 | 'mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave '+ 1198 | 'change select keydown keypress keyup error').split(' ').forEach(function(event) { 1199 | $.fn[event] = function(callback) { 1200 | return (0 in arguments) ? 1201 | this.bind(event, callback) : 1202 | this.trigger(event) 1203 | } 1204 | }) 1205 | 1206 | $.Event = function(type, props) { 1207 | if (!isString(type)) props = type, type = props.type 1208 | var event = document.createEvent(specialEvents[type] || 'Events'), bubbles = true 1209 | if (props) for (var name in props) (name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name]) 1210 | event.initEvent(type, bubbles, true) 1211 | return compatible(event) 1212 | } 1213 | 1214 | })(Zepto) 1215 | 1216 | ;(function($){ 1217 | var jsonpID = +new Date(), 1218 | document = window.document, 1219 | key, 1220 | name, 1221 | rscript = /)<[^<]*)*<\/script>/gi, 1222 | scriptTypeRE = /^(?:text|application)\/javascript/i, 1223 | xmlTypeRE = /^(?:text|application)\/xml/i, 1224 | jsonType = 'application/json', 1225 | htmlType = 'text/html', 1226 | blankRE = /^\s*$/, 1227 | originAnchor = document.createElement('a') 1228 | 1229 | originAnchor.href = window.location.href 1230 | 1231 | // trigger a custom event and return false if it was cancelled 1232 | function triggerAndReturn(context, eventName, data) { 1233 | var event = $.Event(eventName) 1234 | $(context).trigger(event, data) 1235 | return !event.isDefaultPrevented() 1236 | } 1237 | 1238 | // trigger an Ajax "global" event 1239 | function triggerGlobal(settings, context, eventName, data) { 1240 | if (settings.global) return triggerAndReturn(context || document, eventName, data) 1241 | } 1242 | 1243 | // Number of active Ajax requests 1244 | $.active = 0 1245 | 1246 | function ajaxStart(settings) { 1247 | if (settings.global && $.active++ === 0) triggerGlobal(settings, null, 'ajaxStart') 1248 | } 1249 | function ajaxStop(settings) { 1250 | if (settings.global && !(--$.active)) triggerGlobal(settings, null, 'ajaxStop') 1251 | } 1252 | 1253 | // triggers an extra global event "ajaxBeforeSend" that's like "ajaxSend" but cancelable 1254 | function ajaxBeforeSend(xhr, settings) { 1255 | var context = settings.context 1256 | if (settings.beforeSend.call(context, xhr, settings) === false || 1257 | triggerGlobal(settings, context, 'ajaxBeforeSend', [xhr, settings]) === false) 1258 | return false 1259 | 1260 | triggerGlobal(settings, context, 'ajaxSend', [xhr, settings]) 1261 | } 1262 | function ajaxSuccess(data, xhr, settings, deferred) { 1263 | var context = settings.context, status = 'success' 1264 | settings.success.call(context, data, status, xhr) 1265 | if (deferred) deferred.resolveWith(context, [data, status, xhr]) 1266 | triggerGlobal(settings, context, 'ajaxSuccess', [xhr, settings, data]) 1267 | ajaxComplete(status, xhr, settings) 1268 | } 1269 | // type: "timeout", "error", "abort", "parsererror" 1270 | function ajaxError(error, type, xhr, settings, deferred) { 1271 | var context = settings.context 1272 | settings.error.call(context, xhr, type, error) 1273 | if (deferred) deferred.rejectWith(context, [xhr, type, error]) 1274 | triggerGlobal(settings, context, 'ajaxError', [xhr, settings, error || type]) 1275 | ajaxComplete(type, xhr, settings) 1276 | } 1277 | // status: "success", "notmodified", "error", "timeout", "abort", "parsererror" 1278 | function ajaxComplete(status, xhr, settings) { 1279 | var context = settings.context 1280 | settings.complete.call(context, xhr, status) 1281 | triggerGlobal(settings, context, 'ajaxComplete', [xhr, settings]) 1282 | ajaxStop(settings) 1283 | } 1284 | 1285 | function ajaxDataFilter(data, type, settings) { 1286 | if (settings.dataFilter == empty) return data 1287 | var context = settings.context 1288 | return settings.dataFilter.call(context, data, type) 1289 | } 1290 | 1291 | // Empty function, used as default callback 1292 | function empty() {} 1293 | 1294 | $.ajaxJSONP = function(options, deferred){ 1295 | if (!('type' in options)) return $.ajax(options) 1296 | 1297 | var _callbackName = options.jsonpCallback, 1298 | callbackName = ($.isFunction(_callbackName) ? 1299 | _callbackName() : _callbackName) || ('Zepto' + (jsonpID++)), 1300 | script = document.createElement('script'), 1301 | originalCallback = window[callbackName], 1302 | responseData, 1303 | abort = function(errorType) { 1304 | $(script).triggerHandler('error', errorType || 'abort') 1305 | }, 1306 | xhr = { abort: abort }, abortTimeout 1307 | 1308 | if (deferred) deferred.promise(xhr) 1309 | 1310 | $(script).on('load error', function(e, errorType){ 1311 | clearTimeout(abortTimeout) 1312 | $(script).off().remove() 1313 | 1314 | if (e.type == 'error' || !responseData) { 1315 | ajaxError(null, errorType || 'error', xhr, options, deferred) 1316 | } else { 1317 | ajaxSuccess(responseData[0], xhr, options, deferred) 1318 | } 1319 | 1320 | window[callbackName] = originalCallback 1321 | if (responseData && $.isFunction(originalCallback)) 1322 | originalCallback(responseData[0]) 1323 | 1324 | originalCallback = responseData = undefined 1325 | }) 1326 | 1327 | if (ajaxBeforeSend(xhr, options) === false) { 1328 | abort('abort') 1329 | return xhr 1330 | } 1331 | 1332 | window[callbackName] = function(){ 1333 | responseData = arguments 1334 | } 1335 | 1336 | script.src = options.url.replace(/\?(.+)=\?/, '?$1=' + callbackName) 1337 | document.head.appendChild(script) 1338 | 1339 | if (options.timeout > 0) abortTimeout = setTimeout(function(){ 1340 | abort('timeout') 1341 | }, options.timeout) 1342 | 1343 | return xhr 1344 | } 1345 | 1346 | $.ajaxSettings = { 1347 | // Default type of request 1348 | type: 'GET', 1349 | // Callback that is executed before request 1350 | beforeSend: empty, 1351 | // Callback that is executed if the request succeeds 1352 | success: empty, 1353 | // Callback that is executed the the server drops error 1354 | error: empty, 1355 | // Callback that is executed on request complete (both: error and success) 1356 | complete: empty, 1357 | // The context for the callbacks 1358 | context: null, 1359 | // Whether to trigger "global" Ajax events 1360 | global: true, 1361 | // Transport 1362 | xhr: function () { 1363 | return new window.XMLHttpRequest() 1364 | }, 1365 | // MIME types mapping 1366 | // IIS returns Javascript as "application/x-javascript" 1367 | accepts: { 1368 | script: 'text/javascript, application/javascript, application/x-javascript', 1369 | json: jsonType, 1370 | xml: 'application/xml, text/xml', 1371 | html: htmlType, 1372 | text: 'text/plain' 1373 | }, 1374 | // Whether the request is to another domain 1375 | crossDomain: false, 1376 | // Default timeout 1377 | timeout: 0, 1378 | // Whether data should be serialized to string 1379 | processData: true, 1380 | // Whether the browser should be allowed to cache GET responses 1381 | cache: true, 1382 | //Used to handle the raw response data of XMLHttpRequest. 1383 | //This is a pre-filtering function to sanitize the response. 1384 | //The sanitized response should be returned 1385 | dataFilter: empty 1386 | } 1387 | 1388 | function mimeToDataType(mime) { 1389 | if (mime) mime = mime.split(';', 2)[0] 1390 | return mime && ( mime == htmlType ? 'html' : 1391 | mime == jsonType ? 'json' : 1392 | scriptTypeRE.test(mime) ? 'script' : 1393 | xmlTypeRE.test(mime) && 'xml' ) || 'text' 1394 | } 1395 | 1396 | function appendQuery(url, query) { 1397 | if (query == '') return url 1398 | return (url + '&' + query).replace(/[&?]{1,2}/, '?') 1399 | } 1400 | 1401 | // serialize payload and append it to the URL for GET requests 1402 | function serializeData(options) { 1403 | if (options.processData && options.data && $.type(options.data) != "string") 1404 | options.data = $.param(options.data, options.traditional) 1405 | if (options.data && (!options.type || options.type.toUpperCase() == 'GET' || 'jsonp' == options.dataType)) 1406 | options.url = appendQuery(options.url, options.data), options.data = undefined 1407 | } 1408 | 1409 | $.ajax = function(options){ 1410 | var settings = $.extend({}, options || {}), 1411 | deferred = $.Deferred && $.Deferred(), 1412 | urlAnchor, hashIndex 1413 | for (key in $.ajaxSettings) if (settings[key] === undefined) settings[key] = $.ajaxSettings[key] 1414 | 1415 | ajaxStart(settings) 1416 | 1417 | if (!settings.crossDomain) { 1418 | urlAnchor = document.createElement('a') 1419 | urlAnchor.href = settings.url 1420 | // cleans up URL for .href (IE only), see https://github.com/madrobby/zepto/pull/1049 1421 | urlAnchor.href = urlAnchor.href 1422 | settings.crossDomain = (originAnchor.protocol + '//' + originAnchor.host) !== (urlAnchor.protocol + '//' + urlAnchor.host) 1423 | } 1424 | 1425 | if (!settings.url) settings.url = window.location.toString() 1426 | if ((hashIndex = settings.url.indexOf('#')) > -1) settings.url = settings.url.slice(0, hashIndex) 1427 | serializeData(settings) 1428 | 1429 | var dataType = settings.dataType, hasPlaceholder = /\?.+=\?/.test(settings.url) 1430 | if (hasPlaceholder) dataType = 'jsonp' 1431 | 1432 | if (settings.cache === false || ( 1433 | (!options || options.cache !== true) && 1434 | ('script' == dataType || 'jsonp' == dataType) 1435 | )) 1436 | settings.url = appendQuery(settings.url, '_=' + Date.now()) 1437 | 1438 | if ('jsonp' == dataType) { 1439 | if (!hasPlaceholder) 1440 | settings.url = appendQuery(settings.url, 1441 | settings.jsonp ? (settings.jsonp + '=?') : settings.jsonp === false ? '' : 'callback=?') 1442 | return $.ajaxJSONP(settings, deferred) 1443 | } 1444 | 1445 | var mime = settings.accepts[dataType], 1446 | headers = { }, 1447 | setHeader = function(name, value) { headers[name.toLowerCase()] = [name, value] }, 1448 | protocol = /^([\w-]+:)\/\//.test(settings.url) ? RegExp.$1 : window.location.protocol, 1449 | xhr = settings.xhr(), 1450 | nativeSetHeader = xhr.setRequestHeader, 1451 | abortTimeout 1452 | 1453 | if (deferred) deferred.promise(xhr) 1454 | 1455 | if (!settings.crossDomain) setHeader('X-Requested-With', 'XMLHttpRequest') 1456 | setHeader('Accept', mime || '*/*') 1457 | if (mime = settings.mimeType || mime) { 1458 | if (mime.indexOf(',') > -1) mime = mime.split(',', 2)[0] 1459 | xhr.overrideMimeType && xhr.overrideMimeType(mime) 1460 | } 1461 | if (settings.contentType || (settings.contentType !== false && settings.data && settings.type.toUpperCase() != 'GET')) 1462 | setHeader('Content-Type', settings.contentType || 'application/x-www-form-urlencoded') 1463 | 1464 | if (settings.headers) for (name in settings.headers) setHeader(name, settings.headers[name]) 1465 | xhr.setRequestHeader = setHeader 1466 | 1467 | xhr.onreadystatechange = function(){ 1468 | if (xhr.readyState == 4) { 1469 | xhr.onreadystatechange = empty 1470 | clearTimeout(abortTimeout) 1471 | var result, error = false 1472 | if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 || (xhr.status == 0 && protocol == 'file:')) { 1473 | dataType = dataType || mimeToDataType(settings.mimeType || xhr.getResponseHeader('content-type')) 1474 | 1475 | if (xhr.responseType == 'arraybuffer' || xhr.responseType == 'blob') 1476 | result = xhr.response 1477 | else { 1478 | result = xhr.responseText 1479 | 1480 | try { 1481 | // http://perfectionkills.com/global-eval-what-are-the-options/ 1482 | // sanitize response accordingly if data filter callback provided 1483 | result = ajaxDataFilter(result, dataType, settings) 1484 | if (dataType == 'script') (1,eval)(result) 1485 | else if (dataType == 'xml') result = xhr.responseXML 1486 | else if (dataType == 'json') result = blankRE.test(result) ? null : $.parseJSON(result) 1487 | } catch (e) { error = e } 1488 | 1489 | if (error) return ajaxError(error, 'parsererror', xhr, settings, deferred) 1490 | } 1491 | 1492 | ajaxSuccess(result, xhr, settings, deferred) 1493 | } else { 1494 | ajaxError(xhr.statusText || null, xhr.status ? 'error' : 'abort', xhr, settings, deferred) 1495 | } 1496 | } 1497 | } 1498 | 1499 | if (ajaxBeforeSend(xhr, settings) === false) { 1500 | xhr.abort() 1501 | ajaxError(null, 'abort', xhr, settings, deferred) 1502 | return xhr 1503 | } 1504 | 1505 | var async = 'async' in settings ? settings.async : true 1506 | xhr.open(settings.type, settings.url, async, settings.username, settings.password) 1507 | 1508 | if (settings.xhrFields) for (name in settings.xhrFields) xhr[name] = settings.xhrFields[name] 1509 | 1510 | for (name in headers) nativeSetHeader.apply(xhr, headers[name]) 1511 | 1512 | if (settings.timeout > 0) abortTimeout = setTimeout(function(){ 1513 | xhr.onreadystatechange = empty 1514 | xhr.abort() 1515 | ajaxError(null, 'timeout', xhr, settings, deferred) 1516 | }, settings.timeout) 1517 | 1518 | // avoid sending empty string (#319) 1519 | xhr.send(settings.data ? settings.data : null) 1520 | return xhr 1521 | } 1522 | 1523 | // handle optional data/success arguments 1524 | function parseArguments(url, data, success, dataType) { 1525 | if ($.isFunction(data)) dataType = success, success = data, data = undefined 1526 | if (!$.isFunction(success)) dataType = success, success = undefined 1527 | return { 1528 | url: url 1529 | , data: data 1530 | , success: success 1531 | , dataType: dataType 1532 | } 1533 | } 1534 | 1535 | $.get = function(/* url, data, success, dataType */){ 1536 | return $.ajax(parseArguments.apply(null, arguments)) 1537 | } 1538 | 1539 | $.post = function(/* url, data, success, dataType */){ 1540 | var options = parseArguments.apply(null, arguments) 1541 | options.type = 'POST' 1542 | return $.ajax(options) 1543 | } 1544 | 1545 | $.getJSON = function(/* url, data, success */){ 1546 | var options = parseArguments.apply(null, arguments) 1547 | options.dataType = 'json' 1548 | return $.ajax(options) 1549 | } 1550 | 1551 | $.fn.load = function(url, data, success){ 1552 | if (!this.length) return this 1553 | var self = this, parts = url.split(/\s/), selector, 1554 | options = parseArguments(url, data, success), 1555 | callback = options.success 1556 | if (parts.length > 1) options.url = parts[0], selector = parts[1] 1557 | options.success = function(response){ 1558 | self.html(selector ? 1559 | $('
').html(response.replace(rscript, "")).find(selector) 1560 | : response) 1561 | callback && callback.apply(self, arguments) 1562 | } 1563 | $.ajax(options) 1564 | return this 1565 | } 1566 | 1567 | var escape = encodeURIComponent 1568 | 1569 | function serialize(params, obj, traditional, scope){ 1570 | var type, array = $.isArray(obj), hash = $.isPlainObject(obj) 1571 | $.each(obj, function(key, value) { 1572 | type = $.type(value) 1573 | if (scope) key = traditional ? scope : 1574 | scope + '[' + (hash || type == 'object' || type == 'array' ? key : '') + ']' 1575 | // handle data in serializeArray() format 1576 | if (!scope && array) params.add(value.name, value.value) 1577 | // recurse into nested objects 1578 | else if (type == "array" || (!traditional && type == "object")) 1579 | serialize(params, value, traditional, key) 1580 | else params.add(key, value) 1581 | }) 1582 | } 1583 | 1584 | $.param = function(obj, traditional){ 1585 | var params = [] 1586 | params.add = function(key, value) { 1587 | if ($.isFunction(value)) value = value() 1588 | if (value == null) value = "" 1589 | this.push(escape(key) + '=' + escape(value)) 1590 | } 1591 | serialize(params, obj, traditional) 1592 | return params.join('&').replace(/%20/g, '+') 1593 | } 1594 | })(Zepto) 1595 | 1596 | ;(function($){ 1597 | $.fn.serializeArray = function() { 1598 | var name, type, result = [], 1599 | add = function(value) { 1600 | if (value.forEach) return value.forEach(add) 1601 | result.push({ name: name, value: value }) 1602 | } 1603 | if (this[0]) $.each(this[0].elements, function(_, field){ 1604 | type = field.type, name = field.name 1605 | if (name && field.nodeName.toLowerCase() != 'fieldset' && 1606 | !field.disabled && type != 'submit' && type != 'reset' && type != 'button' && type != 'file' && 1607 | ((type != 'radio' && type != 'checkbox') || field.checked)) 1608 | add($(field).val()) 1609 | }) 1610 | return result 1611 | } 1612 | 1613 | $.fn.serialize = function(){ 1614 | var result = [] 1615 | this.serializeArray().forEach(function(elm){ 1616 | result.push(encodeURIComponent(elm.name) + '=' + encodeURIComponent(elm.value)) 1617 | }) 1618 | return result.join('&') 1619 | } 1620 | 1621 | $.fn.submit = function(callback) { 1622 | if (0 in arguments) this.bind('submit', callback) 1623 | else if (this.length) { 1624 | var event = $.Event('submit') 1625 | this.eq(0).trigger(event) 1626 | if (!event.isDefaultPrevented()) this.get(0).submit() 1627 | } 1628 | return this 1629 | } 1630 | 1631 | })(Zepto) 1632 | 1633 | ;(function(){ 1634 | // getComputedStyle shouldn't freak out when called 1635 | // without a valid element as argument 1636 | try { 1637 | getComputedStyle(undefined) 1638 | } catch(e) { 1639 | var nativeGetComputedStyle = getComputedStyle 1640 | window.getComputedStyle = function(element, pseudoElement){ 1641 | try { 1642 | return nativeGetComputedStyle(element, pseudoElement) 1643 | } catch(e) { 1644 | return null 1645 | } 1646 | } 1647 | } 1648 | })() 1649 | return Zepto 1650 | })) 1651 | -------------------------------------------------------------------------------- /src/wtplan-web/login_view.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Kjell Winblad (kjellwinblad@gmail.com, http://winsh.me) 2 | // License: MIT License, see the LICENSE file 3 | 4 | var WTPLAN = WTPLAN || {}; 5 | 6 | (function() { 7 | 8 | //Constructor 9 | WTPLAN.LoginView = function(authenticateCallback) { 10 | WTPLAN.Component.call(this); 11 | this.authenticateCallback = authenticateCallback; 12 | } 13 | //Extending Component 14 | WTPLAN.LoginView.prototype = Object.create(WTPLAN.Component.prototype); 15 | WTPLAN.LoginView.prototype.constructor = WTPLAN.LoginView; 16 | 17 | //Methods 18 | WTPLAN.LoginView.prototype.render = function() { 19 | return WTPLAN.template( 20 | '\
\ 21 |

Password:

\ 22 | \ 23 |

\ 24 |
', 25 | {}); 26 | 27 | }; 28 | 29 | WTPLAN.LoginView.prototype.componentDidMount = function(component) { 30 | var outerThis = this; 31 | $("#loginButton").click(function() { 32 | var password = $("#passwordField").val(); 33 | outerThis.authenticateCallback(password); 34 | }); 35 | }; 36 | 37 | })() 38 | -------------------------------------------------------------------------------- /src/wtplan-web/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Kjell Winblad (kjellwinblad@gmail.com, http://winsh.me) 2 | // License: MIT License, see the LICENSE file 3 | 4 | package main 5 | 6 | //go:generate go run include_text/includetext.go 7 | 8 | import ( 9 | "crypto/rand" 10 | "encoding/json" 11 | "flag" 12 | "fmt" 13 | "net/http" 14 | "os/exec" 15 | "os" 16 | "strings" 17 | ) 18 | 19 | var authenticationTokens []string = nil 20 | var passwordGiven bool = true 21 | 22 | type addCalendarItemMessage struct { 23 | Date string `json:"date"` 24 | Duration string `json:"duration"` 25 | Description string `json:"description"` 26 | LoginToken string `json:"loginToken"` 27 | } 28 | 29 | type editCalendarItemMessage struct { 30 | Date string `json:"date"` 31 | Duration string `json:"duration"` 32 | Description string `json:"description"` 33 | ID string `json:"id"` 34 | LoginToken string `json:"loginToken"` 35 | } 36 | 37 | type removeCalendarItemMessage struct { 38 | IDs []string `json:"ids"` 39 | LoginToken string `json:"loginToken"` 40 | } 41 | 42 | type getCalendarItemsMessage struct { 43 | LoginToken string `json:"loginToken"` 44 | } 45 | 46 | type getLoginTokenMessage struct { 47 | Password string `json:"password"` 48 | } 49 | 50 | type loginTokenResponseMessage struct { 51 | LoginToken string `json:"loginToken"` 52 | } 53 | 54 | type logoutMessage struct { 55 | LoginToken string `json:"loginToken"` 56 | } 57 | 58 | func staticFileHandlerProducer(content string) func(w http.ResponseWriter, r *http.Request) { 59 | return func(w http.ResponseWriter, r *http.Request) { 60 | w.Write([]byte(content)) 61 | } 62 | } 63 | 64 | func runWTPlanCommand(w http.ResponseWriter, loginToken string, command ...string) { 65 | if authenticate(loginToken, w) { 66 | cmd := exec.Command("wtplan", command...) 67 | b, err := cmd.CombinedOutput() 68 | if err != nil { 69 | http.Error(w, string(b), 500) 70 | return 71 | } 72 | w.Write(b) 73 | } 74 | } 75 | 76 | func authenticate(loginToken string, w http.ResponseWriter) bool { 77 | if ! passwordGiven { 78 | return true 79 | } 80 | for i := range(authenticationTokens) { 81 | if authenticationTokens[i] == loginToken { 82 | return true; 83 | } 84 | } 85 | http.Error(w, "NOT_AUTHENTICATED", 500) 86 | return false 87 | } 88 | 89 | func loginTokenRequest(password string) func (w http.ResponseWriter, r *http.Request){ 90 | return func (w http.ResponseWriter, r *http.Request) { 91 | decoder := json.NewDecoder(r.Body) 92 | var msg getLoginTokenMessage 93 | err := decoder.Decode(&msg) 94 | defer r.Body.Close() 95 | if err != nil { 96 | http.Error(w, fmt.Sprintf("Could not parse request message %s", err.Error()), 500) 97 | return 98 | } 99 | if msg.Password == password { 100 | // Create and send back auth token 101 | token := createNewAuthenticationToken() 102 | var response loginTokenResponseMessage 103 | response.LoginToken = token 104 | b, _ := json.MarshalIndent(response, "", " ") 105 | w.Write(b) 106 | } else { 107 | http.Error(w, "NOT_AUTHENTICATED", 500) 108 | } 109 | } 110 | } 111 | 112 | func logoutRequest(w http.ResponseWriter, r *http.Request) { 113 | decoder := json.NewDecoder(r.Body) 114 | var msg logoutMessage 115 | err := decoder.Decode(&msg) 116 | defer r.Body.Close() 117 | if err != nil { 118 | http.Error(w, fmt.Sprintf("Could not parse request message %s", err.Error()), 500) 119 | return 120 | } 121 | var itemsToSave []string 122 | for _, token := range(authenticationTokens) { 123 | if token != msg.LoginToken { 124 | itemsToSave = append(itemsToSave, token) 125 | } 126 | } 127 | authenticationTokens = itemsToSave 128 | } 129 | 130 | func calendarItemsRequest(w http.ResponseWriter, r *http.Request) { 131 | decoder := json.NewDecoder(r.Body) 132 | var msg getCalendarItemsMessage 133 | err := decoder.Decode(&msg) 134 | defer r.Body.Close() 135 | if err != nil { 136 | http.Error(w, fmt.Sprintf("Could not parse request message %s", err.Error()), 500) 137 | return 138 | } 139 | runWTPlanCommand(w, msg.LoginToken, "show", "ALLJSON") 140 | } 141 | 142 | func addCalendarItemRequest(w http.ResponseWriter, r *http.Request) { 143 | decoder := json.NewDecoder(r.Body) 144 | var msg addCalendarItemMessage 145 | err := decoder.Decode(&msg) 146 | defer r.Body.Close() 147 | if err != nil { 148 | http.Error(w, fmt.Sprintf("Could not parse request message %s", err.Error()), 500) 149 | return 150 | } 151 | runWTPlanCommand(w, msg.LoginToken, "add", msg.Date, msg.Duration, msg.Description) 152 | } 153 | 154 | func removeCalendarItemRequest(w http.ResponseWriter, r *http.Request) { 155 | decoder := json.NewDecoder(r.Body) 156 | var msg removeCalendarItemMessage 157 | err := decoder.Decode(&msg) 158 | defer r.Body.Close() 159 | if err != nil { 160 | http.Error(w, "Could not parse request message", 500) 161 | return 162 | } 163 | runWTPlanCommand(w, msg.LoginToken, append([]string{"remove"}, msg.IDs...)...) 164 | } 165 | 166 | func editCalendarItemRequest(w http.ResponseWriter, r *http.Request) { 167 | decoder := json.NewDecoder(r.Body) 168 | var msg editCalendarItemMessage 169 | err := decoder.Decode(&msg) 170 | defer r.Body.Close() 171 | if err != nil { 172 | http.Error(w, fmt.Sprintf("Could not parse request message %s", err.Error()), 500) 173 | return 174 | } 175 | if authenticate(msg.LoginToken, w) { 176 | runWTPlanCommand(w, msg.LoginToken, "remove", msg.ID) 177 | runWTPlanCommand(w, msg.LoginToken, "add", msg.Date, msg.Duration, msg.Description) 178 | } 179 | } 180 | 181 | func createNewAuthenticationToken() string { 182 | letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 183 | c := 512 184 | b := make([]byte, c) 185 | token := make([]rune, c) 186 | _, err := rand.Read(b) 187 | if err != nil { 188 | fmt.Println("error:", err) 189 | os.Exit(1) 190 | } 191 | for i, _ := range(b) { 192 | token[i] = letters[(uint32(b[i]) % uint32(len(letters)))] 193 | } 194 | if len(authenticationTokens) == 10 { 195 | newAuthenticationTokens := make([]string, 9, 10) 196 | copy(newAuthenticationTokens, authenticationTokens[1:10]) 197 | authenticationTokens = newAuthenticationTokens 198 | } 199 | tokenStr := string(token) 200 | authenticationTokens = append(authenticationTokens, tokenStr) 201 | return tokenStr 202 | } 203 | 204 | func main() { 205 | addressPtr := flag.String("address", "127.0.0.1:8005", "The serve address (default \"127.0.0.1:8005\")") 206 | passwordPtr := flag.String("password", "", "The password") 207 | flag.Parse() 208 | if *passwordPtr == "" { 209 | passwordGiven = false 210 | } 211 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 212 | indexPage := indexDhtml 213 | if *passwordPtr == "" { 214 | token := createNewAuthenticationToken() 215 | indexPage = strings.Replace(indexPage, "NOT_AUTHENTICATED", token, 1) 216 | } 217 | w.Write([]byte(indexPage)) 218 | }) 219 | http.HandleFunc("/lib/zepto.js", staticFileHandlerProducer(libDzeptoDjs)) 220 | http.HandleFunc("/component.js", staticFileHandlerProducer(componentDjs)) 221 | http.HandleFunc("/edit_item_view.js", staticFileHandlerProducer(editDitemDviewDjs)) 222 | http.HandleFunc("/main_page.js", staticFileHandlerProducer(mainDpageDjs)) 223 | http.HandleFunc("/flow_view.js", staticFileHandlerProducer(flowDviewDjs)) 224 | http.HandleFunc("/day_list.js", staticFileHandlerProducer(dayDlistDjs)) 225 | http.HandleFunc("/day_list_day.js", staticFileHandlerProducer(dayDlistDdayDjs)) 226 | http.HandleFunc("/day_list_item.js", staticFileHandlerProducer(dayDlistDitemDjs)) 227 | http.HandleFunc("/calendar_item.js", staticFileHandlerProducer(calendarDitemDjs)) 228 | http.HandleFunc("/remote_commands.js", staticFileHandlerProducer(remoteDcommandsDjs)) 229 | http.HandleFunc("/utility_functions.js", staticFileHandlerProducer(utilityDfunctionsDjs)) 230 | http.HandleFunc("/login_view.js", staticFileHandlerProducer(loginDviewDjs)) 231 | http.HandleFunc("/login_token_request", loginTokenRequest(*passwordPtr)) 232 | http.HandleFunc("/calendar_items", calendarItemsRequest) 233 | http.HandleFunc("/add_calendar_item", addCalendarItemRequest) 234 | http.HandleFunc("/edit_calendar_item", editCalendarItemRequest) 235 | http.HandleFunc("/remove_calendar_item", removeCalendarItemRequest) 236 | http.HandleFunc("/logout", logoutRequest) 237 | fmt.Println("Starting wtplan web at http://"+*addressPtr) 238 | http.ListenAndServe(*addressPtr, nil) 239 | } 240 | -------------------------------------------------------------------------------- /src/wtplan-web/main_page.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Kjell Winblad (kjellwinblad@gmail.com, http://winsh.me) 2 | // License: MIT License, see the LICENSE file 3 | 4 | var WTPLAN = WTPLAN || {}; 5 | 6 | (function() { 7 | 8 | 9 | //The MainPage component 10 | 11 | //Constructor 12 | WTPLAN.MainPage = function(showLogoutButton) { 13 | WTPLAN.Component.call(this); 14 | this.state = { 15 | outgoingRequest: true, 16 | page: "MAIN_PAGE", 17 | message: "", 18 | isErrorMessage: false, 19 | calendarItems: [], 20 | showLogoutButton: showLogoutButton 21 | } 22 | this._flowView = new WTPLAN.FlowView( 23 | this.openAddDialog.bind(this), 24 | this.openEditDialog.bind(this), 25 | this.removeItemAction.bind(this) 26 | ); 27 | var outerThis = this; 28 | WTPLAN.remoteFetchCalendarItems(function(items) { 29 | outerThis.state.calendarItems = items; 30 | outerThis.state.outgoingRequest = false; 31 | outerThis.setState(outerThis.state); 32 | }, this.handleNotAuth(function(errorText) { 33 | outerThis.state.message = errorText; 34 | outerThis.state.isErrorMessage = true; 35 | outerThis.state.outgoingRequest = false; 36 | outerThis.setState(outerThis.state); 37 | })); 38 | } 39 | //Extending Component 40 | WTPLAN.MainPage.prototype = Object.create(WTPLAN.Component.prototype); 41 | WTPLAN.MainPage.prototype.constructor = WTPLAN.MainPage; 42 | 43 | //Render methods 44 | WTPLAN.MainPage.prototype.render = function() { 45 | if (this.state.outgoingRequest) { 46 | return "
Loading...
"; 47 | } else if (this.state.page === "MAIN_PAGE") { 48 | return WTPLAN.template( 49 | '
\ 50 | \ 51 | <%logoutButton%>\ 52 |
\ 53 | <%message%>\ 54 |
\ 55 |
',{ 56 | message: (this.state.message ? (this.state.message + "
") : ""), 57 | logoutButton: (this.state.showLogoutButton ? '': "") 58 | }); 59 | } else if (this.state.page === "EDIT_CALENDAR_ITEM_PAGE") { 60 | return '
'; 61 | } else if (this.state.page === "LOGIN_PAGE") { 62 | return '
'; 63 | } 64 | 65 | 66 | }; 67 | 68 | WTPLAN.MainPage.prototype.componentDidMount = function(component) { 69 | if (!this.state.outgoingRequest) { 70 | if (this.state.page === "MAIN_PAGE") { 71 | this._flowView.state.calendarItems = this.state.calendarItems; 72 | this._flowView.renderAt($("#calendarViewMountPoint")); 73 | $('#mainAddButton').click(function() { 74 | this.openAddDialog(WTPLAN.dateToRfc3339String(WTPLAN.getLocalStartOfDay( 75 | WTPLAN.getLocalDayString()))); 76 | }.bind(this)); 77 | if(this.state.showLogoutButton){ 78 | $('#logoutButton').click(function() { 79 | this.showLoadingPage(); 80 | WTPLAN.logoutRequest( 81 | function(){ 82 | window.location.reload(); 83 | }, 84 | function(errorStr){ 85 | this.showMainPage(); 86 | alert(errorStr); 87 | }.bind(this)); 88 | }.bind(this)); 89 | } 90 | } else if (this.state.page === "EDIT_CALENDAR_ITEM_PAGE") { 91 | this._editItemView.renderAt($("#editCalendarItemPageDiv")); 92 | } else if (this.state.page === "LOGIN_PAGE") { 93 | this._loginPage.renderAt($("#loginPageDiv")); 94 | } 95 | } 96 | }; 97 | 98 | //Utility methods 99 | 100 | WTPLAN.MainPage.prototype.showLoginPage = function(retryFunction) { 101 | var oldPage = this.state.page; 102 | this.state.page = "LOGIN_PAGE"; 103 | this._loginPage = new WTPLAN.LoginView( 104 | function(password){ 105 | this.showLoadingPage(); 106 | WTPLAN.loginTokenRequest( 107 | password, 108 | function(){ 109 | this.state.outgoingRequest = false; 110 | this.state.page = oldPage; 111 | this.setState(this.state); 112 | this.showLoadingPage(); 113 | retryFunction(); 114 | }.bind(this), 115 | function(errorStr){ 116 | this.state.outgoingRequest = false; 117 | alert(errorStr); 118 | this.state.page = oldPage; 119 | this.showLoginPage(retryFunction); 120 | }.bind(this)) 121 | }.bind(this) 122 | ); 123 | this.setState(this.state); 124 | } 125 | 126 | WTPLAN.MainPage.prototype.handleNotAuth = function(secondErrorHandler) { 127 | var outerThis = this; 128 | return function(errorString, retryFunction){ 129 | if(errorString.startsWith("NOT_AUTHENTICATED")) { 130 | outerThis.state.outgoingRequest = false; 131 | outerThis.showLoginPage(retryFunction); 132 | } else { 133 | secondErrorHandler(errorString); 134 | } 135 | } 136 | } 137 | 138 | WTPLAN.MainPage.prototype.showCalendarItemPage = function(calendarItem, 139 | successCallback) { 140 | var outerThis = this; 141 | this._editItemView = new WTPLAN.EditItemView(calendarItem, 142 | function(item) { 143 | outerThis.showMainPage(); 144 | successCallback(item); 145 | }, 146 | function() { 147 | outerThis.showMainPage(); 148 | }); 149 | this.state.page = "EDIT_CALENDAR_ITEM_PAGE"; 150 | this.setState(this.state); 151 | }; 152 | 153 | WTPLAN.MainPage.prototype.showMainPage = function() { 154 | this.state.outgoingRequest = false; 155 | this.state.page = "MAIN_PAGE"; 156 | this.setState(this.state); 157 | }; 158 | 159 | WTPLAN.MainPage.prototype.showLoadingPage = function() { 160 | this.state.outgoingRequest = true; 161 | this.setState(this.state); 162 | } 163 | 164 | //Helper functions 165 | WTPLAN.MainPage.prototype.openAddDialog = function(dateString) { 166 | var calendarItemTemplate = { 167 | date: dateString, 168 | duration: "NA", 169 | description: "" 170 | }; 171 | var outerThis = this; 172 | outerThis.showCalendarItemPage( 173 | new WTPLAN.CalendarItem(calendarItemTemplate), 174 | function(item) { 175 | WTPLAN.addCalendarItem(outerThis.state.calendarItems, item); 176 | outerThis.showLoadingPage(); 177 | WTPLAN.remoteAddCalendarItem( 178 | item, 179 | function() { 180 | outerThis.showMainPage(); 181 | }, 182 | this.handleNotAuth(function(error) { 183 | alert(error) 184 | outerThis.showMainPage(); 185 | })); 186 | }.bind(this)); 187 | }; 188 | 189 | WTPLAN.MainPage.prototype.openEditDialog = function(calendarItem) { 190 | var outerThis = this; 191 | outerThis.showCalendarItemPage( 192 | calendarItem, 193 | function(item) { 194 | WTPLAN.updateCalendarItem(outerThis.state.calendarItems, 195 | calendarItem, item); 196 | outerThis.showLoadingPage(); 197 | WTPLAN.remoteEditCalendarItem( 198 | item, calendarItem.id, 199 | function() { 200 | outerThis.showMainPage(); 201 | }, 202 | this.handleNotAuth(function(error) { 203 | alert(error) 204 | outerThis.showMainPage(); 205 | })); 206 | }.bind(this)); 207 | }; 208 | 209 | WTPLAN.MainPage.prototype.removeItemAction = function(id) { 210 | var outerThis = this; 211 | WTPLAN.removeCalendarItem(outerThis.state.calendarItems, id); 212 | outerThis.showLoadingPage(); 213 | WTPLAN.remoteRemoveCalendarItem( 214 | id, 215 | function() { 216 | outerThis.showMainPage(); 217 | }, 218 | this.handleNotAuth(function(error) { 219 | alert(error) 220 | outerThis.showMainPage(); 221 | })); 222 | }; 223 | 224 | })(); 225 | -------------------------------------------------------------------------------- /src/wtplan-web/remote_commands.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Kjell Winblad (kjellwinblad@gmail.com, http://winsh.me) 2 | // License: MIT License, see the LICENSE file 3 | 4 | var WTPLAN = WTPLAN || {}; 5 | 6 | (function() { 7 | 8 | //Remote commands 9 | 10 | WTPLAN.remoteFetchCalendarItems = function(successCallback, errorCallback) { 11 | WTPLAN.serverRequest("/calendar_items", {}, function(itemsStr) { 12 | var items = JSON.parse(itemsStr); 13 | items = items.map(function(item, index) { 14 | return new WTPLAN.CalendarItem(item, index + 1); 15 | }); 16 | successCallback(items) 17 | }, errorCallback); 18 | }; 19 | 20 | 21 | WTPLAN.remoteAddCalendarItem = function(calendarItem, successCallback, 22 | errorCallback) { 23 | WTPLAN.serverRequest("/add_calendar_item", calendarItem.toPlainObject(), successCallback, errorCallback); 24 | }; 25 | 26 | WTPLAN.remoteEditCalendarItem = function(calendarItem, id, successCallback, 27 | errorCallback) { 28 | var message = calendarItem.toPlainObject(); 29 | message.id = "" + id; 30 | WTPLAN.serverRequest("/edit_calendar_item", message, successCallback, errorCallback); 31 | }; 32 | 33 | WTPLAN.remoteRemoveCalendarItem = function(id, successCallback, 34 | errorCallback) { 35 | var message = { 36 | ids: ["" + id] 37 | }; 38 | WTPLAN.serverRequest("/remove_calendar_item", message, successCallback, errorCallback); 39 | }; 40 | 41 | WTPLAN.loginTokenRequest = function(password, successCallback, errorCallback) { 42 | var message = { 43 | password: password 44 | }; 45 | function success(dataStr){ 46 | var data = JSON.parse(dataStr); 47 | WTPLAN.loginToken = data.loginToken; 48 | successCallback(); 49 | } 50 | WTPLAN.serverRequest("/login_token_request", message, success, errorCallback, true); 51 | }; 52 | 53 | WTPLAN.logoutRequest = function(successCallback, errorCallback) { 54 | var message = { 55 | loginToken: WTPLAN.loginToken 56 | }; 57 | WTPLAN.serverRequest("/logout", message, successCallback, errorCallback, true); 58 | }; 59 | 60 | 61 | })(); 62 | -------------------------------------------------------------------------------- /src/wtplan-web/utility_functions.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Kjell Winblad (kjellwinblad@gmail.com, http://winsh.me) 2 | // License: MIT License, see the LICENSE file 3 | 4 | var WTPLAN = WTPLAN || {}; 5 | 6 | (function() { 7 | 8 | WTPLAN.rfc3339regexp = 9 | /([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))/; 10 | 11 | WTPLAN.durationRegExp = /^(\s*NA\s*)|\s*(?:(\d+)h)?\s*(?:(\d+)m)?\s*$/; 12 | 13 | 14 | WTPLAN.serverRequest = function(url, dataObject, successCallback, errorCallback, noAuth){ 15 | var requestFun; 16 | requestFun = function(){ 17 | if(WTPLAN.loginToken === "NOT_AUTHENTICATED" && (noAuth !== true)) { 18 | errorCallback("NOT_AUTHENTICATED", requestFun); 19 | return; 20 | } 21 | if(dataObject !== null && noAuth !== true){ 22 | dataObject.loginToken = WTPLAN.loginToken; 23 | } 24 | $.ajax({ 25 | url: url, 26 | data: JSON.stringify(dataObject), 27 | type: 'POST', 28 | success: successCallback, 29 | error:function(msg){ 30 | errorCallback(msg.responseText, requestFun); 31 | } 32 | }); 33 | }; 34 | requestFun(); 35 | }; 36 | 37 | WTPLAN.addCalendarItem = function(calendarItems, calendarItem) { 38 | calendarItems.push(calendarItem); 39 | calendarItems.sort(function(item1, item2) { 40 | return item1.startDate().getTime() - item2.startDate().getTime(); 41 | }); 42 | calendarItems.forEach(function(item, index) { 43 | item.id = index + 1; 44 | }); 45 | }; 46 | 47 | WTPLAN.updateCalendarItem = function(calendarItems, oldItem, newItem) { 48 | calendarItems.splice(oldItem.id - 1, 1); 49 | WTPLAN.addCalendarItem(calendarItems, newItem); 50 | }; 51 | 52 | WTPLAN.removeCalendarItem = function(calendarItems, id) { 53 | calendarItems.splice(id - 1, 1); 54 | calendarItems.forEach(function(item, index) { 55 | item.id = index + 1; 56 | }); 57 | }; 58 | 59 | WTPLAN.dayStringFromDate = function(date) { 60 | var day = date.getDay(); 61 | if (day === 0) { 62 | return "Sunday"; 63 | } else if (day === 1) { 64 | return "Monday"; 65 | } else if (day === 2) { 66 | return "Tuesday"; 67 | } else if (day === 3) { 68 | return "Wednesday"; 69 | } else if (day === 4) { 70 | return "Thursday"; 71 | } else if (day === 5) { 72 | return "Friday"; 73 | } else if (day === 6) { 74 | return "Saturday"; 75 | } 76 | }; 77 | 78 | WTPLAN.getLocalStartOfDay = function(dateString) { 79 | var local = new Date(dateString); 80 | local.setMinutes(local.getMinutes() + local.getTimezoneOffset()); 81 | return local; 82 | }; 83 | 84 | 85 | WTPLAN.getLocalDayString = function(date) { 86 | var local; 87 | if (date === undefined) { 88 | local = new Date(); 89 | } else { 90 | local = new Date(date); 91 | } 92 | local.setMinutes(local.getMinutes() - local.getTimezoneOffset()); 93 | return local.toJSON().split("T")[0]; 94 | }; 95 | 96 | WTPLAN.dateToRfc3339String = function(dateParam) { 97 | var date; 98 | if (dateParam === undefined) { 99 | date = new Date(); 100 | } else { 101 | date = dateParam; 102 | } 103 | 104 | function pad(num) { 105 | var str = "" + num; 106 | var tmp = ('0' + num); 107 | return tmp.substring(str.length - 1, str.length + 1); 108 | } 109 | var offset = date.getTimezoneOffset(); 110 | var absOffset = Math.abs(offset); 111 | var offsetMinutes = absOffset % 60; 112 | var offsetHours = (absOffset - offsetMinutes) / 60; 113 | return ( 114 | date.getFullYear() + "-" + 115 | pad(date.getMonth() + 1) + "-" + 116 | pad(date.getDate()) + "T" + 117 | pad(date.getHours()) + ":" + 118 | pad(date.getMinutes()) + ":" + 119 | pad(date.getSeconds()) + 120 | (offset === 0 ? "Z" : 121 | ((offset < 0 ? "+" : "-") + 122 | pad(offsetHours) + ":" + 123 | pad(offsetMinutes)))); 124 | }; 125 | 126 | 127 | WTPLAN.template = function(string, varDict) { 128 | Object.keys(varDict).forEach(function(vari){ 129 | string = string.replace(new RegExp('\<%'+vari+'%\>', 'g'), varDict[vari]); 130 | }); 131 | return string; 132 | }; 133 | 134 | })(); 135 | -------------------------------------------------------------------------------- /src/wtplan/generate_version_str/generate_version_str.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Kjell Winblad (kjellwinblad@gmail.com, http://winsh.me) 2 | // License: MIT License, see the LICENSE file 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "os" 10 | "os/exec" 11 | "strings" 12 | ) 13 | 14 | 15 | func main() { 16 | cmd := exec.Command("git", "rev-parse", "HEAD") 17 | buf := new(bytes.Buffer) 18 | cmd.Stdout = buf 19 | cmd.Stdin = os.Stdin 20 | cmd.Stderr = new(bytes.Buffer) 21 | cmd.Dir = "." 22 | cmd.Run() 23 | out, err := os.Create("version.go") 24 | if err != nil { 25 | fmt.Println(err) 26 | } 27 | out.Write([]byte("package main\n\n")) 28 | commitId := strings.TrimSpace(buf.String()) 29 | versionStr := "var version = `" + `wtplan ` + os.Getenv("VERSIONSTR") 30 | if len(commitId) == 0 { 31 | versionStr = versionStr + "`\n" 32 | } else { 33 | versionStr = versionStr + ` 34 | 35 | git commit id = ` + commitId + "`" 36 | } 37 | out.Write([]byte(versionStr)) 38 | } 39 | -------------------------------------------------------------------------------- /src/wtplan/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Kjell Winblad (kjellwinblad@gmail.com, http://winsh.me) 2 | // License: MIT License, see the LICENSE file 3 | 4 | package main 5 | 6 | //go:generate go run "generate_version_str/generate_version_str.go" 7 | 8 | import ( 9 | "encoding/json" 10 | "flag" 11 | "fmt" 12 | "io/ioutil" 13 | "os" 14 | "os/exec" 15 | "os/user" 16 | "path/filepath" 17 | "regexp" 18 | "sort" 19 | "strconv" 20 | "strings" 21 | "time" 22 | ) 23 | 24 | var ( 25 | durationRegExp = regexp.MustCompile(`(?P\s*NA\s*)|\s*(?:(?P\d+)h)?\s*(?:(?P\d+)m)?\s*`) 26 | simpleDateRegExp = regexp.MustCompile(`\s*(?P\d\d\d\d)(?:-(?P\d\d)(?:-(?P\d\d)(?:T(?P\d\d)(?:\:(?P\d\d))?)?)?)?\s*`) 27 | defaultPostChangeCommandsWithGit = [][]string{[]string{"git", "add", "data.json"}, []string{"git", "commit", "-m", "Change!"}} 28 | defaultEditorCommands = []string{"nano"} 29 | customConfigDir = "" 30 | ) 31 | 32 | const ( 33 | calendarDataFileName = "data.json" 34 | configDataFileName = "config.json" 35 | configDirName = ".wtplan" 36 | ) 37 | 38 | type calendarItem struct { 39 | Date string `json:"date"` 40 | Duration string `json:"duration"` 41 | Description string `json:"description"` 42 | ID int `json:"id,omitempty"` 43 | } 44 | 45 | type config struct { 46 | EditorCommand []string `json:"editor_command"` 47 | PostChangeCommands [][]string `json:"post_change_commands"` 48 | AutoAddPostChangeCommandsIfGitRepo bool `json:"auto_add_post_change_commands_if_git_repo"` 49 | SyncCommands [][]string `json:"sync_commands"` 50 | } 51 | 52 | //Implements interface for sorting 53 | type byDate []calendarItem 54 | 55 | func (a byDate) Len() int { return len(a) } 56 | func (a byDate) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 57 | func (a byDate) Less(i, j int) bool { 58 | iTime := a[i].startDate() 59 | jTime := a[j].startDate() 60 | return iTime.Before(jTime) 61 | } 62 | 63 | //Helper methods for calendarItem 64 | func (c calendarItem) startDate() time.Time { 65 | t, _ := time.Parse(time.RFC3339, c.Date) 66 | t = t.Add(1*time.Microsecond) // Makes sure that a time will not happen between days 67 | t = t.In(time.Local) 68 | return t 69 | } 70 | 71 | func (c calendarItem) endDate() time.Time { 72 | match := durationRegExp.FindStringSubmatch(c.Duration) 73 | if match[1] == "NA" { 74 | return c.startDate() 75 | } 76 | hours, _ := strconv.Atoi(match[2]) 77 | minutes, _ := strconv.Atoi(match[3]) 78 | duration := time.Duration(time.Hour*time.Duration(hours) + time.Minute*time.Duration(minutes)) 79 | return c.startDate().Add(duration).In(time.Local) 80 | } 81 | 82 | //Error handling function 83 | func die(msg string) { 84 | fmt.Fprintf(os.Stderr, "%s\n", msg) 85 | os.Exit(1) 86 | } 87 | 88 | //Functions for handling storage and settings file 89 | func configDir() string { 90 | if customConfigDir != "" { 91 | return customConfigDir 92 | } else { 93 | usr, err := user.Current() 94 | if err != nil { 95 | die(fmt.Sprintf("Could not get home directory: %s", err.Error())) 96 | } 97 | return filepath.Join(usr.HomeDir, configDirName) 98 | } 99 | } 100 | 101 | func checkOrCreateConfigDir() { 102 | _, err := os.Stat(configDir()) 103 | if os.IsNotExist(err) { 104 | if nil != os.Mkdir(configDir(), os.ModeDir|0700) { 105 | die(fmt.Sprintf("Error when createing data dir: %s", err.Error())) 106 | } 107 | } else if err != nil { 108 | die(fmt.Sprintf("Error when checking existence of data dir: %s", err.Error())) 109 | } 110 | } 111 | 112 | func createOrReadFileInConfigDir(fileName string, defaultText []byte) []byte { 113 | _, err := os.Stat(filepath.Join(configDir(), fileName)) 114 | if os.IsNotExist(err) { 115 | err = ioutil.WriteFile(filepath.Join(configDir(), fileName), defaultText, 0600) 116 | if err != nil { 117 | die(fmt.Sprintf("Error when createing data file: %s", err.Error())) 118 | } 119 | } else if err != nil { 120 | die(fmt.Sprintf("Error when checking existence of data file: %s", err.Error())) 121 | } 122 | dataFileText, err := ioutil.ReadFile(filepath.Join(configDir(), fileName)) 123 | if err != nil { 124 | die(fmt.Sprintf("Error when reading file: %s", err.Error())) 125 | } 126 | return dataFileText 127 | } 128 | 129 | func createOrReadData() []calendarItem { 130 | checkOrCreateConfigDir() 131 | dataFileText := createOrReadFileInConfigDir(calendarDataFileName, []byte("[]")) 132 | calendarItems := make([]calendarItem, 0) 133 | err := json.Unmarshal(dataFileText, &calendarItems) 134 | if err != nil { 135 | die(fmt.Sprintf("Error when parsing data file: %s", err.Error())) 136 | } 137 | sort.Sort(byDate(calendarItems)) 138 | for i := range calendarItems { 139 | calendarItems[i].ID = i + 1 140 | } 141 | return calendarItems 142 | } 143 | 144 | func createOrReadConfig() config { 145 | checkOrCreateConfigDir() 146 | defaultConfig := config{ 147 | EditorCommand: defaultEditorCommands, 148 | PostChangeCommands: [][]string{}, 149 | AutoAddPostChangeCommandsIfGitRepo: true, 150 | SyncCommands: [][]string{}} 151 | b, _ := json.MarshalIndent(defaultConfig, "", " ") 152 | configData := createOrReadFileInConfigDir(configDataFileName, b) 153 | conf := config{} 154 | json.Unmarshal(configData, &conf) 155 | _, isGitDirErr := os.Stat(filepath.Join(configDir(), ".git")) 156 | if isGitDirErr == nil && 157 | conf.AutoAddPostChangeCommandsIfGitRepo && 158 | len(conf.PostChangeCommands) == 0 { 159 | conf.PostChangeCommands = defaultPostChangeCommandsWithGit 160 | b, _ := json.MarshalIndent(conf, "", " ") 161 | err := ioutil.WriteFile(filepath.Join(configDir(), configDataFileName), b, 0600) 162 | if err != nil { 163 | die(fmt.Sprintf("Error when editing config file: %s", err.Error())) 164 | } 165 | } 166 | return conf 167 | } 168 | 169 | func saveCalendarItems(calendarItems []calendarItem) { 170 | for i := range calendarItems { 171 | calendarItems[i].ID = 0 // sets to empty value 172 | } 173 | b, _ := json.MarshalIndent(calendarItems, "", " ") 174 | err := ioutil.WriteFile(filepath.Join(configDir(), calendarDataFileName), b, 0600) 175 | if err != nil { 176 | die(fmt.Sprintf("Error when writting data file: %s", err.Error())) 177 | } 178 | } 179 | 180 | func addCalendarItem(items []calendarItem, item calendarItem) []calendarItem { 181 | items = append(items, item) 182 | sort.Sort(byDate(items)) 183 | return items 184 | } 185 | 186 | //Functions for handling commands 187 | 188 | func printReport(startDate time.Time, days int) { 189 | startDate = time.Date(startDate.Year(), startDate.Month(), startDate.Day()-1, 0, 0, 0, 0, time.Local) 190 | endDate := startDate.AddDate(0, 0, days) 191 | items := createOrReadData() 192 | itemsToReport := []calendarItem{} 193 | for _, item := range items { 194 | if item.startDate().After(startDate) && item.startDate().Before(endDate) { 195 | itemsToReport = append(itemsToReport, item) 196 | } 197 | } 198 | day := startDate.AddDate(0, 0, 1) 199 | for day.Before(endDate) && !day.Equal(endDate) { 200 | nextDay := day.AddDate(0, 0, 1) 201 | weekdays := []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} 202 | fmt.Printf("=== %s%s %d-%02d-%02d ===\n", weekdays[day.Weekday()], " "[:9-len(weekdays[day.Weekday()])], day.Year(), day.Month(), day.Day()) 203 | for _, item := range itemsToReport { 204 | if (item.startDate().After(day) || item.startDate().Equal(day)) && item.startDate().Before(nextDay) && 205 | (item.endDate().After(day) || item.endDate().Equal(day)) && item.endDate().Before(nextDay) { 206 | //Both end and start date are inside day 207 | if item.startDate().Equal(item.endDate()) { 208 | fmt.Printf("%02d:%02d ID: %d\n", item.startDate().Hour(), item.startDate().Minute(), item.ID) 209 | } else { 210 | fmt.Printf("%02d:%02d-%02d:%02d (Duration %s) ID: %d\n", item.startDate().Hour(), item.startDate().Minute(), item.endDate().Hour(), item.endDate().Minute(), item.Duration, item.ID) 211 | } 212 | fmt.Printf("%s\n", item.Description) 213 | } else if (item.startDate().After(day) || item.startDate().Equal(day)) && item.startDate().Before(nextDay) { 214 | // Start date in day 215 | fmt.Printf("%02d:%02d-(->) (Duration %s) ID: %d\n", item.startDate().Hour(), item.startDate().Minute(), item.Duration, item.ID) 216 | fmt.Printf("%s\n", item.Description) 217 | } else if (item.endDate().After(day) || item.endDate().Equal(day)) && item.endDate().Before(nextDay) { 218 | // End date in day 219 | fmt.Printf("(<-)-%02d:%02d (Duration %s) ID: %d\n", item.endDate().Hour(), item.endDate().Minute(), item.Duration, item.ID) 220 | fmt.Printf("%s\n", item.Description) 221 | } else if item.startDate().Before(day) && (item.endDate().After(nextDay) || item.endDate().Equal(nextDay)) { 222 | //Start date before day and end date after day 223 | fmt.Printf("(<-)-(->) (Duration %s) ID: %d\n", item.Duration, item.ID) 224 | fmt.Printf("%s\n", item.Description) 225 | } 226 | } 227 | day = nextDay 228 | } 229 | } 230 | 231 | func runCommands(commands [][]string) error { 232 | for _, command := range commands { 233 | cmd := exec.Command(command[0], command[1:]...) 234 | cmd.Stdout = os.Stdout 235 | cmd.Stdin = os.Stdin 236 | cmd.Stderr = os.Stderr 237 | cmd.Dir = configDir() 238 | err := cmd.Run() 239 | if err != nil { 240 | return err 241 | } 242 | } 243 | return nil 244 | } 245 | 246 | func checkDurationString(str string) { 247 | match := durationRegExp.FindStringSubmatch(str) 248 | if len(match) == 0 { 249 | die(fmt.Sprintf(`The duration string "%s" is not formatted correctly. 250 | Examples of correctly formatted duration strings are "NA", "2h", "10m" and "2h10m"`, str)) 251 | } 252 | } 253 | 254 | func getCalendarItemFromEditor(initialDateString string, initialDuration string, initialDescription string) calendarItem { 255 | tmpfile, err := ioutil.TempFile(configDir(), "ADD_CALENDAR_ITEM_TEMP") 256 | if err != nil { 257 | die(fmt.Sprintf("Error when creating temp file %s", err.Error())) 258 | } 259 | content := []byte( 260 | fmt.Sprintf(`Timepoint (RFC3339): 261 | %s 262 | ################################## 263 | Duration (NA no duration, 1h30m = 1 hour and 30 minutes): 264 | %s 265 | ################################## 266 | Description (Write on next line): 267 | %s`, initialDateString, initialDuration, initialDescription)) 268 | _, err = tmpfile.Write(content) 269 | if err != nil { 270 | die(fmt.Sprintf("Error when writting temp file: %s", err.Error())) 271 | } 272 | err = tmpfile.Close() 273 | if err != nil { 274 | die(fmt.Sprintf("Error when closing temp file: %s", err.Error())) 275 | } 276 | editorCommand := append(createOrReadConfig().EditorCommand, tmpfile.Name()) 277 | err = runCommands([][]string{editorCommand}) 278 | if err != nil { 279 | die(fmt.Sprintf(`Error when starting editor command. 280 | Note that you can change the editor command in %s: %s`, configDir(), err.Error())) 281 | } 282 | content, err = ioutil.ReadFile(tmpfile.Name()) 283 | if err != nil { 284 | die(fmt.Sprintf("Error when reading temp file: %s", err.Error())) 285 | } 286 | err = os.Remove(tmpfile.Name()) 287 | if err != nil { 288 | die(fmt.Sprintf("Error when removing temp file: %s", err.Error())) 289 | } 290 | stringContent := string(content) 291 | stringContentLines := strings.Split(stringContent, "\n") 292 | dateString := stringContentLines[1] 293 | _, err = time.Parse(time.RFC3339, dateString) 294 | if err != nil { 295 | die(fmt.Sprintf(`Could not parse datetime string "%s". 296 | RFC 3339 strings are accepted. 297 | %s`, dateString, err.Error())) 298 | } 299 | durationString := stringContentLines[4] 300 | checkDurationString(durationString) 301 | descriptionString := strings.Join(stringContentLines[7:], "\n") 302 | if descriptionString == "" { 303 | die("Canceling add because description string is empty.") 304 | } 305 | item := calendarItem{ 306 | Date: dateString, 307 | Duration: durationString, 308 | Description: descriptionString} 309 | return item 310 | } 311 | 312 | func runPostChangeCommands() { 313 | commands := createOrReadConfig().PostChangeCommands 314 | err := runCommands(commands) 315 | if err != nil { 316 | die("Error when running post change commands") 317 | } 318 | } 319 | 320 | func addCommand(args []string) { 321 | var dateString string 322 | if len(args) == 0 { 323 | dateString = time.Now().In(time.Local).Format(time.RFC3339) 324 | } else { 325 | dateString = timeFromDateString(args[0]).Format(time.RFC3339) 326 | } 327 | durationString := "NA" 328 | if len(args) >= 2 { 329 | durationString = args[1] 330 | checkDurationString(durationString) 331 | } 332 | var item calendarItem 333 | if len(args) >= 3 { 334 | item = calendarItem{Date: dateString, Duration: durationString, Description: args[2]} 335 | } else { 336 | item = getCalendarItemFromEditor(dateString, durationString, "") 337 | } 338 | saveCalendarItems(addCalendarItem(createOrReadData(), item)) 339 | runPostChangeCommands() 340 | } 341 | 342 | func timeFromDateString(str string) time.Time { 343 | rfc3339Time, err := time.Parse(time.RFC3339, str) 344 | if err == nil { 345 | return rfc3339Time 346 | } 347 | match := simpleDateRegExp.FindStringSubmatch(str) 348 | if len(match) == 0 { 349 | die(fmt.Sprintf(`Can not parse date %s. 350 | Examples of correctly formatted dates are "2016", "2016-10", "2016-10-20", 351 | "2016-10-20T13, "2016-10-20T13:10" and "2016-11-13T19:05:22+01:00" (RFC3339).", `, str)) 352 | } 353 | year, _ := strconv.Atoi(match[1]) 354 | month, _ := strconv.Atoi(match[2]) 355 | if month == 0 { 356 | month = month + 1 357 | } 358 | day, _ := strconv.Atoi(match[3]) 359 | if day == 0 { 360 | day = day + 1 361 | } 362 | hour, _ := strconv.Atoi(match[4]) 363 | minute, _ := strconv.Atoi(match[5]) 364 | return time.Date(year, 365 | time.Month(month), 366 | day, hour, minute, 0, 0, time.Local) 367 | } 368 | 369 | func showCommand(args []string) { 370 | if len(args) == 0 { 371 | printReport(time.Now(), 10) 372 | } else if len(args) == 1 { 373 | if args[0] == "ALLJSON" { 374 | items := createOrReadData() 375 | for i := range items { 376 | items[i].ID = 0 // sets to empty value 377 | } 378 | b, _ := json.MarshalIndent(items, "", " ") 379 | fmt.Print(string(b)) 380 | return 381 | } 382 | days, err := strconv.Atoi(args[0]) 383 | if err == nil { 384 | printReport(time.Now(), days) 385 | } else { 386 | printReport(timeFromDateString(args[0]), 10) 387 | } 388 | } else { 389 | days, err := strconv.Atoi(args[1]) 390 | if err != nil { 391 | die(fmt.Sprintf("The number of days argument %s is not an integer", args[1])) 392 | } 393 | printReport(timeFromDateString(args[0]), days) 394 | } 395 | } 396 | 397 | func removeItems(calendarItems []calendarItem, ids []int) []calendarItem { 398 | idsToRemove := make(map[int]bool) 399 | for _, id := range ids { 400 | idsToRemove[id] = true 401 | } 402 | itemsToSave := []calendarItem{} 403 | for i := range calendarItems { 404 | if !idsToRemove[i+1] { 405 | itemsToSave = append(itemsToSave, calendarItems[i]) 406 | } 407 | } 408 | return itemsToSave 409 | } 410 | 411 | func removeCommand(args []string) { 412 | items := createOrReadData() 413 | idsToRemove := []int{} 414 | for _, item := range args { 415 | id, err := strconv.Atoi(item) 416 | if err != nil { 417 | die(fmt.Sprintf("Can not parse argument %s as integer", item)) 418 | } 419 | if id < 1 || id > len(items) { 420 | die(fmt.Sprintf("The ID %d is outside the range", id)) 421 | } 422 | idsToRemove = append(idsToRemove, id) 423 | } 424 | saveCalendarItems(removeItems(items, idsToRemove)) 425 | runPostChangeCommands() 426 | } 427 | 428 | func editCommand(args []string) { 429 | if len(args) == 0 { 430 | die("No ID passed to the edit command") 431 | } 432 | id, err := strconv.Atoi(args[0]) 433 | if err != nil { 434 | die(fmt.Sprintf("The ID %s passed to edit is not an integer", args[0])) 435 | } 436 | items := createOrReadData() 437 | if id < 1 || id > len(items) { 438 | die(fmt.Sprintf("The ID %d is out of range", id)) 439 | } 440 | itemToEdit := items[id-1] 441 | newItem := getCalendarItemFromEditor(itemToEdit.Date, itemToEdit.Duration, itemToEdit.Description) 442 | items = removeItems(items, []int{id}) 443 | saveCalendarItems(addCalendarItem(items, newItem)) 444 | runPostChangeCommands() 445 | } 446 | 447 | func gitCommand(args []string) { 448 | command := append([]string{"git"}, args...) 449 | runCommands([][]string{command}) 450 | } 451 | 452 | func webCommand(args []string) { 453 | command := append([]string{"wtplan-web"}, args...) 454 | runCommands([][]string{command}) 455 | } 456 | 457 | func printHelpText() { 458 | fmt.Println( 459 | `usage: wtplan [] 460 | 461 | The wtplan commands are: 462 | show Show calendar items. Examples: 463 | wtplan show 10 (shows today and the following 9 days) 464 | wtplan show 2016-10-30 10 (shows 2016-10-30 and the following 9 days) 465 | wtplan show 2016-10 10 (shows 2016-10-01 and the following 9 days) 466 | wtplan show 2016 365 (shows 2016-01-01 and the following 364 days) 467 | add Add calendar item. Examples: 468 | wtplan add (adds an item using the editor -- see ~/wtplan/config.json) 469 | wtplan add 2016-10-30 (adds an item using the editor) 470 | wtplan add 2016-10-30 NA (adds an item using the editor) 471 | wtplan add 2016-10-30T14:10 1h10m "table tennis" (adds an item) 472 | remove Remove calendar item. Examples: 473 | wtplan remove 7 (removes item with ID 7, 474 | use the show command to show IDs of items) 475 | wtplan remove 7 5 3 (removes several items) 476 | edit Edits calendar item. Examples: 477 | wtplan edit 7 (edit item with ID 7 using editor) 478 | git Run git command in the calendar directory. Example: 479 | wtplan git remote add origin https://gitlab.com/user/calendar.git 480 | web Starts the wtplan web interface. Examples: 481 | wtplan web (Starts the web interface at the default port 8005) 482 | wtplan web --address :8700 --password mypass 483 | (Starts the web interface at port 8700 with password mypass) 484 | 485 | See man page for more detailed help.`) 486 | os.Exit(0) 487 | } 488 | 489 | func main() { 490 | helpPtr := flag.Bool("h", false, "get help") 491 | helpLongPtr := flag.Bool("help", false, "get help") 492 | versionPtr := flag.Bool("v", false, "show version information") 493 | versionLongPtr := flag.Bool("version", false, "show version information") 494 | configDirPtr := flag.String("c", "", "Set the configuration dir") 495 | configDirLongPtr := flag.String("config_dir", "", "Set the configuration dir") 496 | flag.Parse() 497 | if *helpPtr || *helpLongPtr { 498 | printHelpText() 499 | } 500 | if *versionPtr || *versionLongPtr { 501 | fmt.Println(version) 502 | os.Exit(0) 503 | } 504 | args := os.Args[1:] 505 | if *configDirLongPtr != ""{ 506 | customConfigDir = *configDirLongPtr 507 | args = args[2:] 508 | } 509 | if *configDirPtr != "" { 510 | customConfigDir = *configDirPtr 511 | args = args[2:] 512 | } 513 | if len(args) == 0 { 514 | printHelpText() 515 | } 516 | //Init config dir if it does not exist 517 | createOrReadConfig() 518 | //Handle commands 519 | switch args[0] { 520 | case "add": 521 | addCommand(args[1:]) 522 | case "show": 523 | showCommand(args[1:]) 524 | case "remove": 525 | removeCommand(args[1:]) 526 | case "edit": 527 | editCommand(args[1:]) 528 | case "git": 529 | gitCommand(args[1:]) 530 | case "web": 531 | webCommand(args[1:]) 532 | default: 533 | fmt.Printf("%q is not valid command.\n", os.Args[1]) 534 | os.Exit(2) 535 | } 536 | 537 | } 538 | -------------------------------------------------------------------------------- /wtplan.1: -------------------------------------------------------------------------------- 1 | .TH WTPLAN 1 2 | .SH NAME 3 | wtplan VERSION \- keeps track of events with a command line interface or a web interface 4 | .SH SYNOPSIS 5 | .B wtplan 6 | [[\fB\-\-version\fR] | [\fB\-h\fR] | [\fB\-\-help\fR] 7 | 8 | .B wtplan\fR [\fB\-c\fR \fI\fR] | [\fB\-\-config_dir\fR \fI\fR]] 9 | 10 | \fB show\fR [\fINUMBER_OF_DAYS\fR] | [\fIISO_DATE\fR [\fINUMBER_OF_DAYS\fR]] 11 | 12 | \fB add\fR [\fIISO_DATE\fR [\fIDURATION\fR [\fIDESCRIPTION\fR]]] 13 | 14 | \fB remove\fR \fIID\fR ... 15 | 16 | \fB edit\fR \fIID\fR 17 | 18 | \fB git\fR \fIGIT_PARAMETER\fR ... 19 | 20 | \fB web\fR [\fB\-\-address\fR \fIADDRESS\fR] [\fB\-\-password\fR \fIPASSWORD\fR] 21 | 22 | .SH DESCRIPTION 23 | .B wtplan 24 | manages a calendar with events. Every event has a start time, a duration and a description. 25 | \fBwtplan\fR provides both a command line interface (see section \fBCOMMAND LINE INTERFACE\fR) as well as web based interface (see section \fBWEB INTERFACE\fR). 26 | \fBwtplan\fR integrates with git to make it easy to keep calendar data in-sync between several computers (see section \fBGIT INTEGRATION\fR). 27 | The calendar data as well as configuration files are by default located in \fI~/.wtplan\fR but this directory can be changed with the \fB\-c\fR or \fB\-\-config_dir\fR parameters. 28 | .SH OPTIONS 29 | .TP 30 | .BR \-h ", " \-\-help 31 | Shows the help text. 32 | .TP 33 | .BR \-\-version 34 | Shows version information 35 | .TP 36 | .BR \-c " " \fI\fR ", " \-\-config_dir " " \fI\fR 37 | Specifies a directory for the configuration file and the data file other than the default \fI~/.wtplan\fR. 38 | 39 | .SH COMMAND LINE INTERFACE 40 | The command line interface makes it possible to show and modify the calendar data from a command line console. 41 | The \fBadd\fR command and the \fBedit\fR command may open an editor to edit the calendar item if not enough arguments are specified. 42 | By default the editor nano is used but this can be changed using the configuration variable \fIeditor_command\fR in \fI~/wtplan/config.json\fR. 43 | 44 | The wtplan commands are: 45 | \fBshow\fR Show calendar items. Examples: 46 | \fBwtplan show 10\fR (shows today and the following 9 days) 47 | \fBwtplan show 2016-10-30 10\fR (shows 2016-10-30 and the following 9 days) 48 | \fBwtplan show 2016-10 10\fR (shows 2016-10-01 and the following 9 days) 49 | \fBwtplan show 2016 365\fR (shows 2016-01-01 and the following 364 days) 50 | \fBadd\fR Add calendar item. Examples: 51 | \fBwtplan add\fR (adds an item using the editor -- see ~/wtplan/config.json) 52 | \fBwtplan add 2016-10-30\fR (adds an item using the editor) 53 | \fBwtplan add 2016-10-30 NA\fR (adds an item using the editor) 54 | \fBwtplan add 2016-10-30T14:10 1h10m "table tennis"\fR (adds an item) 55 | \fBremove\fR Remove calendar item. Examples: 56 | \fBwtplan remove 7\fR (removes item with ID 7, 57 | use the show command to show IDs of items) 58 | \fBwtplan remove 7 5 3\fR (removes several items) 59 | \fBedit\fR Edits calendar item. Examples: 60 | \fBwtplan edit 7\fR (edit item with ID 7 using editor) 61 | \fBgit\fR Run git command in the calendar directory. Example: 62 | \fBwtplan git remote add origin https://gitlab.com/user/calendar.git\fR 63 | \fBweb\fR Starts the wtplan web interface. Examples: 64 | \fBwtplan web\fR (Starts the web interface at the default port 8005) 65 | \fBwtplan web --address 127.0.0.1:8700 --password mypass\fR 66 | (Starts the web interface at port 8700 with password mypass) 67 | .SH WEB INTERFACE 68 | The web interface is started with the \fBwtplan web\fR command. 69 | By default the web interface listen to port \fI8005\fR. 70 | The web interface can then be accessed by pointing a web browser to the address \fIhttp://127.0.0.1:8005\fR. 71 | For example, the command \fBfirefox --new-window http://127.0.0.1:8005\fR will open the web interface using the firefox web browser if it is installed in your system. 72 | The port and address that the web interface listen to can be changed by specifying the \fB--address\fR option. 73 | The command \fBwtplan web --address 0.0.0.0:8700\fR will start the web interface on port 8700 instead of the default port, and make the web interface available from external machines. 74 | 75 | The \fB--password\fR option can be used to protect the web interface with a password. 76 | Running the command \fBwtplan web --password mySecretPass\fR will start the web interface protected by the password \fImySecretPass\fR. 77 | .SH GIT INTEGRATION 78 | The purpose of the git integration is to make it more convenient to synchronize the calendar data between several computers using the git version control system. 79 | A prerequirement for using the git integration is that git is installed on your system. 80 | 81 | To initiate a git repository for your calendar data, run the command \fBwtplan git init\fR. 82 | This will create a git repository in the default wtplan configuration folder \fI~/.wtplan\fR. 83 | Typically, you want to synchronize your calendar data with a remote repository. 84 | To set this up run \fBwtplan git remote add origin yourRemoteGitRepo\fR. 85 | By default, wtplan will add and commit changes to the calendar data to the repository if it detects that \fI~/.wtplan\fR is a git repository. 86 | For example, running the command \fBwtplan add 2016-10-30T14:10 1h10m "table tennis"\fR will run \fBgit add data.json\fR and \fBgit commit -m "Change!"\fR in the \fI~/.wtplan\fR folder after adding the event. 87 | Note that the configuration variable \fIpost_change_commands\fR in \fI~/.wtplan/config.json\fR will be changed to include those two git commands automatically when the first calendar change is done after initializing a git repository in the configuration directory if the configuration variable \fIauto_add_post_change_commands_if_git_repo\fR is set to \fBtrue\fR. 88 | The \fIpost_change_commands\fR variable can then be modified if one wants to run other commands when the calendar data is changed. 89 | For example, setting the \fIpost_change_commands\fR variable to \fI[["git", "add", "data.json"], ["git", "commit", "-m", "Change!"], ["git", "push"]]\fR will also run the \fBgit push\fR command after committing the change. 90 | 91 | .SH AUTHOR 92 | 93 | Kjell Winblad (kjellwinblad@gmail.com, http://winsh.me) 94 | 95 | .SH REPORTING BUGS 96 | 97 | Bugs can be reported at the projects github page 98 | 99 | .SH COPYRIGHT 100 | 101 | Copyright © 2017 Kjell Winblad. License MIT . 102 | --------------------------------------------------------------------------------