NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @namespace https://openuserjs.org/users/SamLeatherdale // @name Harvest Sorter // @description Adds sorting and aggregate time features to Harvest. // @license GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt // @version 2.4.4 // @updateURL https://openuserjs.org/meta/SamLeatherdale/Harvest_Sorter.meta.js // @match https://*.harvestapp.com/time* // @grant none // ==/UserScript== // ==OpenUserJS== // @author SamLeatherdale // ==/OpenUserJS== /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); /******/ } /******/ }; /******/ /******/ // define __esModule on exports /******/ __webpack_require__.r = function(exports) { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }); /******/ }; /******/ /******/ // create a fake namespace object /******/ // mode & 1: value is a module id, require it /******/ // mode & 2: merge all properties of value into the ns /******/ // mode & 4: return value when already ns object /******/ // mode & 8|1: behave like require /******/ __webpack_require__.t = function(value, mode) { /******/ if(mode & 1) value = __webpack_require__(value); /******/ if(mode & 8) return value; /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; /******/ var ns = Object.create(null); /******/ __webpack_require__.r(ns); /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); /******/ return ns; /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = "./src/harvest.user.js"); /******/ }) /************************************************************************/ /******/ ({ /***/ "./node_modules/css-loader/dist/cjs.js!./node_modules/sass-loader/lib/loader.js!./src/style.scss": /*!*******************************************************************************************************!*\ !*** ./node_modules/css-loader/dist/cjs.js!./node_modules/sass-loader/lib/loader.js!./src/style.scss ***! \*******************************************************************************************************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { exports = module.exports = __webpack_require__(/*! ../node_modules/css-loader/dist/runtime/api.js */ "./node_modules/css-loader/dist/runtime/api.js")(false); // Module exports.push([module.i, ".js-new-placeholder-entry .hui-button {\n background: #00c7ff; }\n\n#new-placeholder-entry-dialog .hui-button-primary {\n background: #00c7ff; }\n\n#day-view-control-row {\n display: flex;\n align-items: center;\n padding: 16px; }\n #day-view-control-row label:first-child, #day-view-control-row label:first-child input {\n font-size: 20px; }\n\nul.day-view-entry-list {\n display: flex;\n flex-direction: column; }\n\nli.day-view-entry {\n position: relative; }\n li.day-view-entry.dragstart {\n background-color: #ccc; }\n li.day-view-entry.dragstart td.timesheet-entry-sort-cell {\n background: transparent; }\n li.day-view-entry.dragover * {\n pointer-events: none; }\n li.day-view-entry.dragover::after {\n content: '';\n position: absolute;\n width: 100%;\n height: 100%;\n z-index: 5;\n top: -1px;\n background-color: rgba(204, 204, 204, 0.5);\n border: 3px solid #00c7ff;\n border-radius: 8px; }\n li.day-view-entry table {\n table-layout: fixed;\n /* td.entry-time-start {\r\n &::after {\r\n content: '-';\r\n position: relative;\r\n left: $cell_padding;\r\n }\r\n } */ }\n li.day-view-entry table td {\n padding-left: 8px;\n padding-right: 8px; }\n li.day-view-entry table td.timesheet-entry-sort-cell {\n cursor: move;\n background: linear-gradient(90deg, white 8px, transparent 1%) center, linear-gradient(white 8px, transparent 1%) center, #848484;\n background-size: 10px 10px;\n width: 35px;\n vertical-align: middle;\n background-position: 0px 0px; }\n li.day-view-entry table td.timesheet-entry-sort-cell input {\n width: 100%;\n text-align: center;\n background-color: white;\n color: #555;\n font-size: 18px;\n z-index: -1;\n cursor: move; }\n li.day-view-entry table td.timesheet-entry-sort-cell input:focus {\n outline: none; }\n li.day-view-entry table td:nth-child(2) {\n padding-left: 16px; }\n li.day-view-entry table td:nth-child(2) .entry-info {\n width: auto; }\n li.day-view-entry table td:nth-child(2) .entry-info .task-notes .task, li.day-view-entry table td:nth-child(2) .entry-info .task-notes .ndash {\n float: none; }\n li.day-view-entry table td:nth-child(2) .entry-info .task-notes .ndash {\n padding: 0; }\n li.day-view-entry table td:nth-child(2) .entry-info .task-notes .notes {\n display: block; }\n li.day-view-entry table td.entry-time, li.day-view-entry table td.entry-time-extra {\n font-size: 20px;\n font-weight: 500;\n text-align: right;\n vertical-align: middle; }\n li.day-view-entry table td.entry-time, li.day-view-entry table td.entry-time-total {\n width: 70px; }\n li.day-view-entry table td.entry-time-total {\n color: green; }\n li.day-view-entry table td.entry-time-end {\n width: 100px;\n font-weight: normal;\n padding-right: 16px; }\n li.day-view-entry table td.entry-time-end span.small {\n font-size: 12px; }\n", ""]); /***/ }), /***/ "./node_modules/css-loader/dist/runtime/api.js": /*!*****************************************************!*\ !*** ./node_modules/css-loader/dist/runtime/api.js ***! \*****************************************************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ // css base code, injected by the css-loader // eslint-disable-next-line func-names module.exports = function (useSourceMap) { var list = []; // return the list of modules as css string list.toString = function toString() { return this.map(function (item) { var content = cssWithMappingToString(item, useSourceMap); if (item[2]) { return "@media ".concat(item[2], "{").concat(content, "}"); } return content; }).join(''); }; // import a list of modules into the list // eslint-disable-next-line func-names list.i = function (modules, mediaQuery) { if (typeof modules === 'string') { // eslint-disable-next-line no-param-reassign modules = [[null, modules, '']]; } var alreadyImportedModules = {}; for (var i = 0; i < this.length; i++) { // eslint-disable-next-line prefer-destructuring var id = this[i][0]; if (id != null) { alreadyImportedModules[id] = true; } } for (var _i = 0; _i < modules.length; _i++) { var item = modules[_i]; // skip already imported module // this implementation is not 100% perfect for weird media query combinations // when a module is imported multiple times with different media queries. // I hope this will never occur (Hey this way we have smaller bundles) if (item[0] == null || !alreadyImportedModules[item[0]]) { if (mediaQuery && !item[2]) { item[2] = mediaQuery; } else if (mediaQuery) { item[2] = "(".concat(item[2], ") and (").concat(mediaQuery, ")"); } list.push(item); } } }; return list; }; function cssWithMappingToString(item, useSourceMap) { var content = item[1] || ''; // eslint-disable-next-line prefer-destructuring var cssMapping = item[3]; if (!cssMapping) { return content; } if (useSourceMap && typeof btoa === 'function') { var sourceMapping = toComment(cssMapping); var sourceURLs = cssMapping.sources.map(function (source) { return "/*# sourceURL=".concat(cssMapping.sourceRoot).concat(source, " */"); }); return [content].concat(sourceURLs).concat([sourceMapping]).join('\n'); } return [content].join('\n'); } // Adapted from convert-source-map (MIT) function toComment(sourceMap) { // eslint-disable-next-line no-undef var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))); var data = "sourceMappingURL=data:application/json;charset=utf-8;base64,".concat(base64); return "/*# ".concat(data, " */"); } /***/ }), /***/ "./node_modules/style-loader/lib/addStyles.js": /*!****************************************************!*\ !*** ./node_modules/style-loader/lib/addStyles.js ***! \****************************************************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { /* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ var stylesInDom = {}; var memoize = function (fn) { var memo; return function () { if (typeof memo === "undefined") memo = fn.apply(this, arguments); return memo; }; }; var isOldIE = memoize(function () { // Test for IE <= 9 as proposed by Browserhacks // @see http://browserhacks.com/#hack-e71d8692f65334173fee715c222cb805 // Tests for existence of standard globals is to allow style-loader // to operate correctly into non-standard environments // @see https://github.com/webpack-contrib/style-loader/issues/177 return window && document && document.all && !window.atob; }); var getTarget = function (target, parent) { if (parent){ return parent.querySelector(target); } return document.querySelector(target); }; var getElement = (function (fn) { var memo = {}; return function(target, parent) { // If passing function in options, then use it for resolve "head" element. // Useful for Shadow Root style i.e // { // insertInto: function () { return document.querySelector("#foo").shadowRoot } // } if (typeof target === 'function') { return target(); } if (typeof memo[target] === "undefined") { var styleTarget = getTarget.call(this, target, parent); // Special case to return head of iframe instead of iframe itself if (window.HTMLIFrameElement && styleTarget instanceof window.HTMLIFrameElement) { try { // This will throw an exception if access to iframe is blocked // due to cross-origin restrictions styleTarget = styleTarget.contentDocument.head; } catch(e) { styleTarget = null; } } memo[target] = styleTarget; } return memo[target] }; })(); var singleton = null; var singletonCounter = 0; var stylesInsertedAtTop = []; var fixUrls = __webpack_require__(/*! ./urls */ "./node_modules/style-loader/lib/urls.js"); module.exports = function(list, options) { if (typeof DEBUG !== "undefined" && DEBUG) { if (typeof document !== "object") throw new Error("The style-loader cannot be used in a non-browser environment"); } options = options || {}; options.attrs = typeof options.attrs === "object" ? options.attrs : {}; // Force single-tag solution on IE6-9, which has a hard limit on the # of <style> // tags it will allow on a page if (!options.singleton && typeof options.singleton !== "boolean") options.singleton = isOldIE(); // By default, add <style> tags to the <head> element if (!options.insertInto) options.insertInto = "head"; // By default, add <style> tags to the bottom of the target if (!options.insertAt) options.insertAt = "bottom"; var styles = listToStyles(list, options); addStylesToDom(styles, options); return function update (newList) { var mayRemove = []; for (var i = 0; i < styles.length; i++) { var item = styles[i]; var domStyle = stylesInDom[item.id]; domStyle.refs--; mayRemove.push(domStyle); } if(newList) { var newStyles = listToStyles(newList, options); addStylesToDom(newStyles, options); } for (var i = 0; i < mayRemove.length; i++) { var domStyle = mayRemove[i]; if(domStyle.refs === 0) { for (var j = 0; j < domStyle.parts.length; j++) domStyle.parts[j](); delete stylesInDom[domStyle.id]; } } }; }; function addStylesToDom (styles, options) { for (var i = 0; i < styles.length; i++) { var item = styles[i]; var domStyle = stylesInDom[item.id]; if(domStyle) { domStyle.refs++; for(var j = 0; j < domStyle.parts.length; j++) { domStyle.parts[j](item.parts[j]); } for(; j < item.parts.length; j++) { domStyle.parts.push(addStyle(item.parts[j], options)); } } else { var parts = []; for(var j = 0; j < item.parts.length; j++) { parts.push(addStyle(item.parts[j], options)); } stylesInDom[item.id] = {id: item.id, refs: 1, parts: parts}; } } } function listToStyles (list, options) { var styles = []; var newStyles = {}; for (var i = 0; i < list.length; i++) { var item = list[i]; var id = options.base ? item[0] + options.base : item[0]; var css = item[1]; var media = item[2]; var sourceMap = item[3]; var part = {css: css, media: media, sourceMap: sourceMap}; if(!newStyles[id]) styles.push(newStyles[id] = {id: id, parts: [part]}); else newStyles[id].parts.push(part); } return styles; } function insertStyleElement (options, style) { var target = getElement(options.insertInto) if (!target) { throw new Error("Couldn't find a style target. This probably means that the value for the 'insertInto' parameter is invalid."); } var lastStyleElementInsertedAtTop = stylesInsertedAtTop[stylesInsertedAtTop.length - 1]; if (options.insertAt === "top") { if (!lastStyleElementInsertedAtTop) { target.insertBefore(style, target.firstChild); } else if (lastStyleElementInsertedAtTop.nextSibling) { target.insertBefore(style, lastStyleElementInsertedAtTop.nextSibling); } else { target.appendChild(style); } stylesInsertedAtTop.push(style); } else if (options.insertAt === "bottom") { target.appendChild(style); } else if (typeof options.insertAt === "object" && options.insertAt.before) { var nextSibling = getElement(options.insertAt.before, target); target.insertBefore(style, nextSibling); } else { throw new Error("[Style Loader]\n\n Invalid value for parameter 'insertAt' ('options.insertAt') found.\n Must be 'top', 'bottom', or Object.\n (https://github.com/webpack-contrib/style-loader#insertat)\n"); } } function removeStyleElement (style) { if (style.parentNode === null) return false; style.parentNode.removeChild(style); var idx = stylesInsertedAtTop.indexOf(style); if(idx >= 0) { stylesInsertedAtTop.splice(idx, 1); } } function createStyleElement (options) { var style = document.createElement("style"); if(options.attrs.type === undefined) { options.attrs.type = "text/css"; } if(options.attrs.nonce === undefined) { var nonce = getNonce(); if (nonce) { options.attrs.nonce = nonce; } } addAttrs(style, options.attrs); insertStyleElement(options, style); return style; } function createLinkElement (options) { var link = document.createElement("link"); if(options.attrs.type === undefined) { options.attrs.type = "text/css"; } options.attrs.rel = "stylesheet"; addAttrs(link, options.attrs); insertStyleElement(options, link); return link; } function addAttrs (el, attrs) { Object.keys(attrs).forEach(function (key) { el.setAttribute(key, attrs[key]); }); } function getNonce() { if (false) {} return __webpack_require__.nc; } function addStyle (obj, options) { var style, update, remove, result; // If a transform function was defined, run it on the css if (options.transform && obj.css) { result = typeof options.transform === 'function' ? options.transform(obj.css) : options.transform.default(obj.css); if (result) { // If transform returns a value, use that instead of the original css. // This allows running runtime transformations on the css. obj.css = result; } else { // If the transform function returns a falsy value, don't add this css. // This allows conditional loading of css return function() { // noop }; } } if (options.singleton) { var styleIndex = singletonCounter++; style = singleton || (singleton = createStyleElement(options)); update = applyToSingletonTag.bind(null, style, styleIndex, false); remove = applyToSingletonTag.bind(null, style, styleIndex, true); } else if ( obj.sourceMap && typeof URL === "function" && typeof URL.createObjectURL === "function" && typeof URL.revokeObjectURL === "function" && typeof Blob === "function" && typeof btoa === "function" ) { style = createLinkElement(options); update = updateLink.bind(null, style, options); remove = function () { removeStyleElement(style); if(style.href) URL.revokeObjectURL(style.href); }; } else { style = createStyleElement(options); update = applyToTag.bind(null, style); remove = function () { removeStyleElement(style); }; } update(obj); return function updateStyle (newObj) { if (newObj) { if ( newObj.css === obj.css && newObj.media === obj.media && newObj.sourceMap === obj.sourceMap ) { return; } update(obj = newObj); } else { remove(); } }; } var replaceText = (function () { var textStore = []; return function (index, replacement) { textStore[index] = replacement; return textStore.filter(Boolean).join('\n'); }; })(); function applyToSingletonTag (style, index, remove, obj) { var css = remove ? "" : obj.css; if (style.styleSheet) { style.styleSheet.cssText = replaceText(index, css); } else { var cssNode = document.createTextNode(css); var childNodes = style.childNodes; if (childNodes[index]) style.removeChild(childNodes[index]); if (childNodes.length) { style.insertBefore(cssNode, childNodes[index]); } else { style.appendChild(cssNode); } } } function applyToTag (style, obj) { var css = obj.css; var media = obj.media; if(media) { style.setAttribute("media", media) } if(style.styleSheet) { style.styleSheet.cssText = css; } else { while(style.firstChild) { style.removeChild(style.firstChild); } style.appendChild(document.createTextNode(css)); } } function updateLink (link, options, obj) { var css = obj.css; var sourceMap = obj.sourceMap; /* If convertToAbsoluteUrls isn't defined, but sourcemaps are enabled and there is no publicPath defined then lets turn convertToAbsoluteUrls on by default. Otherwise default to the convertToAbsoluteUrls option directly */ var autoFixUrls = options.convertToAbsoluteUrls === undefined && sourceMap; if (options.convertToAbsoluteUrls || autoFixUrls) { css = fixUrls(css); } if (sourceMap) { // http://stackoverflow.com/a/26603875 css += "\n/*# sourceMappingURL=data:application/json;base64," + btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))) + " */"; } var blob = new Blob([css], { type: "text/css" }); var oldSrc = link.href; link.href = URL.createObjectURL(blob); if(oldSrc) URL.revokeObjectURL(oldSrc); } /***/ }), /***/ "./node_modules/style-loader/lib/urls.js": /*!***********************************************!*\ !*** ./node_modules/style-loader/lib/urls.js ***! \***********************************************/ /*! no static exports found */ /***/ (function(module, exports) { /** * When source maps are enabled, `style-loader` uses a link element with a data-uri to * embed the css on the page. This breaks all relative urls because now they are relative to a * bundle instead of the current page. * * One solution is to only use full urls, but that may be impossible. * * Instead, this function "fixes" the relative urls to be absolute according to the current page location. * * A rudimentary test suite is located at `test/fixUrls.js` and can be run via the `npm test` command. * */ module.exports = function (css) { // get current location var location = typeof window !== "undefined" && window.location; if (!location) { throw new Error("fixUrls requires window.location"); } // blank or null? if (!css || typeof css !== "string") { return css; } var baseUrl = location.protocol + "//" + location.host; var currentDir = baseUrl + location.pathname.replace(/\/[^\/]*$/, "/"); // convert each url(...) /* This regular expression is just a way to recursively match brackets within a string. /url\s*\( = Match on the word "url" with any whitespace after it and then a parens ( = Start a capturing group (?: = Start a non-capturing group [^)(] = Match anything that isn't a parentheses | = OR \( = Match a start parentheses (?: = Start another non-capturing groups [^)(]+ = Match anything that isn't a parentheses | = OR \( = Match a start parentheses [^)(]* = Match anything that isn't a parentheses \) = Match a end parentheses ) = End Group *\) = Match anything and then a close parens ) = Close non-capturing group * = Match anything ) = Close capturing group \) = Match a close parens /gi = Get all matches, not the first. Be case insensitive. */ var fixedCss = css.replace(/url\s*\(((?:[^)(]|\((?:[^)(]+|\([^)(]*\))*\))*)\)/gi, function(fullMatch, origUrl) { // strip quotes (if they exist) var unquotedOrigUrl = origUrl .trim() .replace(/^"(.*)"$/, function(o, $1){ return $1; }) .replace(/^'(.*)'$/, function(o, $1){ return $1; }); // already a full url? no change if (/^(#|data:|http:\/\/|https:\/\/|file:\/\/\/|\s*$)/i.test(unquotedOrigUrl)) { return fullMatch; } // convert the url to a full url var newUrl; if (unquotedOrigUrl.indexOf("//") === 0) { //TODO: should we add protocol? newUrl = unquotedOrigUrl; } else if (unquotedOrigUrl.indexOf("/") === 0) { // path should be relative to the base url newUrl = baseUrl + unquotedOrigUrl; // already starts with '/' } else { // path should be relative to current directory newUrl = currentDir + unquotedOrigUrl.replace(/^\.\//, ""); // Strip leading './' } // send back the fixed url(...) return "url(" + JSON.stringify(newUrl) + ")"; }); // send back the fixed css return fixedCss; }; /***/ }), /***/ "./src/harvest.user.js": /*!*****************************!*\ !*** ./src/harvest.user.js ***! \*****************************/ /*! no exports provided */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony import */ var _style_scss__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./style.scss */ "./src/style.scss"); /* harmony import */ var _style_scss__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_style_scss__WEBPACK_IMPORTED_MODULE_0__); var FEATURE_PLACEHOLDERS_ENABLED = false; var UPDATE_ROW_INTERVAL = 1000; var KEY_SETTING_ROW_TIME_12HR = 'plugin.setting.row_time_12hr'; var SELECTOR_TIMESHEET_ORDER = '.timesheet-entry-sort'; var SELECTOR_ENTRY = '.day-view-entry'; var dragTarget = null; function getSortedRows() { var rows = $(SELECTOR_ENTRY); rows.sort(function (a, b) { a = parseInt($(a).find(SELECTOR_TIMESHEET_ORDER).val()); b = parseInt($(b).find(SELECTOR_TIMESHEET_ORDER).val()); return a == b ? 0 : a > b ? 1 : -1; }); return rows; } function timeToDecimal(timeObj) { return timeObj.hours + timeObj.minutes / 60; } function decimalToTime(decimal) { var hours = parseInt(decimal); var minutes = decimal - hours; return { hours: hours, minutes: parseInt(60 * minutes) }; } function timeToString(timeObj, twelveHour) { var hours = pad(timeObj.hours, 2); if (twelveHour) { hours = pad(timeObj.hours <= 12 ? timeObj.hours : timeObj.hours - 12, 2); } var minutes = pad(timeObj.minutes, 2); var extension = ""; if (twelveHour) { extension = "<span class='small'>" + (timeObj.hours <= 11 ? "AM" : "PM") + "</span>"; } return hours + ":" + minutes + extension; } // https://stackoverflow.com/questions/1267283/how-can-i-pad-a-value-with-leading-zeros/37327063 function pad(num, len) { return Array(len + 1 - num.toString().length).join('0') + num; } function getSavedStartWork() { var day = "plugin.startwork." + $(".day-view-entry-list").attr("data-test-day"); var time = localStorage.getItem(day); if (time !== null) { $("#start-work-time").val(time); } else { $("#start-work-time").val("09:00"); } } function onChangeStartWork() { var day = "plugin.startwork." + $(".day-view-entry-list").attr("data-test-day"); localStorage.setItem(day, $(this).val()); calculateTotals(); } function getSaved12Hr() { $("#row-time-12hr").prop('checked', localStorage.getItem(KEY_SETTING_ROW_TIME_12HR) === 'true'); } function onChange12Hr() { localStorage.setItem(KEY_SETTING_ROW_TIME_12HR, $("#row-time-12hr").is(":checked")); calculateTotals(); } function onChangeOrder() { var input = $(this); var last = input.data("last"); // Firstly, get the row that holds the sort value that we just switched to var swap = getPosition(input.val()); setPosition(swap, last); // Update that row to have this row's old value setPosition(input, input.val()); //Update this row's data and localStorage // Now, reorder sortRows(); } /** * Returns the input element that holds the specified position. * @param {number} pos */ function getPosition(pos) { return $(SELECTOR_TIMESHEET_ORDER).filter(function (i, el) { return $(el).data("last") == pos; }); } /** * Sets the position of the given element, and also sets the localStorage value. * @param {object} input jQuery object: element * @param {number} pos The new position to set. */ function setPosition(input, pos) { if (!input.is(SELECTOR_TIMESHEET_ORDER)) { throw new Error("Cannot set position: wrong element type (must be timesheet input element)."); } input.val(pos).data("last", pos); localStorage.setItem(getLocalStorageKey(input.data("id")), pos); } function getLocalStorageKey(id) { var pieces = id.split("_"); return "plugin.order." + pieces[pieces.length - 1]; } function onDragStartRow(e) { $(this).parents(SELECTOR_ENTRY).addClass("dragstart"); e.originalEvent.dataTransfer.setData("text/plain", $(this).parents("tr").attr("id")); e.originalEvent.dataTransfer.dropEffect = "move"; } function onDragOverRow(e) { e.preventDefault(); $(this).addClass("dragover"); } function onDragLeaveRow(e) { if (!$(e.target).is(this)) { return; } $(this).removeClass("dragover"); } function onDragEndRow(e) { $(SELECTOR_ENTRY).removeClass("dragover dragstart"); } function onDropRow(e) { e.preventDefault(); //Stop firefox from redirecting $(SELECTOR_ENTRY).removeClass("dragover dragstart"); var id = e.originalEvent.dataTransfer.getData("text/plain"); var original = $("#" + id).find(SELECTOR_TIMESHEET_ORDER); var originalPos = parseInt(original.val()); var swap = $(this).find(SELECTOR_TIMESHEET_ORDER); var swapPos = parseInt(swap.val()); var rows = getSortedRows().get(); console.log("".concat(originalPos, " -> ").concat(swapPos)); if (swapPos === originalPos) { //Dropped in the same place return; } if (swapPos > originalPos) { //Shuffle in-between down for (var i = swapPos; i > originalPos; i--) { var row = $(rows[i - 1]); //Because our index starts at 1 setPosition(row.find(SELECTOR_TIMESHEET_ORDER), i - 1); } } else { //Shuffle in-between up for (var _i = swapPos; _i < originalPos; _i++) { var _row = $(rows[_i - 1]); //Because our index starts at 1 setPosition(_row.find(SELECTOR_TIMESHEET_ORDER), _i + 1); } } setPosition(original, swapPos); sortRows(); } /** * Sorts the rows in the order defined by the checkboxes, * and then calls calculateTotals, since the cumulative totals are defined by the order. */ function sortRows() { var rows = getSortedRows(); rows.each(function (i, el) { $(el).css("order", i + 1); }); // Since the order has changed, we must recalculate totals calculateTotals(); } /** * @summary Assigns each row an order. * @description Firstly, the brower's localStorage is checked to see if there is an existing order. * If nothing is found (the row has just been added), then the next available order is assigned. * Then, if there is no order input element, one is added. * Finally, if any changes were made, the rows are resorted. */ function attachRows() { var records = getSortedRows().get(); var positions = []; // Get all records with IDs records = records.map(function (el) { var obj = { el: el, id: $(el).find("tr").attr("id") }; var storedOrder = localStorage.getItem(getLocalStorageKey(obj.id)); var order = parseInt(storedOrder); if (!isNaN(order)) { obj.order = order; } else { obj.order = null; } if (typeof obj.order === "number") { while (typeof positions[obj.order] !== "undefined") { obj.order++; } positions[obj.order] = obj.id; } return obj; }); // Fill in any missing IDs records.forEach(function (obj) { var i; var hasOrder = typeof obj.order === "number"; // If a row already has an order, we only want to see if we can shift it backwards // Otherwise, we want to see if we can place it anywhere in the array var stop = hasOrder ? obj.order : positions.length; for (i = 1; i < stop; i++) { if (typeof positions[i] === "undefined") { // This position hasn't been used if (hasOrder) { // We are moving this item back to fill a gap delete positions[obj.order]; } obj.order = i; positions[i] = obj.id; return; } } // If we still don't have an ID, the array wasn't long enough, // add another on the end if (typeof obj.order !== "number") { obj.order = i; positions[i] = obj.id; } }); // Loop through rows and add input if needed records.forEach(function (obj) { var record = $(obj.el); var input = record.find(SELECTOR_TIMESHEET_ORDER); if (!record.data("dragevents")) { //Add drag events record.on("dragover", onDragOverRow).on("dragleave", onDragLeaveRow).on("dragend", onDragEndRow).on("drop", onDropRow).data("dragevents", 1); } // Check if it has an input field if (input.length === 0) { // If not, add it var firstCell = record.find("td:first-child"); var newCell = $("<td class='timesheet-entry-sort-cell'>\n <input class='timesheet-entry-sort' type='text' min='1' readonly />\n </td>"); firstCell.before(newCell); // Add it to the DOM input = newCell.find("input"); input.change(onChangeOrder).data("id", obj.id); //Add drag and drop input.parent().attr("draggable", "true").on("dragstart", onDragStartRow); } var value = obj.order; setPosition(input, value); }); // Force update order once sortRows(); // If an item was deleted, all records need their max readjusted //$(SELECTOR_TIMESHEET_ORDER).attr("max", records.length); } /** * @summary Calculates the cumulative total, start time and end time for each row. * @description Cumulative time is ordered according to the user's defined order, which * is why this function must be called each time the order is updated. */ function calculateTotals() { // Include running total var rows = getSortedRows(); var startTimeString = $("#start-work-time").val(); var twelveHour = $("#row-time-12hr").is(":checked"); var startTimePieces = startTimeString.split(":"); var total = 0; var endTime = timeToDecimal({ hours: parseInt(startTimePieces[0]), minutes: parseInt(startTimePieces[1]) }); rows.each(function (i, el) { var row = $(el); var entryTime = row.find(".entry-time"); if (row.find(".entry-time-total").length === 0) { entryTime.after("\n\t\t\t\t<td class='entry-time-extra entry-time-total'></td>\n\t\t\t\t<td class='entry-time-extra entry-time-end'></td>\n\t\t\t"); } var thisTime = parseFloat(entryTime.text()); total += thisTime; row.find(".entry-time-total").text(total.toFixed(2)); endTime += thisTime; var thisEnd = decimalToTime(endTime); row.find(".entry-time-end").html(timeToString(thisEnd, twelveHour)); }); } function onMutateChildList(mutationsList, observer) { /* Summary of event types * Add item: addedNodes: [li.day-view-entry], removedNodes: [] * Edit item: addedNodes: [table, text], removedNodes: [table, text] * Remove item: addedNodes: [], removedNodes: [li.day-view-entry] */ var refreshRows = false; var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = mutationsList[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var mutation = _step.value; // The following section gets triggered when changing days, so disable for now /* const isDelete = mutation.addedNodes.length === 0 && mutation.removedNodes.length > 0; if (isDelete) { for (let removedNode of mutation.removedNodes) { if ($(removedNode).is("li.day-view-entry")) { let id = $(removedNode).find("tr").attr("id"); console.log("Removing data for deleted row " + id); localStorage.removeItem(id); } } } */ // Check if a refresh is required if (!refreshRows) { var allNodes = Array.from(mutation.addedNodes).concat(Array.from(mutation.removedNodes)); var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = allNodes[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var mutatedNode = _step2.value; if ($(mutatedNode).is("table, li.day-view-entry")) { console.info("Rows changed, updating table..."); refreshRows = true; break; } } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) { _iterator2["return"](); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } } } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator["return"] != null) { _iterator["return"](); } } finally { if (_didIteratorError) { throw _iteratorError; } } } if (refreshRows) { attachRows(); } } function onMutateAttributes(mutationsList, observer) { console.info("Day changed, updating start time..."); replaceQuote(); getSavedStartWork(); calculateTotals(); } function onClickCreatePlaceholder() { if ($("#new-placeholder-entry-dialog").length === 0) { $("body").append("\n <div class='hui-dialog-backdrop hui-dialog-open' id='new-placeholder-entry-dialog' role='dialog'>\n <div class='hui-dialog'>\n <h1 class='hui-dialog-title'>New Placeholder Entry</h1>\n <label class='hui-label inline-block mb-5'>Note: This information is stored locally on this computer, and is not sent to Harvest.</label>\n\n\n <form class='day-entry-editor'>\n <div class=\"hui-form-field mb-10 duration\">\n\n <div class=\"notes-container\">\n <textarea name=\"notes\" placeholder=\"Notes (optional)\" class=\"hui-input entry-notes js-notes\"></textarea>\n </div>\n\n <input type=\"text\" name=\"hours\" placeholder=\"0.00\" class=\"hui-input js-hours hours-input\" value=\"\">\n\n <div class=\"js-validation-error-placeholder hours-validation-error\" data-for=\"hours\"></div>\n </div>\n\n <div class=\"hui-form-field-actions mt-20 js-form-buttons\">\n <button type=\"button\" class=\"hui-button hui-button-large hui-button-primary js-submit\">Save</button>\n <button type=\"button\" class=\"hui-button hui-button-large hui-button-cancel js-close\">Cancel</button>\n </div>\n </form>\n </div>\n </div>\n "); var dialog = $("#new-placeholder-entry-dialog"); dialog.find(".js-submit").click(function () { var data = { notes: dialog.find("textarea").val(), time: dialog.find(".hours-input").val() }; createPlaceholderRow(data); dialog.remove(); }); dialog.find(".js-close").click(function () { return dialog.remove(); }); } } function createPlaceholderRow(data) { var row = $(SELECTOR_ENTRY).last().clone(); row.insertAfter($(SELECTOR_ENTRY).last()); row.removeClass(function (i, className) { return className.startsWith("test-entry"); }); row.find("tr").prop("id", ""); row.find(".project-client").text("Placeholder"); row.find(".task-notes").text(data.notes); row.find(".entry-time").text(data.time); row.find(".entry-button, .edit-button").empty(); } function createPlaceholderButton() { var newTimeEntryContainer = $(".new-time-entry-container"); var button = newTimeEntryContainer.children("button").clone().appendTo(newTimeEntryContainer); button.css("top", button.height() + 10 + "px"); button.removeClass("js-new-time-entry test-new-time-entry").addClass("js-new-placeholder-entry"); button.find(".mt-5").text("New Placeholder"); button.click(onClickCreatePlaceholder); } /** * @param min Inclusive minimum * @param max Exclusive maximum * @returns {number} */ function getRandomInt(min, max) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min)) + min; //The maximum is exclusive and the minimum is inclusive } function replaceQuote() { if (window.location.hostname.search(/^emoti/i) === -1) { return; } var quoteContainer = $(".day-view-table .hui-empty .do-not-print"); if (!quoteContainer.length) { return; } var shouldUpdate = getRandomInt(0, 2) === 0; var quotes = [["Both", "Optus ad guy"], ["Regular or chicken salt?", "See Tucker & Eat It owner"], ["Morning joke!", "Dragan"], ["Scott Adams said...", "Dragan"], ["Rogan Josh", "Jim"], ["Strange", "Tom"], ["Want a breadstick?", "Pat"], ["Why is console so slow?", "Reny"], ["97% of scientists are wrong", "Dragan"], ["I love Kool-Aid", "Pat"]]; if (shouldUpdate) { var quoteIdx = getRandomInt(0, quotes.length); var quote = quotes[quoteIdx]; quoteContainer.html("\u201C".concat(quote[0], "\u201D<br> - ").concat(quote[1])); } } function onReady() { $(".day-view-week-nav").after("\n <div id=\"day-view-control-row\">\n <label>I started work at: \n <input type='time' id='start-work-time' value='09:00' />\n </label>\n <label>\n <input type='checkbox' id='row-time-12hr' />\n Add AM/PM labels\n </label>\n </div>"); getSavedStartWork(); $("#start-work-time").change(onChangeStartWork); getSaved12Hr(); $("#row-time-12hr").change(onChange12Hr); //Create placeholder button if (FEATURE_PLACEHOLDERS_ENABLED) { createPlaceholderButton(); } // Attach our mutation observer var childListObserver = new MutationObserver(onMutateChildList); var attributeObserver = new MutationObserver(onMutateAttributes); var targetNode = $(".day-view-entry-list").get(0); childListObserver.observe(targetNode, { childList: true, subtree: true }); attributeObserver.observe(targetNode, { attributes: true, attributeFilter: ["data-test-day"] }); attachRows(); // Run once at startup replaceQuote(); } var waiting = setInterval(function () { if (typeof $ === "undefined") { return; } clearInterval(waiting); $(onReady); }, 100); /***/ }), /***/ "./src/style.scss": /*!************************!*\ !*** ./src/style.scss ***! \************************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { var content = __webpack_require__(/*! !../node_modules/css-loader/dist/cjs.js!../node_modules/sass-loader/lib/loader.js!./style.scss */ "./node_modules/css-loader/dist/cjs.js!./node_modules/sass-loader/lib/loader.js!./src/style.scss"); if(typeof content === 'string') content = [[module.i, content, '']]; var transform; var insertInto; var options = {"hmr":true} options.transform = transform options.insertInto = undefined; var update = __webpack_require__(/*! ../node_modules/style-loader/lib/addStyles.js */ "./node_modules/style-loader/lib/addStyles.js")(content, options); if(content.locals) module.exports = content.locals; if(false) {} /***/ }) /******/ });