Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/timeivy.git
HOTKEYS.md
+# Time Ivy
+
+Time tracking software for hourly billing.
+
+## Navigate Mode Key Bindings
+
+When not editing a cell, the following keyboard bindings apply:
+
+Hot Key | Action
+--- | ---
+Ctrl+i | Insert a copy of the current row below the active cell
+Insert | "
+Delete | Delete the active cell's contents
+Shift+Delete | Delete the active cell's row
+F2 | Enter edit mode for the active cell
+Enter | Navigate one cell down
+Up Arrow | Navigate one cell up
+Down Arrow | Navigate one cell down
+Left Arrow | Navigate one cell left
+Shift+Tab | "
+Right Arrow | Navigate one cell right
+Tab | "
+Page Up | Navigate one page up
+Page Down | Navigate one page down
+Ctrl+Up Arrow | Navigate to the first non-empty cell upwards
+Ctrl+Down Arrow | Navigate to the first non-empty cell downwards
+Ctrl+Left Arrow | Navigate to the first non-empty cell leftwards
+Ctrl+Right Arrow | Navigate to the first non-empty cell rightwards
+Home | Navigate to the current row's first column
+End | Navigate to the current row's last non-empty column
+Ctrl+Home | Navigate to the first row and first column
+Ctrl+End | Navigate to the last row, and last column
+Ctrl+x | Cut active cell text
+Ctrl+c | Copy active cell text
+Ctrl+Insert | "
+Ctrl+v | Paste copied text into the application
+Shift+Insert | "
+Ctrl+z | Undo the previous action
+
+Most other keys will enter edit mode for the active cell after first
+clearing the cell contents.
+
+## Edit Mode Key Bindings
+
+When ending a cell, the following keyboard bindings apply:
+
+Hot Key | Action
+--- | ---
+Esc | Enter navigate mode, undo any changes
+Up Arrow | Enter navigate mode, perform navigation
+Down Arrow | "
+Tab | "
+Shift+Tab | "
+
LICENSE.md
+The MIT License (MIT)
+
+Copyright 2018 White Magic Software, Ltd.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
README.md
+# Time Ivy
+
+Time tracking software for hourly billing.
+
+# Spreadsheet User Interface
+
+The spreadsheet interface offers keyboard bindings similar to classic
+desktop spreadsheet applications.
+
TIMEFORMATS.md
+# Time Ivy
+
+Time tracking software for hourly billing.
+
+## Time Formats
+
+Time can be entered into the system using a wide variety of formats, such as:
+
+Input | Output
+--- | ---
+1:00 pm | 01:00 PM
+1:00 p.m. | 01:00 PM
+1:00 p | 01:00 PM
+1:00pm | 01:00 PM
+1:00p.m. | 01:00 PM
+1:00p | 01:00 PM
+1 pm | 01:00 PM
+1 p.m. | 01:00 PM
+1 p | 01:00 PM
+1pm | 01:00 PM
+1p.m. | 01:00 PM
+1p | 01:00 PM
+13:00 | 01:00 PM
+13 | 01:00 PM
+12 | 12:00 PM
+2400 | 00:00 AM
+1a | 01:00 AM
+100 | 01:00 AM
+123 | 01:23 AM
+1000 | 10:00 AM
+2459 | 00:59 AM
+2359 | 11:59 PM
dev.html
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
+ <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
+ <meta name="description" content="Time tracking software for consultants."/>
+ <title>Time Ivy</title>
+ <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/normalize/8.0.0/normalize.min.css" />
+ <link rel="stylesheet" href="themes/simple.css"/>
+ <link rel="stylesheet" href="themes/app.css"/>
+ <link rel="stylesheet" href="themes/ivy.css"/>
+ <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/base/jquery-ui.min.css" integrity="sha256-sEGfrwMkIjbgTBwGLVK38BG/XwIiNC/EAG9Rzsfda6A=" crossorigin="anonymous" />
+ <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js" integrity="sha256-KM512VNnjElC30ehFwehXjx1YCHPiQkOPmqnrWtpccM=" crossorigin="anonymous"></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.21.0/moment.min.js" integrity="sha256-9YAuB2VnFZNJ+lKfpaQ3dKQT9/C0j3VUla76hHbiVF8=" crossorigin="anonymous"></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/mousetrap/1.6.1/mousetrap.min.js" integrity="sha256-z6XYkzzC5o+5PhoIPMpyq5FOZkWFGiWa0NFIDPJ57zU=" crossorigin="anonymous"></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/json-editor/0.7.28/jsoneditor.min.js" integrity="sha256-51+oMmpgSgS4jV5/DcGKnDHIOL6Jeie2i7ka6sPQVro=" crossorigin="anonymous"></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js" integrity="sha256-nRoO8HoupfqozUr7YKBRgHXmdx40Hl/04OSBzv7e7L8=" crossorigin="anonymous"></script>
+</head>
+<body>
+ <nav class="menu">
+ <ul>
+ <li><a href="#">Edit</a>
+ <ul>
+ <li><a class="app-edit-cut" href="#">Cut</a> <kbd>Ctrl+x</kbd></li>
+ <li><a class="app-edit-copy" href="#">Copy</a> <kbd>Ctrl+c</kbd></li>
+ <li><hr /></li>
+ <li><a class="app-edit-undo" href="#">Undo</a> <kbd>Ctrl+z</kbd></li>
+ <li><a class="app-edit-redo" href="#">Redo</a> <kbd>Ctrl+y</kbd></li>
+ </ul>
+ </li>
+ <li><a href="#">Timesheet</a>
+ <ul>
+ <li><a class="app-insert-shift" href="#">Insert Shift</a></li>
+ <li><a class="app-append-day" href="#">Append Day</a></li>
+ <li><hr /></li>
+ <li><a class="app-delete-row" href="#">Delete Row</a></li>
+ </ul>
+ </li>
+ <li><a href="#">Settings</a>
+ <ul>
+ <li><a class="app-settings-preferences" href="#">Preferences</a></li>
+ </ul>
+ </li>
+ <li><a href="#">Invoice</a>
+ <ul>
+ <li><a href="invoice.png" target="_blank">Preview</a></li>
+ <li><a href="invoice.png" target="_blank">Send</a></li>
+ </ul>
+ </li>
+ <li><a href="#">Export</a>
+ <ul>
+ <li><a href="#" class="ivy-export-csv">CSV</a></li>
+ <li><a href="#" class="ivy-export-json">JSON</a></li>
+ </ul>
+ </li>
+ <!--
+ <li><a href="#">Import</a>
+ <ul>
+ <li><a href="#" class="ivy-import-csv">CSV</a></li>
+ <li><a href="#" class="ivy-import-json">JSON</a></li>
+ </ul>
+ </li>
+ -->
+ <li><a href="#">Contact</a>
+ <ul>
+ <li><a href="https://github.com/DaveJarvis/timeivy/issues" target="_blank">Bug report</a></li>
+ </ul>
+ </li>
+ <li><a href="#">Help</a>
+ <ul>
+ <li><a href="https://github.com/DaveJarvis/timeivy/blob/master/TIMEFORMATS.md#time-ivy" target="_blank">Time formats</a></li>
+ <li><a href="https://github.com/DaveJarvis/timeivy/blob/master/HOTKEYS.md#time-ivy" target="_blank">Hot keys</a></li>
+ </ul>
+ </li>
+ </ul>
+ </nav>
+ <main class="section content container">
+ <table id="ivy" class="ivy" cellspacing="0" cellpadding="0">
+ <thead>
+ <tr>
+ <th class="ivy-readonly"><button accesskey="d" title="Append Day" class="ivy app-append-day">Day</button></th>
+ <th class="ivy-transient">Total</th>
+ <th class="ivy-transient"><button accesskey="i" title="Insert Shift" class="ivy app-insert-shift">Shift</button></th>
+ <th>Began</th>
+ <th>Ended</th>
+ </tr>
+ </thead>
+ <tbody tabindex="0">
+ </tbody>
+ </table>
+ </main>
+ <div id="settings" title="Settings">
+ <div id="editor">
+ </div>
+ </div>
+
+ <script src="js/prototypes.js"></script>
+ <script src="js/ivy-plugin.js"></script>
+ <script src="js/exportable-plugin.js"></script>
+ <script src="js/ivy-app.js"></script>
+</body>
+</html>
geocoding.json
+https://www.geocode.farm/v3/json/forward/?addr=1000+beverly+st+beverly+hills+90028+ca
+
+{
+ "geocoding_results": {
+ "LEGAL_COPYRIGHT": {
+ "copyright_notice": "Copyright (c) 2018 Geocode.Farm - All Rights Reserved.",
+ "copyright_logo": "https:\/\/www.geocode.farm\/images\/logo.png",
+ "terms_of_service": "https:\/\/www.geocode.farm\/policies\/terms-of-service\/",
+ "privacy_policy": "https:\/\/www.geocode.farm\/policies\/privacy-policy\/"
+ },
+ "STATUS": {
+ "access": "FREE_USER, ACCESS_GRANTED",
+ "status": "SUCCESS",
+ "address_provided": "1000 beverly st beverly hills 90028 ca",
+ "result_count": 1
+ },
+ "ACCOUNT": {
+ "ip_address": "24.69.131.109",
+ "distribution_license": "NONE, UNLICENSED",
+ "usage_limit": "250",
+ "used_today": "10",
+ "used_total": "10",
+ "first_used": "09 Apr 2018"
+ },
+ "RESULTS": [
+ {
+ "result_number": 1,
+ "formatted_address": "1000 N Beverly Dr, Beverly Hills, CA 90210, USA",
+ "accuracy": "HIGH_ACCURACY",
+ "ADDRESS": {
+ "street_number": "1000",
+ "street_name": "North Beverly Drive",
+ "locality": "Beverly Hills",
+ "admin_2": "Los Angeles County",
+ "admin_1": "California",
+ "postal_code": "90210",
+ "country": "United States"
+ },
+ "LOCATION_DETAILS": {
+ "elevation": "UNAVAILABLE",
+ "timezone_long": "UNAVAILABLE",
+ "timezone_short": "America\/Los_Angeles"
+ },
+ "COORDINATES": {
+ "latitude": "34.0855558458290",
+ "longitude": "-118.412741940074"
+ },
+ "BOUNDARIES": {
+ "northeast_latitude": "34.0855558458290",
+ "northeast_longitude": "-118.412741940074",
+ "southwest_latitude": "34.0842400197558",
+ "southwest_longitude": "-118.414090880194"
+ }
+ }
+ ],
+ "STATISTICS": {
+ "https_ssl": "ENABLED, SECURE",
+ "time_taken": "0.75789403915405"
+ }
+ }
+}
index.html
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
+ <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
+ <meta name="description" content="Time tracking software for consultants."/>
+ <title>Time Ivy</title>
+ <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/normalize/8.0.0/normalize.min.css" />
+ <link rel="stylesheet" href="themes/simple.css"/>
+ <link rel="stylesheet" href="themes/app.css"/>
+ <link rel="stylesheet" href="themes/ivy.css"/>
+ <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/base/jquery-ui.min.css" integrity="sha256-sEGfrwMkIjbgTBwGLVK38BG/XwIiNC/EAG9Rzsfda6A=" crossorigin="anonymous" />
+ <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js" integrity="sha256-KM512VNnjElC30ehFwehXjx1YCHPiQkOPmqnrWtpccM=" crossorigin="anonymous"></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.21.0/moment.min.js" integrity="sha256-9YAuB2VnFZNJ+lKfpaQ3dKQT9/C0j3VUla76hHbiVF8=" crossorigin="anonymous"></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/mousetrap/1.6.1/mousetrap.min.js" integrity="sha256-z6XYkzzC5o+5PhoIPMpyq5FOZkWFGiWa0NFIDPJ57zU=" crossorigin="anonymous"></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/json-editor/0.7.28/jsoneditor.min.js" integrity="sha256-51+oMmpgSgS4jV5/DcGKnDHIOL6Jeie2i7ka6sPQVro=" crossorigin="anonymous"></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js" integrity="sha256-nRoO8HoupfqozUr7YKBRgHXmdx40Hl/04OSBzv7e7L8=" crossorigin="anonymous"></script>
+</head>
+<body>
+ <nav class="menu">
+ <ul>
+ <li><a href="#">Edit</a>
+ <ul>
+ <li><a class="app-edit-cut" href="#">Cut</a> <kbd>Ctrl+x</kbd></li>
+ <li><a class="app-edit-copy" href="#">Copy</a> <kbd>Ctrl+c</kbd></li>
+ <li><hr /></li>
+ <li><a class="app-edit-undo" href="#">Undo</a> <kbd>Ctrl+z</kbd></li>
+ <li><a class="app-edit-redo" href="#">Redo</a> <kbd>Ctrl+y</kbd></li>
+ </ul>
+ </li>
+ <li><a href="#">Timesheet</a>
+ <ul>
+ <li><a class="app-insert-shift" href="#">Insert Shift</a></li>
+ <li><a class="app-append-day" href="#">Append Day</a></li>
+ <li><hr /></li>
+ <li><a class="app-delete-row" href="#">Delete Row</a></li>
+ </ul>
+ </li>
+ <li><a href="#">Settings</a>
+ <ul>
+ <li><a class="app-settings-preferences" href="#">Preferences</a></li>
+ </ul>
+ </li>
+ <li><a href="#">Invoice</a>
+ <ul>
+ <li><a href="invoice.png" target="_blank">Preview</a></li>
+ <li><a href="invoice.png" target="_blank">Send</a></li>
+ </ul>
+ </li>
+ <li><a href="#">Export</a>
+ <ul>
+ <li><a href="#" class="ivy-export-csv">CSV</a></li>
+ <li><a href="#" class="ivy-export-json">JSON</a></li>
+ </ul>
+ </li>
+ <!--
+ <li><a href="#">Import</a>
+ <ul>
+ <li><a href="#" class="ivy-import-csv">CSV</a></li>
+ <li><a href="#" class="ivy-import-json">JSON</a></li>
+ </ul>
+ </li>
+ -->
+ <li><a href="#">Contact</a>
+ <ul>
+ <li><a href="https://github.com/DaveJarvis/timeivy/issues" target="_blank">Bug report</a></li>
+ </ul>
+ </li>
+ <li><a href="#">Help</a>
+ <ul>
+ <li><a href="https://github.com/DaveJarvis/timeivy/blob/master/TIMEFORMATS.md#time-ivy" target="_blank">Time formats</a></li>
+ <li><a href="https://github.com/DaveJarvis/timeivy/blob/master/HOTKEYS.md#time-ivy" target="_blank">Hot keys</a></li>
+ </ul>
+ </li>
+ </ul>
+ </nav>
+ <main class="section content container">
+ <table id="ivy" class="ivy" cellspacing="0" cellpadding="0">
+ <thead>
+ <tr>
+ <th class="ivy-readonly"><button accesskey="d" title="Append Day" class="ivy app-append-day">Day</button></th>
+ <th class="ivy-transient">Total</th>
+ <th class="ivy-transient"><button accesskey="i" title="Insert Shift" class="ivy app-insert-shift">Shift</button></th>
+ <th>Began</th>
+ <th>Ended</th>
+ </tr>
+ </thead>
+ <tbody tabindex="0">
+ </tbody>
+ </table>
+ </main>
+ <div id="settings" title="Settings">
+ <div id="editor">
+ </div>
+ </div>
+
+ <script src="js/prototypes.js"></script>
+ <script src="js/ivy-plugin.js"></script>
+ <script src="js/exportable-plugin.js"></script>
+ <script src="js/ivy-app.js"></script>
+</body>
+</html>
invoice.png
Binary files differ
invoice.svg
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ id="svg2"
+ xml:space="preserve"
+ width="816"
+ height="1056"
+ viewBox="0 0 816 1056"
+ sodipodi:docname="hoursrate.svg"
+ inkscape:version="0.92.3 (unknown)"
+ inkscape:export-filename="/home/jarvisd/dev/js/timeivy/hoursrate.png"
+ inkscape:export-xdpi="300"
+ inkscape:export-ydpi="300"><metadata
+ id="metadata8"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs6"><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath18"><path
+ d="M 37.2,12.4 H 575.8 V 778 H 37.2 Z"
+ id="path16"
+ inkscape:connector-curvature="0"
+ style="clip-rule:evenodd" /></clipPath><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath154"><path
+ d="m 387.93,636.95 h 158.65 v 18.6 H 387.93 Z"
+ id="path152"
+ inkscape:connector-curvature="0"
+ style="clip-rule:evenodd" /></clipPath><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath166"><path
+ d="m 387.93,500.53 h 158.65 v 28 H 387.93 Z"
+ id="path164"
+ inkscape:connector-curvature="0"
+ style="clip-rule:evenodd" /></clipPath><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath178"><path
+ d="m 387.93,599.35 h 158.65 v 18.6 H 387.93 Z"
+ id="path176"
+ inkscape:connector-curvature="0"
+ style="clip-rule:evenodd" /></clipPath><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath190"><path
+ d="m 208.28,675.58 h 179.45 v 89.625 H 208.28 Z"
+ id="path188"
+ inkscape:connector-curvature="0"
+ style="clip-rule:evenodd" /></clipPath><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath202"><path
+ d="m 127.02,500.53 h 117.05 v 28 H 127.02 Z"
+ id="path200"
+ inkscape:connector-curvature="0"
+ style="clip-rule:evenodd" /></clipPath><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath214"><path
+ d="m 67.625,366.9 h 241.08 v 37.4 H 67.625 Z"
+ id="path212"
+ inkscape:connector-curvature="0"
+ style="clip-rule:evenodd" /></clipPath><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath226"><path
+ d="m 67.625,329.27 h 241.08 v 37.425 H 67.625 Z"
+ id="path224"
+ inkscape:connector-curvature="0"
+ style="clip-rule:evenodd" /></clipPath><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath242"><path
+ d="m 67.625,291.67 h 241.08 v 37.4 H 67.625 Z"
+ id="path240"
+ inkscape:connector-curvature="0"
+ style="clip-rule:evenodd" /></clipPath><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath258"><path
+ d="m 37.2,25 h 538 v 40.825 h -538 z"
+ id="path256"
+ inkscape:connector-curvature="0"
+ style="clip-rule:evenodd" /></clipPath><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath270"><path
+ d="m 244.28,500.53 h 143.45 v 28 H 244.28 Z"
+ id="path268"
+ inkscape:connector-curvature="0"
+ style="clip-rule:evenodd" /></clipPath><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath282"><path
+ d="m 37.2,404.5 h 271.5 v 56.425 H 37.2 Z"
+ id="path280"
+ inkscape:connector-curvature="0"
+ style="clip-rule:evenodd" /></clipPath><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath294"><path
+ d="m 308.9,404.5 h 78.825 v 56.425 H 308.9 Z"
+ id="path292"
+ inkscape:connector-curvature="0"
+ style="clip-rule:evenodd" /></clipPath><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath306"><path
+ d="m 308.9,366.9 h 78.825 v 37.4 H 308.9 Z"
+ id="path304"
+ inkscape:connector-curvature="0"
+ style="clip-rule:evenodd" /></clipPath><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath318"><path
+ d="M 35.2,10.2 H 578 V 780 H 35.2 Z"
+ id="path316"
+ inkscape:connector-curvature="0"
+ style="clip-rule:evenodd" /></clipPath><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath658"><path
+ d="M 36.2,12.6 H 575.6 V 779 H 36.2 Z"
+ id="path656"
+ inkscape:connector-curvature="0"
+ style="clip-rule:evenodd" /></clipPath><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath670"><path
+ d="M 36.2,12.6 H 575.6 V 779 H 36.2 Z"
+ id="path668"
+ inkscape:connector-curvature="0"
+ style="clip-rule:evenodd" /></clipPath><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath682"><path
+ d="M 36.2,12.6 H 575.6 V 779 H 36.2 Z"
+ id="path680"
+ inkscape:connector-curvature="0"
+ style="clip-rule:evenodd" /></clipPath><mask
+ maskUnits="userSpaceOnUse"
+ x="0"
+ y="0"
+ width="1"
+ height="1"
+ id="mask686"><image
+ width="1"
+ height="1"
+ style="image-rendering:optimizeSpeed"
+ preserveAspectRatio="none"
+ xlink:href="data:image/png;base64,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"
+ id="image688" /></mask></defs><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="640"
+ inkscape:window-height="480"
+ id="namedview4"
+ showgrid="false"
+ inkscape:zoom="0.91193182"
+ inkscape:cx="368.31921"
+ inkscape:cy="453.07593"
+ inkscape:current-layer="g10" /><g
+ id="g10"
+ inkscape:groupmode="layer"
+ inkscape:label="hoursrate"
+ transform="matrix(1.3333333,0,0,-1.3333333,0,1056)"><g
+ id="g2601"
+ transform="translate(0,5.2284892e-5)"><path
+ inkscape:connector-curvature="0"
+ id="path20"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 36.6,675.38 h 538.8 v 99.825 H 36.6 Z" /><text
+ y="-744.38"
+ x="389.92999"
+ id="text40"
+ style="font-variant:normal;font-weight:bold;font-size:11.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#ffc000;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ y="-744.38"
+ x="389.92999"
+ id="tspan38"
+ sodipodi:role="line">Your Company Name</tspan></text>
+<text
+ y="-719.96997"
+ x="389.72"
+ id="text44"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ y="-719.96997"
+ x="389.72"
+ id="tspan42"
+ sodipodi:role="line">Street Address</tspan></text>
+<text
+ y="-705.78003"
+ x="389.72"
+ id="text48"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ y="-705.78003"
+ x="389.72"
+ id="tspan46"
+ sodipodi:role="line">Victoria, BC V8V1V1</tspan></text>
+<text
+ y="-691.58002"
+ x="389.72"
+ id="text52"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ y="-691.58002"
+ x="389.72"
+ id="tspan50"
+ sodipodi:role="line">250-555-1212</tspan></text>
+<text
+ y="-711.78003"
+ x="243.48"
+ id="text194"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:30px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ y="-711.78003"
+ x="243.48"
+ id="tspan192"
+ sodipodi:role="line">INVOICE</tspan></text>
+<g
+ transform="translate(0,18)"
+ id="g676"><g
+ clip-path="url(#clipPath682)"
+ id="g678"><g
+ transform="matrix(144.6,0,0,11,51,700.8)"
+ id="g684"><image
+ id="image690"
+ mask="url(#mask686)"
+ xlink:href="data:image/png;base64,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"
+ transform="matrix(1,0,0,-1,0,1)"
+ preserveAspectRatio="none"
+ style="image-rendering:optimizeSpeed"
+ height="1"
+ width="1" /></g></g></g><path
+ inkscape:connector-curvature="0"
+ id="path692"
+ style="fill:#ffc000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 77.919,724.22 c -0.224,0 -0.415,-0.01 -0.573,-0.05 -0.158,-0.03 -0.287,-0.08 -0.386,-0.15 -0.099,-0.06 -0.171,-0.14 -0.217,-0.24 -0.046,-0.09 -0.069,-0.2 -0.069,-0.32 0,-0.2 0.065,-0.36 0.195,-0.48 0.131,-0.12 0.312,-0.18 0.543,-0.18 0.195,0 0.374,0.05 0.537,0.15 0.163,0.1 0.33,0.24 0.498,0.44 v 0.83 z m -8.594,1.96 c -0.092,0 -0.183,-0.02 -0.273,-0.04 -0.09,-0.03 -0.181,-0.08 -0.275,-0.14 -0.094,-0.06 -0.19,-0.14 -0.289,-0.24 -0.099,-0.1 -0.204,-0.22 -0.314,-0.37 v -1.62 c 0.195,-0.24 0.38,-0.43 0.556,-0.57 0.176,-0.13 0.36,-0.2 0.551,-0.2 0.18,0 0.333,0.05 0.46,0.14 0.127,0.09 0.23,0.21 0.311,0.36 0.081,0.15 0.141,0.32 0.179,0.5 0.039,0.19 0.058,0.38 0.058,0.56 0,0.21 -0.016,0.41 -0.047,0.61 -0.031,0.19 -0.084,0.36 -0.16,0.51 -0.075,0.15 -0.174,0.27 -0.297,0.36 -0.123,0.09 -0.276,0.14 -0.46,0.14 z m 74.815,0.09 c -0.2,0 -0.38,-0.04 -0.54,-0.12 -0.15,-0.07 -0.28,-0.18 -0.38,-0.33 -0.09,-0.14 -0.17,-0.32 -0.22,-0.52 -0.05,-0.2 -0.07,-0.44 -0.07,-0.7 0,-0.24 0.02,-0.46 0.06,-0.67 0.04,-0.2 0.1,-0.38 0.19,-0.53 0.09,-0.15 0.22,-0.27 0.37,-0.35 0.15,-0.08 0.34,-0.12 0.57,-0.12 0.21,0 0.39,0.04 0.55,0.11 0.15,0.08 0.28,0.19 0.38,0.33 0.1,0.14 0.17,0.32 0.22,0.52 0.05,0.2 0.07,0.44 0.07,0.7 0,0.24 -0.02,0.46 -0.06,0.67 -0.04,0.2 -0.1,0.38 -0.19,0.53 -0.1,0.15 -0.22,0.26 -0.37,0.35 -0.15,0.08 -0.34,0.13 -0.58,0.13 z m -18.8,0 c -0.2,0 -0.38,-0.04 -0.54,-0.12 -0.15,-0.07 -0.28,-0.18 -0.38,-0.33 -0.09,-0.14 -0.17,-0.32 -0.22,-0.52 -0.05,-0.2 -0.07,-0.44 -0.07,-0.7 0,-0.24 0.02,-0.46 0.06,-0.67 0.04,-0.2 0.1,-0.38 0.19,-0.53 0.09,-0.15 0.22,-0.27 0.37,-0.35 0.15,-0.08 0.34,-0.12 0.57,-0.12 0.21,0 0.39,0.04 0.55,0.11 0.15,0.08 0.28,0.19 0.38,0.33 0.1,0.14 0.17,0.32 0.22,0.52 0.05,0.2 0.07,0.44 0.07,0.7 0,0.24 -0.02,0.46 -0.06,0.67 -0.04,0.2 -0.1,0.38 -0.19,0.53 -0.1,0.15 -0.22,0.26 -0.37,0.35 -0.15,0.08 -0.34,0.13 -0.58,0.13 z m -37.191,0.11 c -0.177,0 -0.33,-0.03 -0.46,-0.1 -0.131,-0.06 -0.239,-0.15 -0.325,-0.26 -0.087,-0.12 -0.153,-0.25 -0.199,-0.4 -0.045,-0.15 -0.072,-0.32 -0.079,-0.49 h 2.07 c 0.011,0.39 -0.067,0.7 -0.234,0.92 -0.167,0.22 -0.425,0.33 -0.773,0.33 z m -24.816,0 c -0.177,0 -0.33,-0.03 -0.46,-0.1 -0.131,-0.06 -0.239,-0.15 -0.325,-0.26 -0.087,-0.12 -0.153,-0.25 -0.199,-0.4 -0.045,-0.15 -0.072,-0.32 -0.079,-0.49 h 2.07 c 0.011,0.39 -0.067,0.7 -0.234,0.92 -0.167,0.22 -0.425,0.33 -0.773,0.33 z m 84.677,0.89 c 0.14,0 0.26,-0.01 0.35,-0.01 0.1,-0.01 0.17,-0.02 0.22,-0.04 0.05,-0.02 0.09,-0.04 0.11,-0.08 0.02,-0.03 0.03,-0.06 0.04,-0.11 l 0.98,-3.61 0.01,-0.06 0.01,0.06 0.91,3.61 c 0.02,0.05 0.03,0.08 0.06,0.11 0.02,0.04 0.06,0.06 0.1,0.08 0.05,0.02 0.12,0.03 0.21,0.04 0.08,0 0.19,0.01 0.33,0.01 0.13,0 0.24,-0.01 0.33,-0.02 0.08,0 0.15,-0.02 0.2,-0.03 0.05,-0.02 0.08,-0.04 0.11,-0.07 0.02,-0.03 0.03,-0.06 0.04,-0.1 l 0.98,-3.63 0.02,-0.06 0.01,0.06 0.94,3.61 c 0,0.05 0.02,0.08 0.04,0.11 0.02,0.04 0.06,0.06 0.11,0.08 0.05,0.02 0.12,0.03 0.21,0.04 0.09,0 0.2,0.01 0.34,0.01 0.13,0 0.24,-0.01 0.33,-0.01 0.09,-0.01 0.15,-0.02 0.2,-0.04 0.05,-0.01 0.09,-0.04 0.1,-0.06 0.02,-0.03 0.03,-0.06 0.03,-0.1 0,-0.04 0,-0.09 -0.01,-0.15 -0.02,-0.06 -0.04,-0.14 -0.07,-0.25 l -1.31,-4.45 c -0.01,-0.06 -0.04,-0.11 -0.07,-0.15 -0.03,-0.04 -0.07,-0.07 -0.13,-0.09 -0.06,-0.02 -0.15,-0.04 -0.26,-0.04 -0.11,-0.01 -0.26,-0.02 -0.44,-0.02 -0.18,0 -0.33,0.01 -0.44,0.02 -0.11,0.01 -0.2,0.02 -0.27,0.05 -0.06,0.02 -0.11,0.05 -0.14,0.09 -0.03,0.03 -0.05,0.08 -0.06,0.14 l -0.83,2.99 -0.01,0.05 -0.01,-0.05 -0.76,-2.99 c -0.02,-0.06 -0.04,-0.11 -0.07,-0.15 -0.02,-0.04 -0.07,-0.07 -0.14,-0.09 -0.06,-0.02 -0.15,-0.04 -0.27,-0.04 -0.11,-0.01 -0.26,-0.02 -0.44,-0.02 -0.18,0 -0.33,0.01 -0.44,0.02 -0.11,0.01 -0.2,0.02 -0.26,0.05 -0.07,0.02 -0.11,0.05 -0.14,0.09 -0.03,0.03 -0.06,0.08 -0.07,0.14 l -1.3,4.45 c -0.03,0.1 -0.05,0.19 -0.06,0.25 -0.01,0.06 -0.02,0.11 -0.02,0.15 0,0.04 0.01,0.07 0.03,0.1 0.02,0.02 0.06,0.05 0.11,0.06 0.06,0.02 0.13,0.03 0.22,0.04 0.09,0 0.2,0.01 0.34,0.01 z m -18.34,0 c 0.14,0 0.25,-0.01 0.34,-0.02 0.09,0 0.16,-0.02 0.21,-0.04 0.06,-0.01 0.09,-0.04 0.12,-0.07 0.02,-0.02 0.03,-0.06 0.03,-0.09 v -2.78 c 0,-0.25 0.02,-0.45 0.05,-0.59 0.03,-0.13 0.08,-0.25 0.15,-0.35 0.07,-0.09 0.15,-0.17 0.26,-0.22 0.1,-0.06 0.23,-0.08 0.37,-0.08 0.17,0 0.35,0.06 0.53,0.19 0.17,0.13 0.36,0.32 0.57,0.56 v 3.27 c 0,0.03 0.01,0.07 0.03,0.09 0.02,0.03 0.05,0.06 0.11,0.07 0.05,0.02 0.12,0.04 0.21,0.04 0.09,0.01 0.2,0.02 0.34,0.02 0.13,0 0.24,-0.01 0.33,-0.02 0.09,0 0.16,-0.02 0.21,-0.04 0.05,-0.01 0.09,-0.04 0.11,-0.07 0.02,-0.02 0.03,-0.06 0.03,-0.09 v -4.91 c 0,-0.04 -0.01,-0.07 -0.02,-0.1 -0.02,-0.03 -0.05,-0.05 -0.1,-0.07 -0.05,-0.02 -0.11,-0.03 -0.18,-0.04 -0.08,-0.01 -0.17,-0.02 -0.29,-0.02 -0.12,0 -0.22,0.01 -0.29,0.02 -0.08,0.01 -0.14,0.02 -0.18,0.04 -0.04,0.02 -0.07,0.04 -0.09,0.07 -0.02,0.03 -0.03,0.06 -0.03,0.1 v 0.56 c -0.27,-0.29 -0.55,-0.51 -0.83,-0.66 -0.28,-0.15 -0.57,-0.22 -0.87,-0.22 -0.34,0 -0.62,0.06 -0.85,0.17 -0.23,0.11 -0.42,0.26 -0.56,0.45 -0.14,0.19 -0.24,0.41 -0.3,0.66 -0.06,0.25 -0.09,0.57 -0.09,0.94 v 3.01 c 0,0.03 0.01,0.07 0.03,0.09 0.02,0.03 0.05,0.06 0.11,0.07 0.05,0.02 0.12,0.04 0.21,0.04 0.09,0.01 0.2,0.02 0.33,0.02 z m -11.8,0 c 0.15,0 0.28,0 0.37,-0.01 0.09,-0.01 0.16,-0.02 0.22,-0.04 0.05,-0.02 0.09,-0.06 0.11,-0.1 0.03,-0.05 0.05,-0.11 0.08,-0.2 l 1.15,-3.29 h 0.02 l 1.06,3.36 c 0.02,0.09 0.05,0.16 0.08,0.19 0.04,0.03 0.1,0.05 0.18,0.07 0.08,0.01 0.22,0.02 0.42,0.02 0.16,0 0.29,-0.01 0.4,-0.02 0.1,-0.02 0.18,-0.05 0.23,-0.09 0.05,-0.04 0.07,-0.09 0.07,-0.16 0,-0.07 -0.01,-0.15 -0.04,-0.24 l -1.64,-4.81 -0.59,-1.74 c -0.04,-0.09 -0.13,-0.16 -0.27,-0.2 -0.15,-0.04 -0.36,-0.06 -0.65,-0.06 -0.14,0 -0.26,0.01 -0.35,0.02 -0.09,0.01 -0.15,0.03 -0.2,0.06 -0.04,0.03 -0.07,0.07 -0.07,0.11 0,0.05 0.01,0.1 0.03,0.16 l 0.66,1.65 c -0.05,0.02 -0.09,0.05 -0.13,0.1 -0.04,0.05 -0.07,0.09 -0.09,0.15 l -1.69,4.51 c -0.05,0.13 -0.07,0.23 -0.07,0.3 0,0.06 0.02,0.12 0.07,0.16 0.04,0.04 0.12,0.06 0.22,0.08 0.1,0.01 0.24,0.02 0.42,0.02 z m -14.67,0 c 0.14,0 0.25,-0.01 0.34,-0.02 0.08,-0.01 0.15,-0.02 0.21,-0.04 0.05,-0.02 0.09,-0.05 0.11,-0.08 0.02,-0.02 0.03,-0.06 0.03,-0.09 v -4.9 c 0,-0.04 -0.01,-0.07 -0.03,-0.1 -0.02,-0.03 -0.06,-0.05 -0.11,-0.07 -0.06,-0.02 -0.13,-0.03 -0.21,-0.04 -0.09,-0.01 -0.2,-0.02 -0.34,-0.02 -0.14,0 -0.25,0.01 -0.34,0.02 -0.08,0.01 -0.15,0.02 -0.21,0.04 -0.05,0.02 -0.09,0.04 -0.11,0.07 -0.02,0.03 -0.03,0.06 -0.03,0.1 v 4.9 c 0,0.03 0.01,0.07 0.03,0.09 0.02,0.03 0.06,0.06 0.11,0.08 0.06,0.02 0.13,0.03 0.21,0.04 0.09,0.01 0.2,0.02 0.34,0.02 z m -8.961,0 c 0.147,0 0.266,-0.01 0.358,-0.01 0.092,-0.01 0.163,-0.02 0.215,-0.04 0.051,-0.02 0.087,-0.04 0.107,-0.08 0.02,-0.03 0.036,-0.06 0.047,-0.11 l 0.975,-3.61 0.011,-0.06 0.011,0.06 0.914,3.61 c 0.011,0.05 0.028,0.08 0.052,0.11 0.024,0.04 0.06,0.06 0.108,0.08 0.047,0.02 0.114,0.03 0.201,0.04 0.086,0 0.197,0.01 0.333,0.01 0.136,0 0.246,-0.01 0.33,-0.02 0.085,0 0.152,-0.02 0.201,-0.03 0.05,-0.02 0.085,-0.04 0.105,-0.07 0.02,-0.03 0.036,-0.06 0.047,-0.1 l 0.98,-3.63 0.017,-0.06 0.011,0.06 0.938,3.61 c 0.01,0.05 0.02,0.08 0.04,0.11 0.02,0.04 0.06,0.06 0.11,0.08 0.05,0.02 0.12,0.03 0.21,0.04 0.09,0 0.2,0.01 0.34,0.01 0.13,0 0.24,-0.01 0.33,-0.01 0.09,-0.01 0.16,-0.02 0.2,-0.04 0.05,-0.01 0.09,-0.04 0.11,-0.06 0.01,-0.03 0.02,-0.06 0.02,-0.1 0,-0.04 0,-0.09 -0.01,-0.15 -0.01,-0.06 -0.03,-0.14 -0.06,-0.25 l -1.31,-4.45 c -0.02,-0.06 -0.05,-0.11 -0.08,-0.15 -0.02,-0.04 -0.07,-0.07 -0.13,-0.09 -0.061,-0.02 -0.147,-0.04 -0.259,-0.04 -0.112,-0.01 -0.258,-0.02 -0.438,-0.02 -0.18,0 -0.327,0.01 -0.441,0.02 -0.114,0.01 -0.203,0.02 -0.267,0.05 -0.064,0.02 -0.111,0.05 -0.14,0.09 -0.03,0.03 -0.052,0.08 -0.066,0.14 l -0.827,2.99 -0.011,0.05 -0.011,-0.05 -0.76,-2.99 c -0.014,-0.06 -0.036,-0.11 -0.063,-0.15 -0.028,-0.04 -0.074,-0.07 -0.141,-0.09 -0.066,-0.02 -0.156,-0.04 -0.269,-0.04 -0.114,-0.01 -0.261,-0.02 -0.441,-0.02 -0.184,0 -0.331,0.01 -0.443,0.02 -0.112,0.01 -0.201,0.02 -0.265,0.05 -0.064,0.02 -0.111,0.05 -0.14,0.09 -0.03,0.03 -0.052,0.08 -0.066,0.14 l -1.3,4.45 c -0.033,0.1 -0.054,0.19 -0.063,0.25 -0.01,0.06 -0.014,0.11 -0.014,0.15 0,0.04 0.01,0.07 0.03,0.1 0.02,0.02 0.057,0.05 0.11,0.06 0.053,0.02 0.125,0.03 0.215,0.04 0.09,0 0.203,0.01 0.339,0.01 z m -10.801,0.09 c 0.146,0 0.291,-0.02 0.432,-0.04 0.141,-0.03 0.273,-0.06 0.396,-0.11 0.123,-0.04 0.234,-0.09 0.331,-0.15 0.097,-0.05 0.166,-0.1 0.206,-0.14 0.041,-0.04 0.069,-0.07 0.086,-0.1 0.016,-0.03 0.029,-0.06 0.038,-0.11 0.01,-0.04 0.017,-0.1 0.022,-0.16 0.006,-0.06 0.009,-0.14 0.009,-0.23 0,-0.22 -0.019,-0.37 -0.055,-0.45 -0.037,-0.09 -0.085,-0.13 -0.144,-0.13 -0.062,0 -0.128,0.02 -0.198,0.07 -0.07,0.06 -0.152,0.11 -0.248,0.18 -0.095,0.06 -0.209,0.11 -0.341,0.17 -0.132,0.05 -0.29,0.07 -0.474,0.07 -0.36,0 -0.635,-0.14 -0.826,-0.41 -0.191,-0.28 -0.286,-0.69 -0.286,-1.22 0,-0.27 0.023,-0.5 0.071,-0.7 0.048,-0.2 0.119,-0.37 0.212,-0.51 0.094,-0.13 0.211,-0.24 0.353,-0.3 0.141,-0.07 0.305,-0.11 0.493,-0.11 0.191,0 0.355,0.03 0.493,0.09 0.137,0.06 0.258,0.12 0.36,0.19 0.103,0.07 0.189,0.13 0.259,0.19 0.07,0.06 0.129,0.08 0.176,0.08 0.034,0 0.061,0 0.083,-0.02 0.022,-0.02 0.04,-0.06 0.052,-0.11 0.013,-0.04 0.023,-0.11 0.031,-0.19 0.007,-0.07 0.011,-0.17 0.011,-0.29 0,-0.1 -0.003,-0.18 -0.009,-0.24 -0.005,-0.07 -0.012,-0.12 -0.022,-0.16 -0.009,-0.05 -0.02,-0.08 -0.033,-0.11 -0.012,-0.03 -0.041,-0.06 -0.085,-0.11 -0.044,-0.04 -0.119,-0.09 -0.226,-0.15 -0.106,-0.06 -0.227,-0.12 -0.363,-0.16 -0.136,-0.05 -0.284,-0.09 -0.444,-0.12 -0.159,-0.03 -0.324,-0.04 -0.493,-0.04 -0.378,0 -0.713,0.06 -1.005,0.17 -0.292,0.12 -0.537,0.29 -0.735,0.52 -0.198,0.23 -0.348,0.51 -0.449,0.84 -0.101,0.33 -0.151,0.71 -0.151,1.14 0,0.49 0.061,0.92 0.184,1.27 0.123,0.36 0.295,0.66 0.515,0.89 0.22,0.24 0.481,0.41 0.782,0.53 0.301,0.11 0.632,0.17 0.992,0.17 z m 75.732,0 c 0.33,0 0.61,-0.05 0.84,-0.16 0.23,-0.11 0.42,-0.26 0.56,-0.45 0.14,-0.19 0.24,-0.41 0.3,-0.67 0.07,-0.25 0.1,-0.55 0.1,-0.91 v -3.03 c 0,-0.04 -0.01,-0.07 -0.04,-0.1 -0.02,-0.03 -0.06,-0.05 -0.11,-0.07 -0.05,-0.02 -0.12,-0.03 -0.21,-0.04 -0.09,-0.01 -0.2,-0.02 -0.33,-0.02 -0.14,0 -0.25,0.01 -0.34,0.02 -0.09,0.01 -0.16,0.02 -0.21,0.04 -0.05,0.02 -0.09,0.04 -0.11,0.07 -0.02,0.03 -0.04,0.06 -0.04,0.1 v 2.8 c 0,0.24 -0.01,0.42 -0.05,0.56 -0.03,0.14 -0.08,0.25 -0.15,0.35 -0.07,0.1 -0.15,0.17 -0.26,0.23 -0.1,0.05 -0.22,0.08 -0.36,0.08 -0.18,0 -0.36,-0.07 -0.54,-0.2 -0.18,-0.13 -0.36,-0.31 -0.56,-0.56 v -3.26 c 0,-0.04 -0.01,-0.07 -0.03,-0.1 -0.02,-0.03 -0.06,-0.05 -0.11,-0.07 -0.06,-0.02 -0.13,-0.03 -0.22,-0.04 -0.08,-0.01 -0.2,-0.02 -0.33,-0.02 -0.14,0 -0.25,0.01 -0.34,0.02 -0.09,0.01 -0.16,0.02 -0.21,0.04 -0.05,0.02 -0.09,0.04 -0.11,0.07 -0.03,0.03 -0.04,0.06 -0.04,0.1 v 4.91 c 0,0.03 0.01,0.07 0.03,0.09 0.02,0.03 0.05,0.06 0.1,0.07 0.05,0.02 0.11,0.04 0.19,0.04 0.07,0.01 0.16,0.02 0.28,0.02 0.12,0 0.21,-0.01 0.29,-0.02 0.08,0 0.14,-0.02 0.18,-0.04 0.04,-0.01 0.07,-0.04 0.09,-0.07 0.02,-0.02 0.03,-0.06 0.03,-0.09 v -0.57 c 0.27,0.29 0.55,0.52 0.83,0.66 0.28,0.15 0.57,0.22 0.88,0.22 z m -14.98,0 c 0.44,0 0.83,-0.06 1.15,-0.18 0.32,-0.12 0.59,-0.29 0.8,-0.52 0.21,-0.23 0.37,-0.52 0.47,-0.86 0.1,-0.33 0.16,-0.72 0.16,-1.16 0,-0.42 -0.06,-0.8 -0.17,-1.14 -0.11,-0.35 -0.28,-0.65 -0.5,-0.9 -0.22,-0.25 -0.51,-0.44 -0.84,-0.57 -0.34,-0.14 -0.74,-0.21 -1.19,-0.21 -0.43,0 -0.81,0.06 -1.14,0.18 -0.32,0.12 -0.59,0.3 -0.8,0.53 -0.21,0.23 -0.37,0.52 -0.48,0.86 -0.1,0.33 -0.15,0.72 -0.15,1.15 0,0.42 0.06,0.8 0.17,1.15 0.11,0.35 0.28,0.64 0.5,0.89 0.23,0.25 0.51,0.44 0.84,0.58 0.34,0.13 0.73,0.2 1.18,0.2 z m -6.67,0 c 0.05,0 0.1,0 0.15,-0.01 0.06,0 0.12,-0.01 0.17,-0.02 0.06,-0.02 0.11,-0.03 0.16,-0.05 0.04,-0.01 0.07,-0.03 0.1,-0.05 0.02,-0.02 0.03,-0.04 0.04,-0.06 0.01,-0.02 0.02,-0.05 0.02,-0.09 0.01,-0.04 0.02,-0.1 0.02,-0.17 0,-0.08 0.01,-0.19 0.01,-0.32 0,-0.13 -0.01,-0.24 -0.02,-0.32 0,-0.09 -0.01,-0.15 -0.03,-0.2 -0.01,-0.05 -0.03,-0.08 -0.06,-0.1 -0.02,-0.02 -0.05,-0.03 -0.09,-0.03 -0.03,0 -0.06,0.01 -0.1,0.02 -0.03,0.01 -0.07,0.03 -0.12,0.04 -0.05,0.02 -0.1,0.03 -0.15,0.04 -0.06,0.02 -0.12,0.02 -0.18,0.02 -0.08,0 -0.16,-0.01 -0.23,-0.04 -0.08,-0.04 -0.16,-0.08 -0.24,-0.15 -0.09,-0.07 -0.17,-0.15 -0.26,-0.26 -0.09,-0.11 -0.19,-0.25 -0.29,-0.41 v -3.06 c 0,-0.04 -0.01,-0.07 -0.04,-0.1 -0.02,-0.03 -0.05,-0.05 -0.11,-0.07 -0.05,-0.02 -0.12,-0.03 -0.21,-0.04 -0.09,-0.01 -0.2,-0.02 -0.34,-0.02 -0.13,0 -0.24,0.01 -0.33,0.02 -0.09,0.01 -0.16,0.02 -0.21,0.04 -0.06,0.02 -0.09,0.04 -0.12,0.07 -0.02,0.03 -0.03,0.06 -0.03,0.1 v 4.91 c 0,0.03 0.01,0.07 0.03,0.09 0.02,0.03 0.05,0.06 0.1,0.07 0.04,0.02 0.11,0.04 0.18,0.04 0.08,0.01 0.17,0.02 0.28,0.02 0.12,0 0.22,-0.01 0.3,-0.02 0.07,0 0.13,-0.02 0.18,-0.04 0.04,-0.01 0.07,-0.04 0.09,-0.07 0.01,-0.02 0.02,-0.06 0.02,-0.09 v -0.61 c 0.13,0.18 0.25,0.33 0.37,0.45 0.11,0.12 0.22,0.22 0.32,0.28 0.11,0.07 0.21,0.12 0.31,0.15 0.1,0.03 0.21,0.04 0.31,0.04 z m -12.13,0 c 0.44,0 0.83,-0.06 1.15,-0.18 0.32,-0.12 0.59,-0.29 0.8,-0.52 0.21,-0.23 0.37,-0.52 0.47,-0.86 0.1,-0.33 0.16,-0.72 0.16,-1.16 0,-0.42 -0.06,-0.8 -0.17,-1.14 -0.11,-0.35 -0.28,-0.65 -0.5,-0.9 -0.22,-0.25 -0.51,-0.44 -0.84,-0.57 -0.34,-0.14 -0.74,-0.21 -1.19,-0.21 -0.43,0 -0.81,0.06 -1.14,0.18 -0.32,0.12 -0.59,0.3 -0.8,0.53 -0.21,0.23 -0.37,0.52 -0.48,0.86 -0.1,0.33 -0.15,0.72 -0.15,1.15 0,0.42 0.06,0.8 0.17,1.15 0.11,0.35 0.28,0.64 0.5,0.89 0.23,0.25 0.51,0.44 0.84,0.58 0.34,0.13 0.73,0.2 1.18,0.2 z m -37.208,0 c 0.411,0 0.762,-0.06 1.054,-0.18 0.292,-0.12 0.532,-0.29 0.719,-0.5 0.187,-0.22 0.325,-0.47 0.413,-0.77 0.088,-0.29 0.132,-0.61 0.132,-0.95 v -0.23 c 0,-0.16 -0.037,-0.29 -0.113,-0.37 -0.075,-0.08 -0.179,-0.12 -0.311,-0.12 h -2.99 c 0,-0.21 0.024,-0.4 0.074,-0.57 0.049,-0.17 0.128,-0.32 0.237,-0.44 0.108,-0.12 0.248,-0.21 0.418,-0.27 0.171,-0.06 0.376,-0.09 0.614,-0.09 0.243,0 0.456,0.02 0.639,0.05 0.184,0.04 0.343,0.07 0.477,0.12 0.134,0.04 0.245,0.08 0.333,0.11 0.088,0.04 0.16,0.05 0.215,0.05 0.033,0 0.06,0 0.082,-0.02 0.022,-0.01 0.041,-0.03 0.056,-0.06 0.014,-0.04 0.024,-0.08 0.03,-0.14 0.005,-0.07 0.008,-0.14 0.008,-0.23 0,-0.08 -0.002,-0.15 -0.005,-0.21 -0.004,-0.06 -0.01,-0.11 -0.017,-0.15 -0.007,-0.04 -0.018,-0.07 -0.033,-0.1 -0.015,-0.03 -0.034,-0.05 -0.058,-0.08 -0.024,-0.02 -0.089,-0.06 -0.195,-0.1 -0.107,-0.05 -0.243,-0.09 -0.408,-0.13 -0.165,-0.04 -0.354,-0.08 -0.567,-0.11 -0.213,-0.03 -0.441,-0.05 -0.683,-0.05 -0.437,0 -0.82,0.06 -1.149,0.17 -0.328,0.11 -0.603,0.27 -0.823,0.5 -0.22,0.22 -0.385,0.5 -0.493,0.85 -0.108,0.34 -0.162,0.74 -0.162,1.2 0,0.43 0.056,0.83 0.17,1.18 0.114,0.35 0.279,0.65 0.496,0.89 0.217,0.24 0.48,0.43 0.79,0.56 0.311,0.13 0.66,0.19 1.05,0.19 z m -10.544,0 c 0.385,0 0.715,-0.04 0.988,-0.11 0.274,-0.08 0.499,-0.19 0.675,-0.35 0.176,-0.15 0.305,-0.35 0.386,-0.6 0.08,-0.25 0.121,-0.54 0.121,-0.88 v -3.3 c 0,-0.05 -0.018,-0.09 -0.055,-0.12 -0.037,-0.03 -0.095,-0.05 -0.174,-0.07 -0.079,-0.01 -0.195,-0.02 -0.349,-0.02 -0.166,0 -0.286,0.01 -0.361,0.02 -0.075,0.02 -0.129,0.04 -0.16,0.07 -0.031,0.03 -0.047,0.07 -0.047,0.12 v 0.39 c -0.202,-0.22 -0.432,-0.39 -0.691,-0.51 -0.259,-0.12 -0.546,-0.18 -0.862,-0.18 -0.261,0 -0.5,0.04 -0.719,0.1 -0.218,0.07 -0.407,0.17 -0.567,0.31 -0.16,0.13 -0.284,0.3 -0.372,0.49 -0.088,0.2 -0.132,0.43 -0.132,0.7 0,0.28 0.056,0.53 0.168,0.74 0.112,0.22 0.279,0.39 0.501,0.53 0.222,0.14 0.499,0.24 0.829,0.3 0.331,0.07 0.714,0.1 1.151,0.1 h 0.479 v 0.3 c 0,0.15 -0.015,0.29 -0.046,0.41 -0.032,0.11 -0.083,0.21 -0.155,0.28 -0.071,0.08 -0.167,0.14 -0.286,0.18 -0.119,0.03 -0.267,0.05 -0.443,0.05 -0.232,0 -0.438,-0.03 -0.62,-0.08 -0.182,-0.05 -0.342,-0.11 -0.482,-0.17 -0.139,-0.06 -0.256,-0.12 -0.35,-0.17 -0.093,-0.05 -0.169,-0.08 -0.228,-0.08 -0.041,0 -0.076,0.02 -0.108,0.04 -0.031,0.03 -0.057,0.07 -0.077,0.11 -0.02,0.05 -0.036,0.11 -0.047,0.18 -0.011,0.07 -0.016,0.15 -0.016,0.23 0,0.11 0.009,0.2 0.027,0.27 0.019,0.07 0.054,0.13 0.105,0.18 0.051,0.05 0.141,0.11 0.27,0.18 0.128,0.06 0.279,0.12 0.452,0.17 0.172,0.06 0.36,0.1 0.564,0.14 0.204,0.03 0.414,0.05 0.631,0.05 z m -7.905,0 c 0.359,0 0.666,-0.07 0.919,-0.21 0.254,-0.14 0.46,-0.33 0.62,-0.58 0.16,-0.25 0.276,-0.53 0.35,-0.86 0.073,-0.33 0.11,-0.68 0.11,-1.05 0,-0.43 -0.047,-0.83 -0.14,-1.18 -0.094,-0.35 -0.231,-0.65 -0.411,-0.89 -0.18,-0.25 -0.404,-0.44 -0.672,-0.57 -0.268,-0.13 -0.574,-0.2 -0.92,-0.2 -0.143,0 -0.274,0.02 -0.393,0.04 -0.12,0.03 -0.235,0.07 -0.347,0.13 -0.112,0.06 -0.223,0.13 -0.334,0.21 -0.11,0.09 -0.224,0.18 -0.341,0.3 v -2.31 c 0,-0.04 -0.011,-0.07 -0.033,-0.1 -0.022,-0.03 -0.06,-0.06 -0.113,-0.08 -0.053,-0.02 -0.124,-0.03 -0.212,-0.04 -0.088,-0.02 -0.2,-0.02 -0.336,-0.02 -0.136,0 -0.248,0 -0.336,0.02 -0.088,0.01 -0.159,0.02 -0.212,0.04 -0.053,0.02 -0.091,0.05 -0.113,0.08 -0.022,0.03 -0.033,0.06 -0.033,0.1 v 6.86 c 0,0.03 0.009,0.07 0.027,0.09 0.019,0.03 0.051,0.06 0.097,0.07 0.046,0.02 0.106,0.04 0.182,0.04 0.075,0.01 0.169,0.02 0.283,0.02 0.11,0 0.203,-0.01 0.278,-0.02 0.076,0 0.136,-0.02 0.182,-0.04 0.046,-0.01 0.078,-0.04 0.097,-0.07 0.018,-0.02 0.027,-0.06 0.027,-0.09 v -0.58 c 0.143,0.15 0.284,0.28 0.421,0.39 0.138,0.11 0.279,0.2 0.422,0.28 0.143,0.07 0.291,0.13 0.443,0.17 0.153,0.03 0.315,0.05 0.488,0.05 z m -6.367,0 c 0.411,0 0.762,-0.06 1.054,-0.18 0.292,-0.12 0.532,-0.29 0.719,-0.5 0.187,-0.22 0.325,-0.47 0.413,-0.77 0.088,-0.29 0.132,-0.61 0.132,-0.95 v -0.23 c 0,-0.16 -0.037,-0.29 -0.113,-0.37 -0.075,-0.08 -0.179,-0.12 -0.311,-0.12 h -2.99 c 0,-0.21 0.024,-0.4 0.074,-0.57 0.049,-0.17 0.128,-0.32 0.237,-0.44 0.108,-0.12 0.248,-0.21 0.418,-0.27 0.171,-0.06 0.376,-0.09 0.614,-0.09 0.243,0 0.456,0.02 0.639,0.05 0.184,0.04 0.343,0.07 0.477,0.12 0.134,0.04 0.245,0.08 0.333,0.11 0.088,0.04 0.16,0.05 0.215,0.05 0.033,0 0.06,0 0.082,-0.02 0.022,-0.01 0.041,-0.03 0.056,-0.06 0.014,-0.04 0.024,-0.08 0.03,-0.14 0.005,-0.07 0.008,-0.14 0.008,-0.23 0,-0.08 -0.002,-0.15 -0.005,-0.21 -0.004,-0.06 -0.01,-0.11 -0.017,-0.15 -0.007,-0.04 -0.018,-0.07 -0.033,-0.1 -0.015,-0.03 -0.034,-0.05 -0.058,-0.08 -0.024,-0.02 -0.089,-0.06 -0.195,-0.1 -0.107,-0.05 -0.243,-0.09 -0.408,-0.13 -0.165,-0.04 -0.354,-0.08 -0.567,-0.11 -0.213,-0.03 -0.441,-0.05 -0.683,-0.05 -0.437,0 -0.82,0.06 -1.149,0.17 -0.328,0.11 -0.603,0.27 -0.823,0.5 -0.22,0.22 -0.385,0.5 -0.493,0.85 -0.108,0.34 -0.162,0.74 -0.162,1.2 0,0.43 0.056,0.83 0.17,1.18 0.114,0.35 0.279,0.65 0.496,0.89 0.217,0.24 0.48,0.43 0.79,0.56 0.311,0.13 0.66,0.19 1.05,0.19 z m -7.081,0.61 v -2.02 h 0.738 c 0.206,0 0.386,0.02 0.54,0.07 0.154,0.05 0.283,0.12 0.385,0.21 0.103,0.09 0.18,0.2 0.232,0.32 0.051,0.13 0.077,0.27 0.077,0.42 0,0.24 -0.053,0.43 -0.16,0.6 -0.106,0.16 -0.281,0.27 -0.523,0.34 -0.073,0.02 -0.157,0.03 -0.251,0.04 -0.093,0.01 -0.223,0.02 -0.388,0.02 z m 131.325,0.04 c -0.35,0 -0.65,-0.06 -0.89,-0.2 -0.24,-0.13 -0.43,-0.31 -0.58,-0.54 -0.14,-0.22 -0.25,-0.48 -0.31,-0.78 -0.06,-0.3 -0.09,-0.62 -0.09,-0.95 0,-0.39 0.03,-0.74 0.09,-1.05 0.06,-0.31 0.16,-0.58 0.3,-0.8 0.14,-0.22 0.32,-0.38 0.56,-0.5 0.23,-0.12 0.53,-0.17 0.88,-0.17 0.36,0 0.66,0.06 0.9,0.19 0.24,0.14 0.43,0.32 0.58,0.54 0.14,0.23 0.25,0.5 0.31,0.8 0.06,0.3 0.09,0.63 0.09,0.97 0,0.37 -0.03,0.71 -0.09,1.02 -0.06,0.31 -0.16,0.57 -0.3,0.79 -0.14,0.21 -0.33,0.38 -0.57,0.5 -0.23,0.12 -0.52,0.18 -0.88,0.18 z m -14.85,0 c -0.35,0 -0.65,-0.06 -0.89,-0.2 -0.24,-0.13 -0.43,-0.31 -0.58,-0.54 -0.15,-0.22 -0.25,-0.48 -0.31,-0.78 -0.06,-0.3 -0.09,-0.62 -0.09,-0.95 0,-0.39 0.03,-0.74 0.09,-1.05 0.06,-0.31 0.15,-0.58 0.29,-0.8 0.14,-0.22 0.33,-0.38 0.57,-0.5 0.23,-0.12 0.53,-0.17 0.88,-0.17 0.36,0 0.66,0.06 0.9,0.19 0.23,0.14 0.43,0.32 0.57,0.54 0.15,0.23 0.25,0.5 0.31,0.8 0.07,0.3 0.1,0.63 0.1,0.97 0,0.37 -0.03,0.71 -0.09,1.02 -0.06,0.31 -0.16,0.57 -0.3,0.79 -0.14,0.21 -0.33,0.38 -0.57,0.5 -0.23,0.12 -0.53,0.18 -0.88,0.18 z m -66.51,0.55 c 0.14,0 0.25,0 0.34,-0.01 0.09,-0.01 0.16,-0.02 0.21,-0.04 0.05,-0.03 0.09,-0.05 0.11,-0.08 0.02,-0.03 0.04,-0.06 0.04,-0.1 v -1.09 h 1.06 c 0.04,0 0.07,-0.01 0.1,-0.03 0.03,-0.02 0.05,-0.05 0.07,-0.09 0.02,-0.05 0.03,-0.1 0.04,-0.18 0.01,-0.07 0.01,-0.16 0.01,-0.27 0,-0.2 -0.01,-0.34 -0.05,-0.43 -0.04,-0.08 -0.09,-0.13 -0.16,-0.13 h -1.07 v -2.31 c 0,-0.26 0.04,-0.46 0.12,-0.6 0.09,-0.13 0.24,-0.2 0.45,-0.2 0.08,0 0.14,0.01 0.2,0.02 0.06,0.01 0.11,0.03 0.16,0.04 0.04,0.02 0.08,0.03 0.11,0.05 0.04,0.01 0.06,0.02 0.09,0.02 0.02,0 0.04,-0.01 0.06,-0.02 0.02,-0.02 0.04,-0.04 0.05,-0.08 0.01,-0.04 0.02,-0.09 0.03,-0.16 0.01,-0.06 0.01,-0.15 0.01,-0.25 0,-0.16 -0.01,-0.29 -0.03,-0.37 -0.02,-0.09 -0.04,-0.15 -0.08,-0.18 -0.03,-0.04 -0.08,-0.07 -0.14,-0.1 -0.07,-0.02 -0.14,-0.05 -0.23,-0.07 -0.08,-0.02 -0.18,-0.03 -0.28,-0.05 -0.1,-0.01 -0.2,-0.01 -0.3,-0.01 -0.28,0 -0.52,0.03 -0.72,0.1 -0.2,0.07 -0.37,0.18 -0.5,0.32 -0.13,0.15 -0.23,0.33 -0.29,0.55 -0.07,0.22 -0.1,0.48 -0.1,0.78 v 2.52 h -0.59 c -0.07,0 -0.12,0.05 -0.16,0.13 -0.03,0.09 -0.05,0.23 -0.05,0.43 0,0.11 0,0.2 0.01,0.27 0.01,0.08 0.03,0.13 0.04,0.18 0.02,0.04 0.05,0.07 0.07,0.09 0.03,0.02 0.06,0.03 0.1,0.03 h 0.58 v 1.09 c 0,0.04 0.01,0.07 0.03,0.1 0.02,0.03 0.06,0.05 0.11,0.08 0.06,0.02 0.13,0.03 0.22,0.04 0.09,0.01 0.2,0.01 0.33,0.01 z m -50.984,0.52 h 1.845 c 0.188,0 0.342,-0.01 0.463,-0.02 0.121,0 0.231,-0.01 0.33,-0.02 0.287,-0.04 0.545,-0.11 0.774,-0.21 0.23,-0.1 0.424,-0.23 0.584,-0.39 0.16,-0.16 0.282,-0.35 0.366,-0.57 0.085,-0.22 0.127,-0.47 0.127,-0.75 0,-0.24 -0.03,-0.46 -0.091,-0.65 -0.06,-0.2 -0.149,-0.37 -0.267,-0.53 -0.117,-0.15 -0.263,-0.29 -0.435,-0.4 -0.173,-0.11 -0.369,-0.21 -0.589,-0.28 0.106,-0.05 0.206,-0.11 0.3,-0.18 0.093,-0.08 0.182,-0.17 0.264,-0.27 0.083,-0.11 0.161,-0.22 0.234,-0.36 0.074,-0.13 0.145,-0.28 0.215,-0.45 l 0.6,-1.41 c 0.055,-0.14 0.092,-0.24 0.111,-0.3 0.018,-0.07 0.027,-0.12 0.027,-0.15 0,-0.05 -0.007,-0.08 -0.022,-0.11 -0.015,-0.02 -0.05,-0.05 -0.105,-0.07 -0.055,-0.01 -0.135,-0.03 -0.242,-0.03 -0.106,-0.01 -0.251,-0.02 -0.435,-0.02 -0.154,0 -0.277,0.01 -0.369,0.02 -0.092,0 -0.164,0.02 -0.218,0.04 -0.053,0.02 -0.091,0.04 -0.113,0.07 -0.022,0.04 -0.04,0.07 -0.055,0.12 l -0.639,1.59 c -0.077,0.18 -0.152,0.34 -0.225,0.48 -0.074,0.14 -0.156,0.25 -0.245,0.35 -0.09,0.09 -0.194,0.16 -0.312,0.21 -0.117,0.05 -0.253,0.07 -0.407,0.07 h -0.452 v -2.71 c 0,-0.04 -0.012,-0.07 -0.036,-0.1 -0.024,-0.03 -0.063,-0.06 -0.118,-0.07 -0.055,-0.02 -0.129,-0.04 -0.22,-0.05 -0.092,-0.01 -0.21,-0.02 -0.353,-0.02 -0.139,0 -0.256,0.01 -0.35,0.02 -0.093,0.01 -0.168,0.03 -0.223,0.05 -0.055,0.01 -0.093,0.04 -0.115,0.07 -0.022,0.03 -0.033,0.06 -0.033,0.1 v 6.47 c 0,0.16 0.041,0.28 0.123,0.35 0.083,0.07 0.185,0.11 0.306,0.11 z m 110.464,0.03 c 0.14,0 0.26,-0.01 0.35,-0.02 0.1,-0.01 0.17,-0.02 0.22,-0.04 0.06,-0.02 0.1,-0.04 0.12,-0.07 0.03,-0.03 0.04,-0.07 0.04,-0.1 v -5.74 h 2.24 c 0.04,0 0.07,-0.01 0.1,-0.03 0.02,-0.02 0.05,-0.05 0.07,-0.1 0.01,-0.04 0.03,-0.1 0.04,-0.18 0.01,-0.07 0.01,-0.17 0.01,-0.28 0,-0.11 0,-0.2 -0.01,-0.27 -0.01,-0.08 -0.03,-0.14 -0.04,-0.19 -0.02,-0.05 -0.05,-0.09 -0.07,-0.11 -0.03,-0.02 -0.06,-0.03 -0.1,-0.03 h -3.27 c -0.12,0 -0.22,0.03 -0.3,0.11 -0.08,0.07 -0.13,0.18 -0.13,0.35 v 6.47 c 0,0.03 0.02,0.07 0.04,0.1 0.02,0.03 0.06,0.05 0.12,0.07 0.05,0.02 0.13,0.03 0.22,0.04 0.1,0.01 0.21,0.02 0.35,0.02 z m 15.02,0.09 c 0.31,0 0.59,-0.02 0.84,-0.07 0.26,-0.04 0.48,-0.09 0.67,-0.15 0.19,-0.06 0.34,-0.13 0.47,-0.2 0.12,-0.07 0.21,-0.13 0.26,-0.18 0.05,-0.05 0.08,-0.12 0.11,-0.22 0.02,-0.09 0.03,-0.23 0.03,-0.41 0,-0.11 0,-0.2 -0.01,-0.28 -0.01,-0.07 -0.03,-0.14 -0.04,-0.18 -0.02,-0.05 -0.04,-0.08 -0.06,-0.1 -0.03,-0.02 -0.06,-0.03 -0.09,-0.03 -0.05,0 -0.12,0.04 -0.23,0.1 -0.11,0.07 -0.25,0.14 -0.42,0.22 -0.18,0.08 -0.39,0.15 -0.63,0.21 -0.25,0.07 -0.53,0.1 -0.86,0.1 -0.34,0 -0.65,-0.06 -0.93,-0.18 -0.28,-0.12 -0.52,-0.29 -0.72,-0.5 -0.2,-0.22 -0.35,-0.48 -0.46,-0.79 -0.1,-0.31 -0.15,-0.65 -0.15,-1.02 0,-0.41 0.05,-0.76 0.16,-1.07 0.11,-0.31 0.26,-0.57 0.45,-0.78 0.2,-0.21 0.43,-0.37 0.7,-0.48 0.28,-0.1 0.58,-0.16 0.91,-0.16 0.16,0 0.32,0.02 0.48,0.06 0.16,0.03 0.3,0.09 0.44,0.16 v 1.66 h -1.35 c -0.07,0 -0.12,0.04 -0.15,0.12 -0.04,0.08 -0.06,0.22 -0.06,0.42 0,0.1 0.01,0.19 0.02,0.26 0,0.07 0.02,0.13 0.04,0.17 0.01,0.04 0.04,0.07 0.06,0.09 0.03,0.02 0.06,0.03 0.09,0.03 h 2.41 c 0.06,0 0.11,-0.01 0.15,-0.03 0.05,-0.02 0.09,-0.05 0.13,-0.09 0.03,-0.04 0.06,-0.09 0.08,-0.15 0.01,-0.05 0.02,-0.12 0.02,-0.19 v -2.84 c 0,-0.11 -0.02,-0.2 -0.05,-0.29 -0.04,-0.08 -0.12,-0.14 -0.24,-0.19 -0.12,-0.05 -0.27,-0.1 -0.44,-0.15 -0.18,-0.05 -0.36,-0.1 -0.55,-0.13 -0.19,-0.04 -0.38,-0.07 -0.58,-0.09 -0.19,-0.01 -0.38,-0.02 -0.58,-0.02 -0.57,0 -1.08,0.08 -1.53,0.24 -0.45,0.17 -0.83,0.4 -1.14,0.71 -0.31,0.31 -0.55,0.69 -0.71,1.14 -0.17,0.44 -0.25,0.95 -0.25,1.51 0,0.59 0.09,1.11 0.26,1.58 0.18,0.46 0.43,0.86 0.75,1.18 0.32,0.33 0.71,0.57 1.17,0.75 0.46,0.17 0.97,0.26 1.53,0.26 z m 6.91,0 c 0.54,0 1.02,-0.07 1.43,-0.21 0.42,-0.15 0.76,-0.37 1.04,-0.66 0.27,-0.3 0.48,-0.68 0.63,-1.13 0.14,-0.45 0.21,-0.99 0.21,-1.61 0,-0.59 -0.08,-1.12 -0.22,-1.58 -0.15,-0.47 -0.37,-0.86 -0.66,-1.19 -0.29,-0.32 -0.65,-0.57 -1.07,-0.74 -0.43,-0.17 -0.93,-0.26 -1.49,-0.26 -0.55,0 -1.04,0.07 -1.45,0.22 -0.41,0.14 -0.76,0.37 -1.03,0.66 -0.28,0.3 -0.49,0.68 -0.62,1.14 -0.14,0.46 -0.21,1 -0.21,1.63 0,0.57 0.07,1.09 0.22,1.55 0.15,0.46 0.37,0.86 0.66,1.18 0.29,0.32 0.65,0.57 1.07,0.74 0.43,0.18 0.93,0.26 1.49,0.26 z m -14.85,0 c 0.54,0 1.02,-0.07 1.43,-0.21 0.41,-0.15 0.76,-0.37 1.04,-0.66 0.27,-0.3 0.48,-0.68 0.62,-1.13 0.14,-0.45 0.22,-0.99 0.22,-1.61 0,-0.59 -0.08,-1.12 -0.22,-1.58 -0.15,-0.47 -0.37,-0.86 -0.66,-1.19 -0.29,-0.32 -0.65,-0.57 -1.08,-0.74 -0.42,-0.17 -0.92,-0.26 -1.48,-0.26 -0.56,0 -1.04,0.07 -1.45,0.22 -0.42,0.14 -0.76,0.37 -1.04,0.66 -0.27,0.3 -0.48,0.68 -0.61,1.14 -0.14,0.46 -0.21,1 -0.21,1.63 0,0.57 0.07,1.09 0.22,1.55 0.15,0.46 0.36,0.86 0.65,1.18 0.29,0.32 0.65,0.57 1.08,0.74 0.43,0.18 0.93,0.26 1.49,0.26 z m -69.6,0.23 c 0.3,0 0.51,-0.05 0.62,-0.16 0.11,-0.1 0.17,-0.29 0.17,-0.56 0,-0.28 -0.06,-0.47 -0.17,-0.58 -0.12,-0.1 -0.33,-0.16 -0.64,-0.16 -0.31,0 -0.52,0.05 -0.63,0.16 -0.1,0.1 -0.16,0.29 -0.16,0.55 0,0.28 0.06,0.48 0.17,0.59 0.11,0.1 0.32,0.16 0.64,0.16 z m 6.76,0.18 c 0.13,0 0.25,0 0.33,-0.01 0.09,-0.01 0.16,-0.03 0.22,-0.05 0.05,-0.02 0.09,-0.04 0.11,-0.07 0.02,-0.03 0.03,-0.07 0.03,-0.1 v -2.72 c 0.24,0.24 0.48,0.41 0.73,0.53 0.25,0.11 0.51,0.17 0.78,0.17 0.33,0 0.61,-0.05 0.84,-0.16 0.23,-0.11 0.42,-0.26 0.56,-0.45 0.14,-0.2 0.24,-0.42 0.3,-0.67 0.07,-0.26 0.1,-0.57 0.1,-0.93 v -3.01 c 0,-0.04 -0.01,-0.07 -0.04,-0.1 -0.02,-0.03 -0.06,-0.05 -0.11,-0.07 -0.05,-0.02 -0.12,-0.03 -0.21,-0.04 -0.09,-0.01 -0.2,-0.02 -0.33,-0.02 -0.14,0 -0.25,0.01 -0.34,0.02 -0.09,0.01 -0.16,0.02 -0.21,0.04 -0.05,0.02 -0.09,0.04 -0.11,0.07 -0.02,0.03 -0.04,0.06 -0.04,0.1 v 2.8 c 0,0.24 -0.01,0.42 -0.05,0.56 -0.03,0.14 -0.08,0.25 -0.15,0.35 -0.07,0.1 -0.15,0.17 -0.26,0.23 -0.1,0.05 -0.22,0.08 -0.36,0.08 -0.18,0 -0.36,-0.07 -0.54,-0.2 -0.18,-0.13 -0.36,-0.31 -0.56,-0.56 v -3.26 c 0,-0.04 -0.01,-0.07 -0.03,-0.1 -0.02,-0.03 -0.06,-0.05 -0.11,-0.07 -0.06,-0.02 -0.13,-0.03 -0.22,-0.04 -0.08,-0.01 -0.2,-0.02 -0.33,-0.02 -0.14,0 -0.25,0.01 -0.34,0.02 -0.09,0.01 -0.16,0.02 -0.21,0.04 -0.05,0.02 -0.09,0.04 -0.11,0.07 -0.03,0.03 -0.04,0.06 -0.04,0.1 v 7.24 c 0,0.03 0.01,0.07 0.04,0.1 0.02,0.03 0.06,0.05 0.11,0.07 0.05,0.02 0.12,0.04 0.21,0.05 0.09,0.01 0.2,0.01 0.34,0.01 z m -36.474,0 c 0.136,0 0.248,0 0.336,-0.01 0.088,-0.01 0.159,-0.03 0.212,-0.05 0.053,-0.02 0.091,-0.04 0.113,-0.07 0.022,-0.03 0.033,-0.07 0.033,-0.1 v -7.24 c 0,-0.04 -0.011,-0.07 -0.033,-0.1 -0.022,-0.03 -0.06,-0.05 -0.113,-0.07 -0.053,-0.02 -0.124,-0.03 -0.212,-0.04 -0.088,-0.01 -0.2,-0.02 -0.336,-0.02 -0.136,0 -0.248,0.01 -0.336,0.02 -0.088,0.01 -0.159,0.02 -0.212,0.04 -0.053,0.02 -0.091,0.04 -0.113,0.07 -0.022,0.03 -0.033,0.06 -0.033,0.1 v 7.24 c 0,0.03 0.011,0.07 0.033,0.1 0.022,0.03 0.06,0.05 0.113,0.07 0.053,0.02 0.124,0.04 0.212,0.05 0.088,0.01 0.2,0.01 0.336,0.01 z m 119.134,0.18 c 0.11,0 0.21,0 0.28,-0.01 0.07,0 0.13,-0.01 0.18,-0.02 0.05,-0.02 0.08,-0.03 0.1,-0.05 0.02,-0.01 0.04,-0.03 0.05,-0.05 0.38,-0.77 0.67,-1.55 0.87,-2.33 0.2,-0.79 0.3,-1.6 0.3,-2.42 0,-0.41 -0.03,-0.82 -0.07,-1.22 -0.05,-0.41 -0.12,-0.8 -0.22,-1.2 -0.1,-0.39 -0.22,-0.79 -0.37,-1.17 -0.14,-0.39 -0.31,-0.77 -0.51,-1.16 -0.01,-0.02 -0.03,-0.05 -0.06,-0.07 -0.03,-0.02 -0.06,-0.03 -0.11,-0.04 -0.05,-0.02 -0.12,-0.03 -0.19,-0.04 -0.07,0 -0.16,-0.01 -0.26,-0.01 -0.15,0 -0.27,0.01 -0.35,0.03 -0.09,0.01 -0.15,0.03 -0.19,0.06 -0.04,0.03 -0.06,0.07 -0.06,0.11 0,0.04 0.01,0.09 0.04,0.15 0.3,0.72 0.53,1.46 0.69,2.22 0.16,0.77 0.24,1.55 0.24,2.35 0,0.8 -0.08,1.58 -0.24,2.35 -0.15,0.76 -0.39,1.5 -0.7,2.21 -0.02,0.05 -0.03,0.09 -0.02,0.13 0.01,0.05 0.03,0.08 0.08,0.1 0.04,0.03 0.11,0.05 0.19,0.06 0.08,0.01 0.19,0.02 0.33,0.02 z m -139.768,0 c 0.132,0 0.24,-0.01 0.325,-0.02 0.084,-0.01 0.149,-0.03 0.193,-0.06 0.044,-0.02 0.068,-0.05 0.074,-0.1 0.005,-0.04 -10e-4,-0.08 -0.019,-0.13 -0.309,-0.71 -0.542,-1.45 -0.7,-2.21 -0.158,-0.77 -0.237,-1.55 -0.237,-2.35 0,-0.8 0.08,-1.58 0.24,-2.34 0.16,-0.76 0.39,-1.5 0.691,-2.23 0.022,-0.06 0.033,-0.11 0.033,-0.15 0,-0.04 -0.02,-0.08 -0.06,-0.11 -0.041,-0.03 -0.102,-0.05 -0.185,-0.06 -0.083,-0.02 -0.197,-0.03 -0.344,-0.03 -0.103,0 -0.191,0.01 -0.265,0.01 -0.073,0.01 -0.134,0.02 -0.184,0.04 -0.05,0.01 -0.088,0.02 -0.116,0.04 -0.027,0.02 -0.047,0.05 -0.058,0.07 -0.198,0.39 -0.37,0.77 -0.517,1.16 -0.147,0.38 -0.269,0.78 -0.367,1.17 -0.097,0.4 -0.169,0.79 -0.217,1.2 -0.048,0.4 -0.072,0.81 -0.072,1.22 0,0.41 0.025,0.82 0.075,1.22 0.049,0.4 0.124,0.8 0.223,1.2 0.099,0.39 0.221,0.78 0.366,1.17 0.145,0.39 0.315,0.78 0.509,1.16 0.008,0.02 0.022,0.04 0.045,0.05 0.022,0.02 0.055,0.03 0.099,0.05 0.044,0.01 0.103,0.02 0.179,0.02 0.075,0.01 0.171,0.01 0.289,0.01 z" /></g><g
+ id="g2523"
+ transform="translate(0,-3.6020071)"><path
+ inkscape:connector-curvature="0"
+ id="path22"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 387.73,627.75 h 159.05 v 19 H 387.73 Z" /><text
+ y="-650.38"
+ x="89.025002"
+ id="text58"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ id="tspan56"
+ sodipodi:role="line"
+ y="-650.38"
+ x="89.025002 95.228996 98.829201 104.6196 110.41 112.995 118.8042">BILL TO</tspan></text>
+<text
+ y="-634.15002"
+ x="-93.542221"
+ id="text62"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.9486928"
+ transform="matrix(1,0,-0.33330001,-1,0,0)"><tspan
+ style="stroke-width:0.9486928"
+ id="tspan60"
+ y="-634.15002"
+ x="-93.542221">#</tspan></text>
+<text
+ y="-615.34998"
+ x="-105.87117"
+ id="text66"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.9486928"
+ transform="matrix(1,0,-0.33330001,-1,0,0)"><tspan
+ style="stroke-width:0.9486928"
+ id="tspan64"
+ sodipodi:role="line"
+ y="-615.34998"
+ x="-105.87117 -99.084373 -94.280983 -86.478981">Name</tspan></text>
+<text
+ y="-596.54999"
+ x="-108.80513"
+ id="text70"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.9486928"
+ transform="matrix(1,0,-0.33330001,-1,0,0)"><tspan
+ style="stroke-width:0.9486928"
+ id="tspan68"
+ sodipodi:role="line"
+ y="-596.54999"
+ x="-108.80513 -102.60115 -97.374741 -92.204742 -88.604538 -83.810539 -79.63694">Address</tspan></text>
+<text
+ y="-577.75"
+ x="-132.36406"
+ id="text74"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.9486928"
+ transform="matrix(1,0,-0.33330001,-1,0,0)"><tspan
+ style="stroke-width:0.9486928"
+ id="tspan72"
+ sodipodi:role="line"
+ y="-577.75"
+ x="-132.36406 -126.16008 -123.54689 -119.9561 -115.16209 -112.5489 -109.96389 -104.1735 -100.5733 -95.779297 -92.1791 -87.385101 -84.800102 -78.990898 -75.390701">City, State ZIP</tspan></text>
+<text
+ y="-558.95001"
+ x="-96.673058"
+ id="text78"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.9486928"
+ transform="matrix(1,0,-0.33330001,-1,0,0)"><tspan
+ style="stroke-width:0.9486928"
+ id="tspan76"
+ sodipodi:role="line"
+ y="-558.95001"
+ x="-96.673058 -90.469063 -85.242661 -80.072662 -74.84626 -71.274261 -67.674057">Country</tspan></text>
+<text
+ y="-540.13"
+ x="-83.200356"
+ id="text82"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.9486928"
+ transform="matrix(1,0,-0.33330001,-1,0,0)"><tspan
+ style="stroke-width:0.9486928"
+ id="tspan80"
+ sodipodi:role="line"
+ y="-540.13"
+ x="-83.200356 -76.996353 -71.769951 -66.599953 -61.373528">Phone</tspan></text>
+<path
+ inkscape:connector-curvature="0"
+ id="path320"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 386.33,647.15 h 2.8 v 1 h -2.8 z" /><path
+ inkscape:connector-curvature="0"
+ id="path322"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 388.13,645.35 h 1 v 1 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path324"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 386.33,645.35 h 1 v 2.8 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path326"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 388.13,645.35 h 1 v 1 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path334"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 545.17,645.35 h 1 v 1 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path336"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 546.98,645.35 h 1 v 2.8 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path340"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 386.33,610.55 h 1 v 34.8 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path342"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 388.13,610.55 h 1 v 34.8 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path344"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 545.17,610.55 h 1 v 34.8 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path346"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 546.98,610.55 h 1 v 34.8 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path600"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 545.17,645.35 h 1 v 1 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path602"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 545.17,647.15 h 2.8 v 1 h -2.8 z" /><path
+ inkscape:connector-curvature="0"
+ id="path604"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 389.13,647.15 h 156.05 v 1 H 389.13 Z" /><path
+ inkscape:connector-curvature="0"
+ id="path606"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 389.13,645.35 h 156.05 v 1 H 389.13 Z" /><path
+ inkscape:connector-curvature="0"
+ id="path24"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 387.73,590.15 h 159.05 v 19 H 387.73 Z" /><g
+ transform="translate(-49.654207,-9)"
+ id="g172"><g
+ clip-path="url(#clipPath178)"
+ id="g174"><text
+ id="text182"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="matrix(1,0,0,-1,444.15,605.95)"><tspan
+ y="0"
+ x="0"
+ id="tspan180"
+ sodipodi:role="line">INVOICE #</tspan></text>
+</g></g><path
+ inkscape:connector-curvature="0"
+ id="path328"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 388.13,609.55 h 1 v 1 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path330"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 388.13,609.55 h 1 v 1 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path332"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 388.13,607.75 h 1 v 1 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path338"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 545.17,609.55 h 1 v 1 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path348"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 386.33,607.75 h 1 v 2.8 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path350"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 388.13,607.75 h 1 v 1 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path352"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 388.13,571.95 h 1 v 1 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path354"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 386.33,570.15 h 2.8 v 1 h -2.8 z" /><path
+ inkscape:connector-curvature="0"
+ id="path356"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 545.17,607.75 h 1 v 1 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path358"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 546.98,607.75 h 1 v 2.8 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path386"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 388.13,571.95 h 1 v 1 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path388"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 386.33,570.15 h 1 v 2.8 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path398"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 546.98,570.15 h 1 v 2.8 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path400"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 545.17,571.95 h 1 v 1 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path456"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 386.33,572.95 h 1 v 34.8 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path458"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 388.13,572.95 h 1 v 34.8 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path476"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 545.17,572.95 h 1 v 34.8 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path478"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 546.98,572.95 h 1 v 34.8 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path608"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 545.17,607.75 h 1 v 1 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path610"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 545.17,609.55 h 1 v 1 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path612"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 389.13,609.55 h 156.05 v 1 H 389.13 Z" /><path
+ inkscape:connector-curvature="0"
+ id="path614"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 389.13,607.75 h 156.05 v 1 H 389.13 Z" /><path
+ inkscape:connector-curvature="0"
+ id="path616"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 545.17,570.15 h 2.8 v 1 h -2.8 z" /><path
+ inkscape:connector-curvature="0"
+ id="path618"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 545.17,571.95 h 1 v 1 h -1 z" /><path
+ inkscape:connector-curvature="0"
+ id="path620"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 389.13,571.95 h 156.05 v 1 H 389.13 Z" /><path
+ inkscape:connector-curvature="0"
+ id="path622"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 389.13,570.15 h 156.05 v 1 H 389.13 Z" /><path
+ d="m 387.73,551.25 h 159.05 v 19 H 387.73 Z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1215"
+ inkscape:connector-curvature="0" /><g
+ transform="translate(-49.654207,-47.9)"
+ id="g1223"><g
+ id="g1221"
+ clip-path="url(#clipPath178)"><text
+ transform="matrix(1,0,0,-1,444.15,605.95)"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="text1219"><tspan
+ sodipodi:role="line"
+ id="tspan1217"
+ x="0"
+ y="0">DUE</tspan></text>
+</g></g><path
+ d="m 388.13,570.65 h 1 v 1 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1225"
+ inkscape:connector-curvature="0" /><path
+ d="m 388.13,570.65 h 1 v 1 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1227"
+ inkscape:connector-curvature="0" /><path
+ d="m 388.13,568.85 h 1 v 1 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1229"
+ inkscape:connector-curvature="0" /><path
+ d="m 545.17,570.65 h 1 v 1 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1231"
+ inkscape:connector-curvature="0" /><path
+ d="m 386.33,568.85 h 1 v 2.8 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1233"
+ inkscape:connector-curvature="0" /><path
+ d="m 388.13,568.85 h 1 v 1 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1235"
+ inkscape:connector-curvature="0" /><path
+ d="m 388.13,533.05 h 1 v 1 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1237"
+ inkscape:connector-curvature="0" /><path
+ d="m 386.33,531.25 h 2.8 v 1 h -2.8 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1239"
+ inkscape:connector-curvature="0" /><path
+ d="m 545.17,568.85 h 1 v 1 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1241"
+ inkscape:connector-curvature="0" /><path
+ d="m 546.98,568.85 h 1 v 2.8 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1243"
+ inkscape:connector-curvature="0" /><path
+ d="m 388.13,533.05 h 1 v 1 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1245"
+ inkscape:connector-curvature="0" /><path
+ d="m 386.33,531.25 h 1 v 2.8 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1247"
+ inkscape:connector-curvature="0" /><path
+ d="m 546.98,531.25 h 1 v 2.8 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1249"
+ inkscape:connector-curvature="0" /><path
+ d="m 545.17,533.05 h 1 v 1 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1251"
+ inkscape:connector-curvature="0" /><path
+ d="m 386.33,534.05 h 1 v 34.8 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1253"
+ inkscape:connector-curvature="0" /><path
+ d="m 388.13,534.05 h 1 v 34.8 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1255"
+ inkscape:connector-curvature="0" /><path
+ d="m 545.17,534.05 h 1 v 34.8 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1257"
+ inkscape:connector-curvature="0" /><path
+ d="m 546.98,534.05 h 1 v 34.8 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1259"
+ inkscape:connector-curvature="0" /><path
+ d="m 545.17,568.85 h 1 v 1 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1261"
+ inkscape:connector-curvature="0" /><path
+ d="m 545.17,570.65 h 1 v 1 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1263"
+ inkscape:connector-curvature="0" /><path
+ d="m 389.13,570.65 h 156.05 v 1 H 389.13 Z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1265"
+ inkscape:connector-curvature="0" /><path
+ d="m 389.13,568.85 h 156.05 v 1 H 389.13 Z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1267"
+ inkscape:connector-curvature="0" /><path
+ d="m 545.17,531.25 h 2.8 v 1 h -2.8 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1269"
+ inkscape:connector-curvature="0" /><path
+ d="m 545.17,533.05 h 1 v 1 h -1 z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1271"
+ inkscape:connector-curvature="0" /><path
+ d="m 389.13,533.05 h 156.05 v 1 H 389.13 Z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1273"
+ inkscape:connector-curvature="0" /><path
+ d="m 389.13,531.25 h 156.05 v 1 H 389.13 Z"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path1275"
+ inkscape:connector-curvature="0" /><g
+ transform="translate(-38.654207,-9)"
+ id="g148"><g
+ clip-path="url(#clipPath154)"
+ id="g150"><text
+ id="text158"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="matrix(1,0,0,-1,433.15,643.55)"><tspan
+ y="0"
+ x="0"
+ id="tspan156"
+ sodipodi:role="line">INVOICED</tspan></text>
+</g></g></g><g
+ id="g2583"><path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path28"
+ style="fill:#f2f2f2;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.96663612"
+ d="M 106.64999,135.04252 H 331.65 v 370.98 H 106.64999 Z" /><path
+ sodipodi:nodetypes="ccccc"
+ d="m 36.6,135.04252 h 75 v 370.98 h -75 z"
+ style="fill:#d9d9d9;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.11936307"
+ id="path1430"
+ inkscape:connector-curvature="0" /><path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path30"
+ style="fill:#d9d9d9;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.04465592"
+ d="m 331.65,135.04252 h 75 v 370.98 h -75 z" /><path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path32"
+ style="fill:#bfbfbf;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.01204765"
+ d="m 406.65,135.04252 h 75 v 370.98 h -75 z" /><path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path34"
+ style="fill:#ffc000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.94875926"
+ d="m 481.65,135.04252 h 93.75 v 370.98 h -93.75 z" /><text
+ y="-473.59253"
+ x="429.07062"
+ id="text90"
+ style="font-variant:normal;font-weight:bold;font-size:11.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ y="-473.59253"
+ x="429.07062"
+ id="tspan88"
+ sodipodi:role="line">Cost</tspan></text>
+<text
+ y="-473.59253"
+ x="503.504"
+ id="text94"
+ style="font-variant:normal;font-weight:bold;font-size:11.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ y="-473.59253"
+ x="503.504"
+ id="tspan92"
+ sodipodi:role="line">Amount</tspan></text>
+<text
+ y="-426.99252"
+ x="443.56769"
+ id="text98"
+ style="font-variant:normal;font-weight:normal;font-size:9.39999962px;font-family:Arial;-inkscape-font-specification:ArialMT;writing-mode:lr-tb;fill:#202020;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ y="-426.99252"
+ x="443.56769"
+ id="tspan96"
+ sodipodi:role="line">25.00</tspan></text>
+<text
+ y="-426.99252"
+ x="532.08527"
+ id="text102"
+ style="font-variant:normal;font-weight:normal;font-size:9.39999962px;font-family:Arial;-inkscape-font-specification:ArialMT;writing-mode:lr-tb;fill:#202020;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ y="-426.99252"
+ x="532.08527"
+ id="tspan100"
+ sodipodi:role="line">100.00</tspan></text>
+<path
+ inkscape:connector-curvature="0"
+ id="path36"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 36.6,23.825 h 538.8 v 113.02 H 36.6 Z" /><text
+ y="-119.45"
+ x="427.0079"
+ id="text106"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ y="-119.45"
+ x="427.0079"
+ id="tspan104"
+ sodipodi:role="line">SUBTOTAL</tspan></text>
+<text
+ y="-119.45"
+ x="524.58545"
+ id="text110"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ y="-119.45"
+ x="524.58545"
+ id="tspan108"
+ sodipodi:role="line">149.50</tspan></text>
+<text
+ y="-34.025002"
+ x="69.425003"
+ id="text114"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ y="-34.025002"
+ id="tspan112"
+ sodipodi:role="line"
+ x="69.425003">Please make cheques payable to COMPANY NAME</tspan></text>
+<text
+ y="-91.230003"
+ x="368.13"
+ id="text118"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ id="tspan116"
+ sodipodi:role="line"
+ y="-91.230003"
+ x="368.13 374.33401 380.12439">GST</tspan></text>
+<text
+ y="-91.230003"
+ x="455.87802"
+ id="text122"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ y="-91.230003"
+ x="455.87802"
+ id="tspan120"
+ sodipodi:role="line">5.0%</tspan></text>
+<text
+ y="-91.230003"
+ x="537.79041"
+ id="text126"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ y="-91.230003"
+ x="537.79041"
+ id="tspan124"
+ sodipodi:role="line">7.48</tspan></text>
+<text
+ y="-34.025002"
+ x="441.54404"
+ id="text142"
+ style="font-variant:normal;font-weight:bold;font-size:11.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#ffc000;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ y="-34.025002"
+ x="441.54404"
+ id="tspan140"
+ sodipodi:role="line">TOTAL </tspan></text>
+<text
+ y="-34.824997"
+ x="524.71857"
+ id="text146"
+ style="font-variant:normal;font-weight:bold;font-size:9.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#ffc000;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ y="-34.824997"
+ x="524.71857"
+ id="tspan144"
+ sodipodi:role="line">156.98</tspan></text>
+<g
+ transform="translate(38.322358,44.892525)"
+ id="g276"><g
+ clip-path="url(#clipPath282)"
+ id="g278"><text
+ id="text286"
+ style="font-variant:normal;font-weight:bold;font-size:11.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="matrix(1,0,0,-1,143.85,429.3)"><tspan
+ y="0"
+ x="0"
+ id="tspan284"
+ sodipodi:role="line">Description</tspan></text>
+</g></g><g
+ transform="translate(16.4292,44.892525)"
+ id="g288"><g
+ clip-path="url(#clipPath294)"
+ id="g290"><text
+ id="text298"
+ style="font-variant:normal;font-weight:bold;font-size:11.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="matrix(1,0,0,-1,333.5,429.3)"><tspan
+ y="0"
+ x="0"
+ id="tspan296"
+ sodipodi:role="line">Hours</tspan></text>
+</g></g><g
+ style="fill:#202020;fill-opacity:1"
+ transform="translate(6.070098,44.892525)"
+ id="g300"><g
+ style="fill:#202020;fill-opacity:1"
+ clip-path="url(#clipPath306)"
+ id="g302"><text
+ id="text310"
+ style="font-variant:normal;font-weight:normal;font-size:9.39999962px;font-family:Arial;-inkscape-font-specification:ArialMT;writing-mode:lr-tb;fill:#202020;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="matrix(1,0,0,-1,367.73,382.7)"><tspan
+ y="0"
+ id="tspan308"
+ sodipodi:role="line"
+ x="0">4.00</tspan></text>
+</g></g><path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path648"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 36.6,449.79252 h 538.8 v 1 H 36.6 Z" /><path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path650"
+ style="fill:#808080;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 36.6,447.99252 h 538.8 v 1 H 36.6 Z" /><g
+ style="fill:#202020;fill-opacity:1"
+ transform="translate(52.110244,44.892525)"
+ id="g208"><g
+ style="fill:#202020;fill-opacity:1"
+ clip-path="url(#clipPath214)"
+ id="g210"><text
+ id="text218"
+ style="font-variant:normal;font-weight:normal;font-size:9.39999962px;font-family:Arial;-inkscape-font-specification:ArialMT;writing-mode:lr-tb;fill:#202020;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="matrix(1,0,0,-1,69.425,382.7)"><tspan
+ y="0"
+ x="0"
+ id="tspan216"
+ sodipodi:role="line">Weekly meal preparation</tspan></text>
+</g></g><text
+ y="-409.97385"
+ x="120.8789"
+ id="text230"
+ style="font-variant:normal;font-weight:normal;font-size:9.39999962px;font-family:Arial;-inkscape-font-specification:ArialMT;writing-mode:lr-tb;fill:#202020;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ y="-409.97385"
+ x="120.8789"
+ id="tspan228"
+ sodipodi:role="line">Food purchases</tspan></text>
+<g
+ transform="translate(-85.174512,44.892525)"
+ id="g1428"><g
+ id="g1426"
+ clip-path="url(#clipPath282)"><text
+ transform="matrix(1,0,0,-1,143.85,429.3)"
+ style="font-variant:normal;font-weight:bold;font-size:11.39999962px;font-family:BellGothicStd;-inkscape-font-specification:BellGothicStd-Black;writing-mode:lr-tb;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="text1424"><tspan
+ sodipodi:role="line"
+ id="tspan1422"
+ x="0"
+ y="0">Date</tspan></text>
+</g></g><text
+ transform="scale(1,-1)"
+ style="font-variant:normal;font-weight:normal;font-size:9.39999962px;font-family:Arial;-inkscape-font-specification:ArialMT;writing-mode:lr-tb;fill:#202020;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="text1617"
+ x="443.56769"
+ y="-409.97385"><tspan
+ sodipodi:role="line"
+ id="tspan1615"
+ x="443.56769"
+ y="-409.97385">49.50</tspan></text>
+<text
+ y="-409.97385"
+ x="537.31769"
+ id="text1621"
+ style="font-variant:normal;font-weight:normal;font-size:9.39999962px;font-family:Arial;-inkscape-font-specification:ArialMT;writing-mode:lr-tb;fill:#202020;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ transform="scale(1,-1)"><tspan
+ y="-409.97385"
+ x="537.31769"
+ id="tspan1619"
+ sodipodi:role="line">49.50</tspan></text>
+</g></g></svg>
js/exportable-plugin.js
+/**
+ * HTML table to CSV converter for jQuery.
+ *
+ * https://stackoverflow.com/a/16203218/59087
+ */
+;(function( $, window, document, undefined ) {
+ "use strict";
+
+ /** @const */
+ const PLUGIN_NAME = "exportable";
+ /** @const */
+ const PLUGIN_KEY = "plugin_" + PLUGIN_NAME;
+
+ /**
+ * Insert delimiters to avoid accidental split of actual contents.
+ * Exports to "export.csv" by default.
+ */
+ var defaults = {
+ source: "table",
+ filename: "export.csv",
+ exports: {
+ csv: {
+ file_extension: "csv",
+ media_type_text: "text/csv",
+ media_type_data: "application/csv",
+ col_delimiter: '","',
+ row_delimiter: '"\r\n"',
+ temp_col_delimiter: String.fromCharCode( 11 ),
+ temp_row_delimiter: String.fromCharCode( 0 ),
+ },
+ json: {
+ file_extension: "json",
+ media_type_text: "text/plain",
+ media_type_data: "application/json",
+ },
+ },
+ charset: "utf-8",
+ };
+
+ /**
+ * Applies settings.
+ *
+ * @constructor
+ */
+ function Plugin( element, options ) {
+ this.element = element;
+
+ this.settings = $.extend( {}, defaults, options );
+ this._defaults = defaults;
+ this._name = PLUGIN_NAME;
+ this.init();
+ }
+
+ /**
+ * Permit the plug-in to be extended.
+ */
+ $.extend( Plugin.prototype, {
+ /**
+ * Convert a table to a given format based on its filename.
+ *
+ * @protected
+ */
+ init: function() {
+ let plugin = this;
+ let settings = plugin.settings;
+ let filename = settings.filename;
+
+ // Extract filename extension.
+ // https://stackoverflow.com/a/12900504/59087
+ let ext = filename.slice( (filename.lastIndexOf( "." ) - 1 >>> 0) + 2 );
+
+ // Call the function that maps to the predetermined extension.
+ $(plugin.element).on( "click tap", function( event ) {
+ plugin[ext]();
+ });
+ },
+ /**
+ * Returns the exclude class name as a jQuery CSS selector.
+ *
+ * @private
+ */
+ toSelector: function( element, excludeClass ) {
+ if( excludeClass === 'undefined' ) {
+ excludeClass = "";
+ }
+
+ let selector = element + ":not('" + excludeClass + "')";
+
+ return selector;
+ },
+ /**
+ * Converts a table to CSV format.
+ *
+ * @param {string} excludeClass Table data elements of this class are
+ * excluded from the output; if undefined, all columns are included.
+ * @public
+ */
+ export_csv: function( excludeClass ) {
+ let plugin = this;
+ let settings = plugin.settings;
+ let exports = settings.exports;
+ let trd = exports.csv.temp_row_delimiter;
+ let tcd = exports.csv.temp_col_delimiter;
+ let rd = exports.csv.row_delimiter;
+ let cd = exports.csv.col_delimiter;
+ let dataSelector = plugin.toSelector( "td", excludeClass );
+
+ // Find all the table data (td) elements.
+ let $rows = $(settings.source).find( "tr:has(td)" );
+
+ let csv = '"' + $rows.map( function( i, row ) {
+ let $row = $(row);
+ let $cols = $row.find( dataSelector );
+
+ return $cols.map( function( j, col ) {
+ let $col = $(col);
+ let text = $col.text();
+
+ // Escape double quotes.
+ return text.replace( /"/g, '""' );
+ }).get().join( tcd );
+ }).get().join( trd )
+ .split( trd ).join( rd )
+ .split( tcd ).join( cd ) + '"';
+
+ return csv;
+ },
+ /**
+ * Converts a table to CSV format and sends to browser for download.
+ *
+ * @protected
+ */
+ csv: function() {
+ let plugin = this;
+ let settings = plugin.settings;
+ let exports = settings.exports;
+ let csv = this.export_csv();
+ this.download( csv, exports.csv );
+ },
+ /**
+ * Converts a table to JSON format.
+ *
+ * @param {string} excludeClass Table data elements of this class are
+ * excluded from the output; if undefined, all columns are included.
+ * @return {object} A JSON string.
+ * @public
+ */
+ export_json: function( excludeClass ) {
+ let plugin = this;
+ let settings = plugin.settings;
+ let source = settings.source;
+ let headers = [];
+ let th = "thead > tr > " + plugin.toSelector( "th", excludeClass );
+ let td = plugin.toSelector( "td", excludeClass );
+
+ // Find the headings for the json data map.
+ $.each( $(source).parent().find( th ), function( i, j ) {
+ headers.push( $(j).text().toLowerCase() );
+ });
+
+ let json = [];
+
+ $.each( $(source).find( "tr" ), function( k, v ) {
+ let row = {};
+
+ $.each( $(this).find( td ), function( i, j ) {
+ row[ headers[i] ] = $(this).text().trim();
+ });
+
+ json.push( row );
+ });
+
+ return JSON.stringify( json );
+ },
+ /**
+ * Converts a table to JSON format and sends to browser for download.
+ *
+ * @protected
+ */
+ json: function() {
+ let plugin = this;
+ let settings = plugin.settings;
+ let exports = settings.exports;
+ let json = this.export_json();
+ this.download( json, exports.json );
+ },
+ /**
+ * Submits the data to the browser.
+ *
+ * @param {object} data The data to transmit.
+ * @param {object} content_type What type of data to transmit.
+ */
+ download: function( data, content_type ) {
+ let plugin = this;
+ let settings = plugin.settings;
+
+ let href = "";
+ let target = "";
+
+ if( window.Blob && window.URL ) {
+ let blob = new Blob( [data], {
+ type: content_type.meda_type_text + "; charset=" + settings.charset
+ });
+
+ href = URL.createObjectURL( blob );
+ }
+ else {
+ href =
+ "data:" + content_type.media_type_data +
+ "; charset=" + settings.charset +
+ "," + encodeURIComponent( data );
+ }
+
+ $(plugin.element).attr({
+ "download": settings.filename,
+ "href": href,
+ "target": target
+ });
+ },
+ /**
+ * Returns an array of comma-separated values. This function splits
+ * the given string at each end of line marker.
+ *
+ * @param content The string to split.
+ * @precondition content parameter was generated using the export function.
+ * @return {array} The given content split into an array of strings.
+ */
+ toArray: function( content ) {
+ let delim = this.settings.exports.csv.row_delimiter;
+
+ return content.split( delim );
+ }
+ });
+
+ $.fn[ PLUGIN_NAME ] = function( options ) {
+ var plugin;
+
+ this.each( function() {
+ if( !$.data( this, PLUGIN_KEY ) ) {
+ plugin = new Plugin( this, options );
+ $.data( this, PLUGIN_KEY, plugin );
+ }
+ });
+
+ return plugin;
+ };
+
+ window.Plugin = Plugin;
+})(jQuery, window, document);
+
js/ivy-app.js
+/**
+ * The timesheet app must be applied to table bodies. This ensures that the
+ * row and column calculations can leverage the rows and columns of the table
+ * cells.
+ */
+;$(document).ready( function() {
+ /** @const */
+ const COL_DATED = 0;
+ /** @const */
+ const COL_TOTAL = 1;
+ /** @const */
+ const COL_SHIFT = 2;
+ /** @const */
+ const COL_BEGAN = 3;
+ /** @const */
+ const COL_ENDED = 4;
+
+ /** @const */
+ const APP_PREFERENCES = "ivy.preferences";
+
+ /** @const */
+ const APP_DATE_FORMAT_ACTIVE = "YYYY-MM-DD";
+
+ /** @const */
+ const APP_DATE_FORMAT_FIRST = "YYYY-MM-01";
+
+ // Schema editor: https://github.com/json-editor/json-editor
+ let user_preferences_schema = {
+ "description": "Control application behaviour.",
+ "title": "Preferences",
+ "type": "object",
+ "properties": {
+ "active": {
+ "description": "Edit timesheet data for this month.",
+ "type": "string",
+ "format": "date",
+ "title": "Active Month",
+ },
+ "weekdays": {
+ "description": "Predefine daily timeslots.",
+ "type": "array",
+ "title": "Weekdays",
+ "format": "table",
+ "uniqueItems": true,
+ "items": {
+ "type": "object",
+ "required": "weekday",
+ "properties": {
+ "weekday": {
+ "title": "Weekday",
+ "type": "string",
+ "enum": [0, 1, 2, 3, 4, 5, 6],
+ "options": {
+ "enum_titles": [
+ "Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+ ],
+ },
+ },
+ "times": {
+ "title": "Times",
+ "type": "array",
+ "format": "table",
+ "items": {
+ "type": "object",
+ "properties": {
+ "began": {
+ "type": "string",
+ "title": "Began",
+ },
+ "ended": {
+ "type": "string",
+ "title": "Ended",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "inclusion": {
+ "description": "Change what types of days are included.",
+ "type": "object",
+ "title": "Include",
+ "properties": {
+ "weekends": {
+ "type": "boolean",
+ "title": "Weekends",
+ "format": "checkbox",
+ },
+ "holidays": {
+ "type": "boolean",
+ "title": "Holidays",
+ "format": "checkbox",
+ },
+ },
+ },
+ "saving": {
+ "description": "Change timesheet persistence behaviour.",
+ "type": "object",
+ "title": "Saving",
+ "properties": {
+ "timeout": {
+ "description": "Time between autosaves, in seconds.",
+ "type": "integer",
+ "title": "Autosave",
+ "format": "number",
+ },
+ },
+ },
+ "columns": {
+ "description": "Columns for custom purposes (e.g. ticket number).",
+ "type": "array",
+ "title": "Columns",
+ "format": "tabs",
+ "maxItems": 3,
+ "items": {
+ "type": "string",
+ "title": "Column",
+ },
+ },
+ "formats": {
+ "description": "Timesheet data formats; changing these can break the application.",
+ "title": "Format",
+ "type": "object",
+ "properties": {
+ "format_date": {
+ "description": "Format for timesheet day cells.",
+ "type": "string",
+ "title": "Date",
+ "default": APP_DATE_FORMAT_ACTIVE,
+ },
+ "format_time": {
+ "description": "Format for timesheet time cells.",
+ "type": "string",
+ "title": "Time",
+ "default": "HH:mm A",
+ },
+ "format_prec": {
+ "description": "Decimal places to show for time calculations.",
+ "type": "integer",
+ "title": "Precision",
+ "default": 2,
+ },
+ "format_keys": {
+ "description": "Internal format used as timesheet key index for local data storage.",
+ "type": "string",
+ "title": "Primary Key",
+ "default": "YYYYMM",
+ },
+ },
+ },
+ },
+ };
+
+ /**
+ * Main application entry point.
+ *
+ * User-defined application preferences, including day templates.
+ */
+ let ivy = $("#ivy tbody").ivy({
+ /**
+ * Called when Ivy is initialized.
+ */
+ init: function() {
+ this.initMenu();
+ this.initHeadings();
+ this.initTimesheet();
+ this.initPreferencesDialog();
+ this.initPreferencesEditor();
+ this.initSuperhero();
+ this.setChanged( false );
+ },
+ /**
+ * Maps user-interface menu items to ivy function calls.
+ */
+ initMenu: function() {
+ let plugin = this.getPlugin();
+
+ let ui = [
+ { a: "edit-cut", f: "Cut" },
+ { a: "edit-copy", f: "Copy" },
+ { a: "edit-undo", f: "Undo" },
+ { a: "edit-redo", f: "Redo" },
+ { a: "insert-shift", f: "DuplicateRow" },
+ { a: "delete-row", f: "DeleteRow" },
+ { a: "append-day", f: "AppendRow" },
+ ];
+
+ let len = ui.length;
+
+ for( let i = 0; i < len; i++ ) {
+ $( ".app-" + ui[i].a ).on( "click", function( e ) {
+ ivy[ "edit" + ui[i].f ]();
+ });
+ }
+
+ this._exporter_csv = $(".ivy-export-csv").exportable({
+ source: plugin.element,
+ filename: "timesheet.csv"
+ });
+
+ this._exporter_json = $(".ivy-export-json").exportable({
+ source: plugin.element,
+ filename: "timesheet.json"
+ });
+ },
+ /**
+ * Called to insert headings based on user's preferences.
+ */
+ initHeadings: function() {
+ let plugin = this.getPlugin();
+ let $table = $(plugin.getTableBodyElement());
+ let $head = $table.prev( "thead" ).find( "tr:first" );
+
+ let columns = this.getPreferences().columns;
+ let len = columns.length;
+ let html = "";
+
+ for( let i = 0; i < len; i++ ) {
+ html += ("<th>" + columns[i] + "</th>");
+ }
+
+ // Append the user-defined headings to the fixed application headings.
+ $head.append( html );
+ },
+ /**
+ * Creates the first day for a timesheet, based on today's date.
+ */
+ createTimesheet: function() {
+ return JSON.stringify( {} );
+ },
+ /**
+ * Reads data from local storage and drops in the information by month.
+ */
+ initTimesheet: function() {
+ let self = this;
+ let plugin = self.getPlugin();
+
+ let cssTransient = plugin.settings.classCellTransient;
+ let cssReadOnly = plugin.settings.classCellReadOnly;
+ let jsonTimesheet = self.loadTimesheet();
+
+ if( typeof jsonTimesheet === 'undefined' ) {
+ jsonTimesheet = self.createTimesheet();
+ }
+
+ let timesheet = JSON.parse( jsonTimesheet );
+ let css = [];
+
+ $.each( timesheet, function() {
+ let row = [];
+
+ row.push( this.day );
+ row.push( "" );
+ row.push( "" );
+ row.push( this.began.toTime() );
+ row.push( this.ended.toTime() );
+
+ // Prevent adding CSS for keys that have fixed styles.
+ delete this.day;
+ delete this.began;
+ delete this.ended;
+
+ // No need to recreate the CSS each time.
+ if( css.length === 0 ) {
+ css.push( cssReadOnly );
+ css.push( cssTransient );
+ css.push( cssTransient );
+
+ // The following values have no styles:
+ // * began (+1)
+ // * ended (+1)
+ // * all user-provided columns (+length)
+ let len = Object.keys( this ).length + 2;
+
+ for( let i = 0; i < len; i++ ) {
+ css.push( "" );
+ }
+ }
+
+ for( let k in this ) {
+ if( this.hasOwnProperty( k ) ) {
+ row.push( this[k] );
+ }
+ }
+
+ plugin.editAppendRow( row, css );
+ });
+
+ self.fillTimesheet( css );
+ },
+ /**
+ * Fills out the month's remaining days according to user preferences.
+ */
+ fillTimesheet: function( css ) {
+ let self = this;
+ let plugin = self.getPlugin();
+ let prefs = self.getPreferences();
+
+ // If there are no rows in the spreadsheet, then populate the first
+ // day of this month (so that the next date can be calculated).
+ let tally = plugin.getRowCount();
+ let day = moment().startOf( 'month' );
+
+ // If the spreadsheet has data, it means that it was loaded from
+ // disk.
+ if( tally > 0 ) {
+ let date = $(plugin.getCellLastRow( COL_DATED )).text();
+ day = moment( date );
+ }
+
+ // This line will fail if the spreadsheet doesn't have at least one
+ // row of data.
+ let next = day.clone().add( 1, "month" );
+ let date_format = prefs.formats.format_date;
+
+ day.add( 1, "day" );
+
+ while( day.month() < next.month() ) {
+ if( !self.isWorkDay( day ) ) {
+ day.add( 1, "day" );
+ continue;
+ }
+
+ // TODO: Insert times according to user preferences based on weekday.
+ plugin.editAppendRow(
+ [day.format(date_format), "", "", "8:00".toTime(), "9:00".toTime()],
+ css
+ );
+
+ day = self.getNextWorkDay( day );
+ }
+
+ this.getPlugin().refreshCells();
+ },
+ /**
+ * Initializes the UI dialog that contains the schema editor.
+ */
+ initPreferencesDialog: function() {
+ $("#settings").dialog({
+ dialogClass: "settings-dialog",
+ autoOpen: false,
+ maxHeight: $(window).height() * 0.75,
+ height: "auto",
+ width: "auto",
+ closeOnEscape: true,
+ position: {
+ my: "right top",
+ at: "right top",
+ of: window
+ },
+ buttons: {
+ Ok: function() {
+ $(this).dialog( "close" );
+ },
+ Cancel: function() {
+ $(this).dialog( "close" );
+ },
+ },
+ });
+
+ $(".app-settings-preferences").on( "click", function( e ) {
+ $("#settings").dialog("open");
+ });
+ },
+ /**
+ * Call once to initialize the schema editor for user preferences.
+ */
+ initPreferencesEditor: function() {
+ let self = this;
+ let e = document.getElementById( "editor" );
+ let editor = new JSONEditor( e, {
+ theme: "jqueryui",
+ disable_collapse: true,
+ disable_edit_json: true,
+ disable_properties: true,
+ disable_array_reorder: true,
+ disable_array_delete_all_rows: true,
+ disable_array_delete_last_row: true,
+ required_by_default: true,
+ no_additional_properties: true,
+ remove_empty_properties: true,
+ schema: user_preferences_schema,
+ startval: self.getPreferences(),
+ });
+ },
+ /**
+ * Saves changes and saves upon an unload event (tab close, window close,
+ * navigate away, and such).
+ */
+ initSuperhero: function() {
+ let self = this;
+ let prefs = self.getPreferences();
+
+ $(window).on( 'unload', function() {
+ self.save();
+ });
+
+ // Save the application every so often.
+ setInterval( function() { self.save(); }, 1000 * prefs.saving.timeout );
+ },
+ /**
+ * Answers whether the given day is a work day.
+ */
+ isWorkDay: function( day ) {
+ let prefs = this.getPreferences();
+
+ return prefs.inclusion.weekends || (!day.toDate().isWeekend());
+ },
+ /**
+ * Returns the day after the given day, taking into consideration
+ * the user's proferences for including weekends. This does not
+ * mutate the given day, but returns a new day that will be advanced
+ * by 1, 2, or 3 days depending on weekend preferences.
+ *
+ * TODO: Take holidays into consideration.
+ *
+ * @param {object} day The momentjs date to advance by 1 day.
+ * @return {object} A momentjs date instance advanced by n days.
+ */
+ getNextWorkDay: function( day ) {
+ let prefs = this.getPreferences();
+ let d = day.clone();
+
+ // Weekends don't last forever. Skipping two days would work, but
+ // eventually we'll want configurable weekends. (Some people work
+ // Tue through Sat, with Sun/Mon as weekends.)
+ do {
+ d.add( 1, "day" );
+ }
+ while( !this.isWorkDay( d ) );
+
+ return d;
+ },
+ /**
+ * Calculates shifts and totals for each day.
+ */
+ refreshCells: function() {
+ let plugin = this.getPlugin();
+ let MAX_ROWS = plugin.getMaxRows();
+
+ for( let row = 0; row <= MAX_ROWS; row++ ) {
+ this.onCellValueChangeAfter( row, COL_BEGAN );
+ }
+ },
+ /**
+ * Called before a cell value is changed. This sets the cell value
+ * format to the standard time.
+ */
+ onCellValueChangeBefore: function( cellValue, row, col ) {
+ if( col === COL_BEGAN || col === COL_ENDED ) {
+ cellValue = cellValue.toTime();
+ }
+
+ return cellValue;
+ },
+ /**
+ * Called after a cell value is changed. This computes the total number
+ * of hours worked in a day.
+ *
+ * @param {number} row The row value that changed.
+ * @param {number} col The column value that changed.
+ */
+ onCellValueChangeAfter: function( row, col ) {
+ // Trigger saving user-entered data.
+ if( col !== COL_SHIFT || col !== COL_TOTAL || col !== COL_DATED ) {
+ let plugin = this.getPlugin();
+ let prefs = this.getPreferences();
+ let began = this.updateCellTime( row, COL_BEGAN );
+
+ // TODO: Use a preference for the time jump (60).
+ let ended = this.updateCellTime( row, COL_ENDED, began, 60 );
+ let delta = moment.duration( ended.diff( began ) );
+ let hours = delta.asHours();
+
+ if( hours.toFixed ) {
+ hours = Math.abs( hours ).toFixed( prefs.formats.format_prec );
+ }
+
+ // Careful that this doesn't go recursive.
+ $(plugin.getCell( row, COL_SHIFT )).text( hours );
+
+ let indexes = this.findConsecutive( row, 0 );
+ let sum = this.sumConsecutive( indexes );
+
+ // Set the total for the day.
+ $(plugin.getCell( indexes[0], COL_TOTAL )).text( sum );
+
+ // Enusre the changes are saved.
+ this.setChanged( true );
+ }
+ },
+ /**
+ * Called after a row is inserted. This sets the begin time to the
+ * end time of the previous row.
+ *
+ * @param {object} $clone The clone inserted after the given row.
+ */
+ onRowDuplicateAfter: function( $clone ) {
+ let plugin = this.getPlugin();
+ let ended = $clone.find( "td:eq(" + COL_ENDED + ")" ).text();
+
+ $clone.find( "td:not(:first-child)" ).empty();
+
+ let row = $clone.index();
+ let col = COL_BEGAN;
+
+ plugin.setCellValue( ended, row, col );
+
+ // Sometimes setting the cell value doesn't refresh the daily
+ // total. Calling refresh here has a slight performance hit, but
+ // it ensures the correct daily total.
+ plugin.refreshCells();
+ },
+ /**
+ * After a row has been deleted, make sure that a row exists.
+ *
+ * @param {object} $row The row that was deleted.
+ */
+ onRowDeleteAfter: function( $row ) {
+ let plugin = this.getPlugin();
+ let rows = plugin.getRowCount();
+
+ if( rows === 0 ) {
+ let cssTransient = plugin.settings.classCellTransient;
+ let cssReadOnly = plugin.settings.classCellReadOnly;
+ let date = moment();
+ let day = this.getNextWorkDay( date ).format( APP_DATE_FORMAT_FIRST );
+
+ // TODO: Apply user preferences to fill in the new row.
+ plugin.editAppendRow(
+ [day, "", "", "8:00".toTime(), "9:00".toTime()],
+ [cssReadOnly, cssTransient, cssTransient, "", ""]
+ );
+ }
+
+ // Ensure the totals are updated.
+ plugin.refreshCells();
+ },
+ /**
+ * Called after a row is appended. This increments the day of the
+ * month, if possible.
+ *
+ * @param {object} $row The row used as the template for the clone.
+ onRowAppendAfter: function( $row ) {
+ let $date = $row.find( "td:first" );
+ let day = moment( $date.text() );
+ let m1 = day.month();
+
+ day = this.getNextWorkDay( day );
+
+ let m2 = day.month();
+
+ // Refresh the date if within the same month.
+ if( m1 === m2 ) {
+ let prefs = this.getPreferences();
+ $date.text( day.format( prefs.formats.format_date ) );
+ }
+
+ this.getPlugin().refreshCells();
+ },
+ */
+ /**
+ * Called to ensure the cell at the given row and column has a valid
+ * time.
+ *
+ * @param {number} row The cell row to update.
+ * @param {number} col The cell column to update.
+ * @param {defaultTime} The starting time for the cell value.
+ * @param {defaultIncrement} Number of minutes to increment the cell by.
+ * @return {object} The moment object for the time at the given cell.
+ */
+ updateCellTime: function( row, col, defaultTime, defaultIncrement ) {
+ let plugin = this.getPlugin();
+ let prefs = this.getPreferences();
+ let $cell = $(plugin.getCell( row, col ));
+ let time = $cell.text();
+
+ if( time == "" ) {
+ // Clone because moments are mutable.
+ time = moment( defaultTime ).add( defaultIncrement, "minutes" )
+ time = time.format( prefs.formats.format_time );
+ $cell.text( time );
+ }
+
+ return moment.utc( time, prefs.formats.format_time );
+ },
+ /**
+ * Returns the first and last row for a consecutive series of equal values.
+ */
+ findConsecutive: function( row, col ) {
+ let plugin = this.getPlugin();
+ let iterator = row;
+ let comparator = $(plugin.getCell( row, col )).text();
+ let comparand = comparator;
+
+ // Search backwards from the active row until a non-matching value.
+ while( comparator == comparand && --iterator >= 0 ) {
+ comparand = $(plugin.getCell( iterator, col )).text();
+ }
+
+ let beginIndex = iterator + 1;
+ let MAX_ROWS = plugin.getMaxRows();
+
+ iterator = row;
+ comparand = comparator;
+
+ // Search forewards from the active row until a non-matching value.
+ while( comparator == comparand && ++iterator < MAX_ROWS ) {
+ comparand = $(plugin.getCell( iterator, col )).text();
+ }
+
+ let endedIndex = iterator - 1;
+
+ return [beginIndex, endedIndex];
+ },
+ /**
+ * Given a start and end index, this computes the total number of hours
+ * over all shifts in each day.
+ */
+ sumConsecutive: function( indexes ) {
+ let plugin = this.getPlugin();
+ let prefs = this.getPreferences();
+ let sum = 0;
+
+ // Sum shift times within the same day.
+ for( let i = indexes[0]; i <= indexes[1]; i++ ) {
+ sum += parseFloat( $(plugin.getCell( i, COL_SHIFT )).text(), 10 )||0;
+ }
+
+ if( sum.toFixed ) {
+ sum = sum.toFixed( prefs.formats.format_prec );
+ }
+
+ return sum;
+ },
+ /**
+ * Called to set the state of the changed flag, which is used when saving
+ * to determine if any action is required.
+ *
+ * @param {boolean} changed Set true when timesheet needs saving.
+ * @private
+ */
+ setChanged: function( changed ) {
+ this._changed = changed;
+ },
+ /**
+ * Returns the changed state, which is used to determine whether saving
+ * the timesheet is required.
+ *
+ * @return {boolean} Indicates whether the timesheet has been modified
+ * by the user.
+ * @private
+ */
+ getChanged: function() {
+ return this._changed;
+ },
+ /**
+ * Returns the storage facility for timesheets and preferences. Must
+ * implement put and get methods to store and retrieve data.
+ *
+ * @return {object} A reference to the browser's local storage.
+ * @protected
+ */
+ getDataStore: function() {
+ return localStorage;
+ },
+ /**
+ * Called to put a key/value pair into storage.
+ *
+ * @param {string} key The key to associate with the given value.
+ * @param {object} value The value to associate with the given key.
+ * @protected
+ */
+ put: function( key, value ) {
+ this.getDataStore().put( key, value );
+ },
+ /**
+ * Called to retrieve a value for a given key from storage.
+ *
+ * @param {string} key The key associated with the given value.
+ * @param {object} value The value associated with the given key.
+ * @protected
+ */
+ get: function( key, defaultValue ) {
+ return this.getDataStore().get( key, defaultValue );
+ },
+ /**
+ * Returns user-defined timesheet data from storage for the month being
+ * edited (as set in the preferences).
+ *
+ * @public
+ */
+ loadTimesheet: function() {
+ let month = this.getTimesheetKey();
+ let timesheet = this.get( month );
+
+ return timesheet;
+ },
+ /**
+ * Saves a CSV file that represents user defined timesheets data to the
+ * data store.
+ *
+ * @public
+ */
+ saveTimesheet: function() {
+ let month = this.getTimesheetKey();
+ let timesheet = this.getTimesheetData();
+
+ this.put( month, timesheet );
+ },
+ /**
+ * Returns the key that represents the month being edited. This value
+ * is set in the prefereces.
+ *
+ * @return {object} The active year and month from user preferences.
+ * @public
+ */
+ getTimesheetKey: function() {
+ let prefs = this.getPreferences();
+ let active = moment( prefs.active, APP_DATE_FORMAT_ACTIVE );
+ let key = active.format( prefs.formats.format_keys );
+
+ return key;
+ },
+ /**
+ * Retrieves timesheet values from local storage.
+ *
+ * @return {object} The timesheet data stored against the month key,
+ * @public
+ */
+ getTimesheetData: function() {
+ let plugin = this.getPlugin();
+ let cssClass = plugin.settings.classCellTransient;
+ let exporter = this.getExporter();
+
+ // Export the tabular data, excluding elements marked as protected.
+ // When importing, all the table elements are refreshed.
+ let exported = exporter.export_json( "." + cssClass );
+
+ return exported;
+ },
+ /**
+ * Returns the exporter used to slurp the table data.
+ *
+ * @return {object} The tabular data in a machine-readable format.
+ * @public
+ */
+ getExporter: function() {
+ return this._exporter_json;
+ },
+ /**
+ * Called at regular intervals to save the timesheet.
+ *
+ * @public
+ */
+ save: function() {
+ // TODO: Introspect the input field to get its value, if editing.
+ // This will prevent the race-condition that isEditing() avoids.
+ if( this.getChanged() && this.getPlugin().isEditing() === false ) {
+ // Avoid concurrent saves by clearing the dirty flag first.
+ this.setChanged( false );
+ this.saveTimesheet();
+ }
+ },
+ /**
+ * @see user_preferences_schema
+ */
+ getDefaultPreferences: function() {
+ return {
+ "active": moment().format( APP_DATE_FORMAT_FIRST ),
+ "formats": {
+ "format_date": APP_DATE_FORMAT_ACTIVE,
+ "format_time": "hh:mm A",
+ "format_prec": 2,
+ "format_keys": "YYYYMM",
+ },
+ "weekdays": [{
+ "weekday": 1,
+ "times": [
+ {
+ "began": "745",
+ "ended": "930"
+ },
+ {
+ "began": "930",
+ "ended": "1000"
+ },
+ {
+ "began": "1000",
+ "ended": "345p"
+ }
+ ]},
+ ],
+ "saving": {
+ "timeout": 5,
+ },
+ "inclusion": {
+ "weekends": false,
+ "holidays": false
+ },
+ "columns": ["Description"]
+ };
+ },
+ getPreferences: function() {
+ return this.get( APP_PREFERENCES, this.getDefaultPreferences() );
+ },
+ setPreferences: function( prefs ) {
+ this.put( APP_PREFERENCES, prefs );
+ },
+ getPlugin: function() {
+ return this.ivy;
+ }
+ });
+});
+
js/ivy-plugin.js
+/**
+ * Spreadsheet.
+ *
+ * Copyright 2018 White Magic Software, Ltd.
+ */
+;(function( $, window, document, undefined ) {
+ "use strict";
+
+ /** @const */
+ const PLUGIN_NAME = "ivy";
+ /** @const */
+ const PLUGIN_KEY = "plugin_" + PLUGIN_NAME;
+
+ /**
+ * Cells cannot be indexed to values less than MIN_INDEX.
+ *
+ * @const
+ */
+ const MIN_INDEX = 0;
+ /**
+ * Cells cannot be indexed to values greater than MAX_INDEX.
+ *
+ * @const
+ */
+ const MAX_INDEX = 1000;
+
+ var defaults = {
+ classCellActive: PLUGIN_NAME + "-active",
+ classCellActiveInput: PLUGIN_NAME + "-editor",
+ classCellTransient: PLUGIN_NAME + "-transient",
+ classCellReadOnly: PLUGIN_NAME + "-readonly",
+ maxPageSize: 30,
+ dispatchKeysNavigate: [
+ { k: 'enter', f: 'navigateDown' },
+ { k: 'up', f: 'navigateUp' },
+ { k: 'down', f: 'navigateDown' },
+ { k: 'left', f: 'navigateLeft' },
+ { k: 'right', f: 'navigateRight' },
+ { k: 'ctrl+up', f: 'navigateUpSkip' },
+ { k: 'ctrl+down', f: 'navigateDownSkip' },
+ { k: 'ctrl+left', f: 'navigateLeftSkip' },
+ { k: 'ctrl+right', f: 'navigateRightSkip' },
+ { k: 'pageup', f: 'navigatePageUp' },
+ { k: 'pagedown', f: 'navigatePageDown' },
+ { k: 'home', f: 'navigateRowHome' },
+ { k: 'end', f: 'navigateRowEnd' },
+ { k: 'ctrl+home', f: 'navigateHome' },
+ { k: 'ctrl+end', f: 'navigateEnd' },
+ { k: 'shift+tab', f: 'navigateLeft' },
+ { k: 'tab', f: 'navigateRight' },
+
+ { k: 'f2', f: 'editStart' },
+ { k: 'ctrl+x', f: 'editCut' },
+ { k: 'ctrl+c', f: 'editCopy' },
+ { k: 'ctrl+ins', f: 'editCopy' },
+ { k: 'del', f: 'editErase' },
+ { k: 'shift+del', f: 'editDeleteRow' },
+ { k: 'ins', f: 'editDuplicateRow' },
+ { k: 'ctrl+i', f: 'editDuplicateRow' },
+ { k: 'command+i', f: 'editDuplicateRow' },
+ { k: 'shift+space', f: 'editAppendRow' },
+
+ { k: 'ctrl+z', f: 'editUndo' },
+ { k: 'command+z', f: 'editUndo' },
+ { k: 'ctrl+shift+z', f: 'editRedo' },
+ { k: 'ctrl+y', f: 'editRedo' },
+ { k: 'command+y', f: 'editRedo' },
+ ],
+ dispatchKeysEdit: [
+ { k: 'up', f: 'navigateUp' },
+ { k: 'down', f: 'navigateDown' },
+ { k: 'shift+tab', f: 'navigateLeft' },
+ { k: 'tab', f: 'navigateRight' },
+
+ { k: 'enter', f: 'editAccept' },
+ { k: 'esc', f: 'editCancel' },
+ ],
+ /**
+ * Called when the plugin is initialized.
+ */
+ init: function() {
+ },
+ /**
+ * Called before users can edit the data.
+ */
+ refreshCells: function() {
+ },
+ /**
+ * Called immediately before the active cell value changes.
+ *
+ * @param {string} cellValue The value for the new cell.
+ * @param {number} row The cellValue row that has changed.
+ * @param {number} col The cellValue column that has changed.
+ * @return {string} Value to set at row and column.
+ */
+ onCellValueChangeBefore: function( cellValue, row, col ) {
+ return cellValue;
+ },
+ /**
+ * Called immediately after the given row and column cell value changes.
+ *
+ * @param {number} row The cellValue row that has changed.
+ * @param {number} col The cellValue column that has changed.
+ */
+ onCellValueChangeAfter: function( row, col ) {
+ },
+ /**
+ * Called after a row is duplicated.
+ *
+ * @param {object} $clone The clone inserted after the given row.
+ */
+ onRowDuplicateAfter: function( $clone ) {
+ },
+ /**
+ * Called after a row is inserted.
+ *
+ * @param {object} $row The row used as the template for the clone.
+ * @param {object} $clone The clone inserted after the given row.
+ */
+ onRowInsertAfter: function( $row, $clone ) {
+ },
+ /**
+ * Called after a row is appended.
+ *
+ * @param {object} $row The row used as the template for the clone.
+ * @param {object} $clone The clone appended to the last row.
+ */
+ onRowAppendAfter: function( $row, $clone ) {
+ },
+ /**
+ * Called after a row is deleted.
+ *
+ * @param {object} $row The row that was deleted.
+ */
+ onRowDeleteAfter: function( $row ) {
+ }
+ };
+
+ /**
+ * Applies settings, initializes keyboard bindings.
+ *
+ * @constructor
+ */
+ function Plugin( element, options ) {
+ this.element = element;
+
+ this.settings = $.extend({}, defaults, options );
+ this.settings.ivy = this;
+ this._defaults = defaults;
+ this._name = PLUGIN_NAME;
+ this._cell = [MIN_INDEX, MIN_INDEX];
+ this._$cellInput = false;
+ this._commandExecutor = new CommandExecutor();
+ this.init();
+ }
+
+ /**
+ * Permit the plug-in to be extended.
+ */
+ $.extend( Plugin.prototype, {
+ /**
+ * Called during plugin construction to bind event handlers.
+ *
+ * @protected
+ */
+ init: function() {
+ // Notify extensions that the plugin is ready.
+ this.settings.init();
+
+ // Prevent keys from bubbling to the browser container.
+ this.setup();
+
+ // Start in navigation mode.
+ this.bindNavigateMode();
+
+ // Start edit mode when any non-navigable key is pressed.
+ this.bindPrintableKeys();
+
+ // Allow click (mouse, tap, etc.) navigation.
+ this.bindNavigationClicks();
+
+ // Trap pasting into the browser.
+ this.bindPasteHandler();
+
+ // Highlight the default cell location.
+ this.activate();
+ },
+ /**
+ * Delegates to the settings to inform the client that the cells need
+ * to be refreshed.
+ */
+ refreshCells: function() {
+ this.settings.refreshCells();
+ },
+ /**
+ * Calls the key binding library to prevent keys from bubbling to the
+ * browser itself.
+ *
+ * @private
+ */
+ setup: function() {
+ Mousetrap.prototype.stopCallback = function( e, element, combo ) {
+ e.preventDefault();
+ return false;
+ }
+ },
+ /**
+ * Jumps to a given table cell. This is used upon receiving click,
+ * double-click, tap, and double-tap events.
+ *
+ * @param {object} $cell The cell to activate.
+ * @public
+ */
+ navigateTableCell: function( $cell ) {
+ let col = $cell.parent().children().index( $cell );
+ let row = $cell.parent().parent().children().index( $cell.parent() );
+
+ // Navigating from the active cell to itself would undo issues,
+ // so ensure that the click navigation can be undone by filtering
+ // out clicks to the same cell.
+ if( this.isActiveCell( row, col ) === false ) {
+ this.navigate( row, col );
+ }
+ },
+ /**
+ * Binds single and double mouse clicks to navigation.
+ *
+ * @protected
+ */
+ bindNavigationClicks: function() {
+ let plugin = this;
+ let $table = $(plugin.getTableBodyElement());
+
+ $table.on( "click", "td", function() {
+ plugin.navigateTableCell( $(this) );
+ });
+
+ $table.on( "dblclick doubletap", "td", function() {
+ plugin.navigateTableCell( $(this) );
+ plugin.editStart();
+ });
+ },
+ /**
+ * Start cell editing when a printable character is typed.
+ *
+ * @protected
+ */
+ bindPrintableKeys: function() {
+ let plugin = this;
+ let $table = $(plugin.getTableBodyElement());
+
+ $table.on( "keypress", function( e ) {
+ // If the character code is numeric, it is a non-printable char.
+ var charCode = (typeof e.which === "number") ? e.which : e.keyCode;
+
+ // Control keys and meta keys (Mac Command ⌘) do not trigger edit mode.
+ if( e.type === "keypress" && charCode && !e.ctrlKey && !e.metaKey ) {
+ plugin.editStart( String.fromCharCode( charCode ) );
+
+ // Some browsers pass the pressed key into the input field... while
+ // other browsers do not. This levels the playing field.
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ });
+ },
+ /**
+ * Stop cell editing when a printable character is typed.
+ *
+ * @protected
+ */
+ unbindPrintableKeys: function() {
+ let plugin = this;
+ let $table = $(plugin.getTableBodyElement());
+
+ $table.off( "keypress" );
+ },
+ /**
+ * Resets the key bindings and injects a new mapping.
+ *
+ * @param {array} keymap The list of keyboard events to bind.
+ * @private
+ */
+ _bindDispatchKeys: function( keymap ) {
+ Mousetrap.reset();
+
+ let plugin = this;
+
+ // Apply configurable keyboard bindings.
+ for( let i = 0; i < keymap.length; i++ ) {
+ let k = keymap[i].k;
+ let f = keymap[i].f;
+
+ Mousetrap.bind( k, function( e ) {
+ // Call the mapped function by its string name.
+ plugin[ f ]();
+ });
+ }
+ },
+ /**
+ * Binds the keyboard to cell model navigation.
+ *
+ * @protected
+ */
+ bindNavigateMode: function() {
+ let plugin = this;
+ plugin._bindDispatchKeys( this.settings.dispatchKeysNavigate );
+ },
+ /**
+ * Binds the keyboard to cell edits.
+ *
+ * @protected
+ */
+ bindEditMode: function() {
+ let plugin = this;
+ plugin._bindDispatchKeys( this.settings.dispatchKeysEdit );
+ },
+ /**
+ * Binds paste events to replace cell content.
+ *
+ * @protected
+ */
+ bindPasteHandler: function() {
+ let plugin = this;
+
+ $(document).on( "paste", function( e ) {
+ if( e.originalEvent ) {
+ let buffer = e.originalEvent.clipboardData.getData( "text" );
+ plugin.cellUpdate( buffer );
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ });
+ },
+ /**
+ * Primitive to sanitize the row and column values. This will return
+ * MIN_INDEX if index is less than MIN_INDEX, or max if index is
+ * greater than max, otherwise this returns the index.
+ *
+ * @param {number} i The row or column index to sanitize.
+ * @param {number} max The maximum extent allowed for the index value.
+ * @return {number} Sanitized cell index number.
+ * @private
+ */
+ _sanitizeCellIndex: function( i, max ) {
+ return i = i > max ? max : (i < MIN_INDEX ? MIN_INDEX : i);
+ },
+ /**
+ * Primitive to get the table body via jQuery.
+ *
+ * @return {object} An element that represents the tbody containing cells.
+ * @public
+ */
+ getTableBodyElement: function() {
+ return $(this.element)[ 0 ];
+ },
+ /**
+ * Primitive to get the active cell row from the model.
+ *
+ * @return {number} The active cell row, 0-based.
+ * @public
+ */
+ getCellRow: function() {
+ return this._cell[ 0 ];
+ },
+ /**
+ * Primitive to get the active cell column from the model.
+ *
+ * @return {number} The active cell column, 0-based.
+ * @public
+ */
+ getCellCol: function() {
+ return this._cell[ 1 ];
+ },
+ /**
+ * Primitive to change the cell row without updating the user interface.
+ * Any value that exceeds the table range is set to the extent of the
+ * table range.
+ *
+ * @param {number} row The new value for the active cell row.
+ * @postcondition The cell data model row is set to the given row.
+ * @protected
+ */
+ setCellRow: function( row ) {
+ this._cell[ 0 ] = this._sanitizeCellIndex( row, this.getMaxRows() );
+ },
+ /**
+ * Primitive to change the cell column without updating the user interface.
+ * Any value that exceeds the table range is set to the extent of the
+ * table range.
+ *
+ * @param {number} col The new value for the active cell column.
+ * @postcondition The cell data model column is set to the given column.
+ * @protected
+ */
+ setCellCol: function( col ) {
+ this._cell[ 1 ] = this._sanitizeCellIndex( col, this.getMaxCols() );
+ },
+ /**
+ * Primitive to get the table cell at the given row and column. The row
+ * and column values must be sanitized prior to calling.
+ *
+ * @param {number} row The cell value's row to retrieve.
+ * @param {number} col The cell value's column to retrieve.
+ * @return {string} The cell value at the given row and column.
+ * @public
+ */
+ getCell: function( row, col ) {
+ let table = this.getTableBodyElement();
+
+ return table.rows[ row ].cells[ col ];
+ },
+ /**
+ * Primitive to get the last cell in the table for a column. The column
+ * value must be sanitized prior to calling.
+ *
+ * @param {number} col The cell value's column to retrieve.
+ * @return {string} The cell value at the last row and given column.
+ * @public
+ */
+ getCellLastRow: function( col ) {
+ let row = this.getMaxRows();
+
+ return this.getCell( row, col );
+ },
+ /**
+ * Primitive to get the active table cell element, which can be referenced
+ * using jQuery.
+ *
+ * @return {object} This returns a td element that can be styled.
+ * @public
+ */
+ getActiveCell: function() {
+ let row = this.getCellRow();
+ let col = this.getCellCol();
+
+ return this.getCell( row, col );
+ },
+ /**
+ * Primitive that returns the maximum number of table rows.
+ *
+ * @return {number} The last row index, 0-based.
+ * @public
+ */
+ getMaxRows: function() {
+ let table = this.getTableBodyElement();
+ let max = table.rows.length - 1;
+
+ return max;
+ },
+ /**
+ * Primitive that returns the maximum number of table columns.
+ *
+ * @return {number} The last column index, 0-based.
+ * @public
+ */
+ getMaxCols: function() {
+ let table = this.getTableBodyElement();
+ let max = table.rows[ MIN_INDEX ].cells.length - 1;
+
+ return max;
+ },
+ /**
+ * Primitive to return the number of table rows.
+ *
+ * @return {number} The number of table rows.
+ * @public
+ */
+ getRowCount: function() {
+ let table = this.getTableBodyElement();
+
+ return table.rows.length;
+ },
+ /**
+ * Answers whether the table has any rows with data to edit.
+ *
+ * @return {boolean} True means the table has rows.
+ * @public
+ */
+ isActive: function() {
+ let table = this.getTableBodyElement();
+
+ return table.rows.length > 0;
+ },
+ /**
+ * Answers whether the active cell is at the given row and column.
+ *
+ * @return {boolean} True means the row and column denote the active cell.
+ * @public
+ */
+ isActiveCell: function( row, col ) {
+ return this.isActiveCellRow( row ) && this.isActiveCellCol( col );
+ },
+ /**
+ * Answers whether the active cell is at the given row.
+ *
+ * @return {boolean} True means the row equals the active cell's row.
+ * @public
+ */
+ isActiveCellRow: function( row ) {
+ return row === this.getCellRow();
+ },
+ /**
+ * Answers whether the active cell is at the given column.
+ *
+ * @return {boolean} True means column equals the active cell's column.
+ * @public
+ */
+ isActiveCellCol: function( col ) {
+ return col === this.getCellCol();
+ },
+ /**
+ * Answers whether the active cell has a read-only or protected class.
+ *
+ * @return {boolean} True means the active cell has a read-only or
+ * protected class.
+ * @public
+ */
+ isActiveCellProtected: function() {
+ let $cell = $(this.getActiveCell());
+
+ return $cell.hasClass( this.settings.classCellReadOnly ) ||
+ $cell.hasClass( this.settings.classCellTransient );
+ },
+ /**
+ * Primitive to add the active class to the table cell represented by
+ * the cell model.
+ *
+ * @postcondition The active cell has its active cell class added.
+ * @protected
+ */
+ activate: function() {
+ // Sanitize the row.
+ this.setCellRow( this.getCellRow() );
+
+ // Sanitize the column.
+ this.setCellCol( this.getCellCol() );
+
+ let $cell = $(this.getActiveCell());
+ $cell.addClass( this.settings.classCellActive );
+ },
+ /**
+ * Primitive to remove the active class from the table cell represented by
+ * the cell model.
+ *
+ * @postcondition The active cell has its active cell class removed.
+ * @protected
+ */
+ deactivate: function() {
+ let $cell = $(this.getActiveCell());
+ $cell.removeClass( this.settings.classCellActive );
+ },
+ /**
+ * Returns the state of the plugin, which can be restored using the
+ * restore state function.
+ *
+ * @return {object} The plugin's state.
+ * @public
+ */
+ cellStateRetrieve: function() {
+ let $cell = $(this.getActiveCell());
+ let result = {
+ cellRow: this.getCellRow(),
+ cellCol: this.getCellCol(),
+ cellValue: $cell.text()
+ };
+
+ return result;
+ },
+ /**
+ * Reverts the state of the plugin from a previously retrieved state.
+ *
+ * @param {object} state A state object.
+ * @private
+ */
+ cellStateRestore: function( state ) {
+ this._navigate( state.cellRow, state.cellCol );
+ this.setActiveCellValue( state.cellValue );
+ },
+ /**
+ * @private
+ */
+ getCommandExecutor: function() {
+ return this._commandExecutor;
+ },
+ /**
+ * Delegates execution of a command to the command executor, which records
+ * commands for undo and redo purposes.
+ *
+ * @private
+ */
+ execute: function( command ) {
+ this.getCommandExecutor().execute( command );
+ },
+ /**
+ * Ensure that the document body retains focus. This has the side
+ * effect that when the table is loaded, the user can start typing
+ * immediately.
+ *
+ * @public
+ */
+ focus: function() {
+ $(this.getTableBodyElement()).focus();
+ },
+ /**
+ * Navigates to the given row and column without storing the command
+ * in the undo/redo history.
+ *
+ * @param {number} row The new row number for the active cell.
+ * @param {number} col The new column number for the active cell.
+ * @postcondition The previously activated cell is deactivated.
+ * @postcondition The cell at (row, col) is activated.
+ * @postcondition The table body has input focus.
+ * @private
+ */
+ _navigate: function( row, col ) {
+ this.deactivate();
+ this.setCellRow( row );
+ this.setCellCol( col );
+ this.activate();
+ this.focus();
+ },
+ /**
+ * Changes the active cell. All other navigate functions call this
+ * function to stop cell editing and navigate to another cell.
+ *
+ * @param {number} row The new row number for the active cell.
+ * @param {number} col The new column number for the active cell.
+ * @postcondition Cell editing has stopped.
+ * @postcondition The previously activated cell is deactivated.
+ * @postcondition The cell at (row, col) is activated.
+ * @postcondition The undo buffer includes this navigate command.
+ * @postcondition The table body has input focus.
+ * @public
+ */
+ navigate: function( row, col ) {
+ this.editStop();
+ this.execute( new CommandNavigate( this, row, col ) );
+ },
+ /**
+ * Helper method for navigating to a different row within the active
+ * column.
+ *
+ * @param {number} skip The number of cells to move.
+ * @protected
+ */
+ navigateRow: function( skip ) {
+ // TODO: Skip protected cells.
+ this.navigate( this.getCellRow() + skip, this.getCellCol() );
+ },
+ /**
+ * Helper method for navigating to a different column within the active
+ * row.
+ *
+ * @param {number} skip The number of cells to move.
+ * @protected
+ */
+ navigateCol: function( skip ) {
+ // TODO: Skip protected cells.
+ this.navigate( this.getCellRow(), this.getCellCol() + skip );
+ },
+ /**
+ * @public
+ */
+ navigatePageUp: function() {
+ this.navigateRow( -this.settings.maxPageSize );
+ },
+ /**
+ * @public
+ */
+ navigatePageDown: function() {
+ this.navigateRow( +this.settings.maxPageSize );
+ },
+ /**
+ * Changes the active cell location upwards one cell.
+ *
+ * @public
+ */
+ navigateUp: function() {
+ this.navigateRow( -1 );
+ },
+ /**
+ * Changes the active cell location downwards one cell.
+ *
+ * @public
+ */
+ navigateDown: function() {
+ this.navigateRow( +1 );
+ },
+ /**
+ * Changes the active cell location leftwards one cell.
+ *
+ * @public
+ */
+ navigateLeft: function() {
+ this.navigateCol( -1 );
+ },
+ /**
+ * Changes the active cell location rightwards one cell.
+ *
+ * @public
+ */
+ navigateRight: function() {
+ this.navigateCol( +1 );
+ },
+ /**
+ * Changes the active cell location upwards to the first non-empty cell.
+ *
+ * @public
+ */
+ navigateUpSkip: function() {
+ console.log( "Navigate up skip" );
+ this.navigateRow( -1 );
+ },
+ /**
+ * Changes the active cell location downwards to the first non-empty cell.
+ *
+ * @public
+ */
+ navigateDownSkip: function() {
+ console.log( "Navigate down skip" );
+ this.navigateRow( +1 );
+ },
+ /**
+ * Changes the active cell location leftwards to the first non-empty cell.
+ *
+ * @public
+ */
+ navigateLeftSkip: function() {
+ console.log( "Navigate left skip" );
+ this.navigateCol( -1 );
+ },
+ /**
+ * Changes the active cell location rightwards to the first non-empty cell.
+ *
+ * @public
+ */
+ navigateRightSkip: function() {
+ console.log( "Navigate right skip" );
+ this.navigateCol( +1 );
+ },
+ /**
+ * Changes the active cell location to the upper-left cell.
+ *
+ * @public
+ */
+ navigateHome: function() {
+ this.navigate( MIN_INDEX, MIN_INDEX );
+ },
+ /**
+ * Changes the active cell location to the lower-right cell.
+ *
+ * @public
+ */
+ navigateEnd: function() {
+ this.navigate( MAX_INDEX, MAX_INDEX );
+ },
+ /**
+ * Changes the active cell location to the left-most column.
+ *
+ * @public
+ */
+ navigateRowHome: function() {
+ this.navigateCol( -MAX_INDEX );
+ },
+ /**
+ * Changes the active cell location to the right-most column.
+ *
+ * @public
+ */
+ navigateRowEnd: function() {
+ this.navigateCol( +MAX_INDEX );
+ },
+ /**
+ * Copies the active cell's contents and then erases the contents.
+ *
+ * @public
+ */
+ editCut: function() {
+ this.execute( new CommandCellCut( this ) );
+ },
+ /**
+ * Copies the active cell's contents into the clipboard buffer.
+ *
+ * @private
+ */
+ editCopy: function() {
+ let $temp = $("<input>");
+ let $cell = $(this.getActiveCell());
+ $("body").append( $temp );
+ $temp.val( $cell.text() ).select();
+ document.execCommand( "copy" );
+ $temp.remove();
+ },
+ /**
+ * Erases the active cell's contents.
+ *
+ * @postcondition The update operation is added to the stack.
+ * @postcondition The active cell is empty.
+ * @postcondition The client is notified of the erase event.
+ * @public
+ */
+ editErase: function() {
+ this.cellUpdate( "" );
+ },
+ /**
+ * Sets the active cell's contents so long as the cell is not read-only.
+ *
+ * @precondition The active cell is not read-only.
+ * @postcondition The active cell value is changed (if not read-only).
+ * @param {string} cellValue The new cell value.
+ * @public
+ */
+ cellUpdate: function( cellValue ) {
+ let plugin = this;
+
+ if( !plugin.isActiveCellProtected() ) {
+ plugin.execute( new CommandCellUpdate( this, cellValue ) );
+ }
+ },
+ /**
+ * Creates an input field at the active table cell using the
+ * given cell's dimensions.
+ *
+ * @param {object} $cell Contains the cell width and text value used
+ * to create and populate the cell input field.
+ * @private
+ */
+ cellInputCreate: function( $cell, charCode ) {
+ $cell.addClass( this.settings.classCellActiveInput );
+
+ let cellWidth = $cell.width();
+ let cellValue = charCode ? charCode : $cell.text();
+ let $input = $("<input>");
+
+ $input.css({
+ "width": cellWidth,
+ "max-width": cellWidth,
+ });
+
+ $input.val( cellValue );
+
+ return $input;
+ },
+ /**
+ * Destroys the previously created input field.
+ *
+ * @return The input field value.
+ * @private
+ */
+ cellInputDestroy: function() {
+ let $input = this.getCellInput();
+ let cellValue = $input.val();
+
+ $input.remove();
+ this.setCellInput( false );
+
+ return cellValue;
+ },
+ /**
+ * @protected
+ */
+ getCellInput: function() {
+ return this._$cellInput;
+ },
+ /**
+ * Sets the input field widget used for editing by the user.
+ *
+ * @param {object} $input The new value for the cell input field widget.
+ * @protected
+ */
+ setCellInput: function( $input ) {
+ this._$cellInput = $input;
+ },
+ /**
+ * Enables cell editing for the active table cell, so long as the cell
+ * is not read-only.
+ *
+ * @param {string} charCode Set the initial value to this character.
+ * @public
+ */
+ editStart: function( charCode ) {
+ let plugin = this;
+
+ if( !plugin.isActiveCellProtected() ) {
+ plugin.execute( new CommandCellEditStart( plugin, charCode ) );
+ }
+ },
+ /**
+ * Disables cell editing for the active table cell. This must not add
+ * the command to the undo/redo history. This ensures that the cell
+ * is being edited prior to destroying it.
+ *
+ * @return {boolean} False means the cell was not being edited.
+ * @public
+ */
+ editStop: function() {
+ let $input = this.getCellInput();
+ let edit = false;
+
+ // Ensure edit mode is engaged before disabling edit mode.
+ if( this.isEditing() ) {
+ let plugin = this;
+ let cellValue = plugin.cellInputDestroy();
+ let $cell = $(plugin.getActiveCell());
+
+ $cell.removeClass( plugin.settings.classCellActiveInput );
+
+ plugin.setActiveCellValue( cellValue );
+ plugin.focus();
+
+ plugin.bindNavigateMode();
+ plugin.bindPrintableKeys();
+
+ edit = true;
+ }
+
+ return edit;
+ },
+ /**
+ * Called when the user presses Enter while editing a cell.
+ */
+ editAccept: function() {
+ this.editStop();
+ this.navigateDown();
+ },
+ /**
+ * Returns true if the active cell is in edit mode.
+ *
+ * @return {boolean} False means that no editing is taking place.
+ * @public
+ */
+ isEditing: function() {
+ return this.getCellInput() !== false;
+ },
+ /**
+ * Called when a new value is applied to the active cell.
+ *
+ * @postcondition The client callback onCellValueChanged is called.
+ * @postcondition The cell value at the given row and column is changed
+ * to the result from the callback (default is the given cell value).
+ * @param {string} v The new active cell value.
+ * @param {number} row The row for the value to change.
+ * @param {number} col The columns for the value to change.
+ * @public
+ */
+ setCellValue: function( v, row, col ) {
+ let plugin = this;
+
+ v = plugin.settings.onCellValueChangeBefore( v, row, col );
+ $(plugin.getCell( row, col )).text( v );
+ plugin.settings.onCellValueChangeAfter( row, col );
+ },
+ /**
+ * Changes the active cell value to the given value.
+ *
+ * @param {string} cellValue The new active cell value.
+ * @public
+ */
+ setActiveCellValue: function( cellValue ) {
+ let plugin = this;
+ let row = plugin.getCellRow();
+ let col = plugin.getCellCol();
+
+ plugin.setCellValue( cellValue, row, col );
+ },
+ /**
+ * Sets the active cell's contents without notifying clients.
+ *
+ * @param {string} v The new active cell value.
+ * @protected
+ */
+ setActiveCellValueSilent: function( v ) {
+ if( v !== false ) {
+ let plugin = this;
+ let cell = plugin.getActiveCell();
+ $(cell).text( v );
+ }
+ },
+ /**
+ * Reverts any edits to their previous value; if there are no edits in
+ * progress, then this will do nothing.
+ *
+ * @protected
+ */
+ editCancel: function() {
+ // Prevent multiple undo actions from consecutive Esc key presses.
+ if( this.editStop() ) {
+ this.editUndo();
+ }
+ },
+ /**
+ * Adds a new column to the end of columns.
+ *
+ * @public
+ */
+ editInsertColumn: function( name ) {
+ this.execute( new CommandInsertColumn( this, name ) );
+ },
+ /**
+ * Removes the column with the given name.
+ *
+ * @public
+ */
+ editDeleteColumn: function( name ) {
+ this.execute( new CommandDeleteColumn( this, name ) );
+ },
+ /**
+ * Duplicates the active cell row and inserts it immediately after.
+ *
+ * @public
+ */
+ editDuplicateRow: function() {
+ this.execute( new CommandDuplicateRow( this ) );
+ },
+ /**
+ * Removes the existing row for the active cell row.
+ *
+ * @public
+ */
+ editDeleteRow: function() {
+ this.execute( new CommandDeleteRow( this ) );
+ },
+ /**
+ * Appends a new row to the end.
+ *
+ * @public
+ */
+ editAppendRow: function( content, classes ) {
+ this.execute( new CommandAppendRow( this, content, classes ) );
+ },
+ /**
+ * Un-executes the previously executed command.
+ *
+ * @public
+ */
+ editUndo: function() {
+ this.getCommandExecutor().undo();
+ this.refreshCells();
+ },
+ /**
+ * Re-executes the previously un-executed command.
+ *
+ * @public
+ */
+ editRedo: function() {
+ this.getCommandExecutor().redo();
+ },
+ /**
+ * Called after a row is inserted.
+ *
+ * @param {object} $clone The clone inserted after the given row.
+ * @public
+ */
+ onRowDuplicateAfter: function( $clone ) {
+ this.settings.onRowDuplicateAfter( $clone );
+ },
+ /**
+ * Called by a command after a new row is appended.
+ *
+ * @param {object} $row The row that had a new row insterted after it.
+ * @param {object} $clone The newly inserted row.
+ * @public
+ */
+ onRowAppendAfter: function( $row, $clone ) {
+ this.settings.onRowAppendAfter( $row, $clone );
+ },
+ /**
+ * Called by a command after an existing row is deleted.
+ *
+ * @param {object} $row The row that was deleted.
+ * @public
+ */
+ onRowDeleteAfter: function( $row ) {
+ this.settings.onRowDeleteAfter( $row );
+ }
+ });
+
+ /**
+ * Tracks the list of commands that were executed so that they can be
+ * undone or redone as desired. This uses a stack to track the commands.
+ */
+ class CommandExecutor {
+ constructor() {
+ this._undoStack = [];
+ this._redoStack = [];
+ }
+
+ /**
+ * Returns undo command stack.
+ *
+ * @return {array} Stack of commands.
+ * @private
+ */
+ getUndoStack() {
+ return this._undoStack;
+ }
+
+ /**
+ * Returns redo command stack.
+ *
+ * @return {array} Stack of commands.
+ * @private
+ */
+ getRedoStack() {
+ return this._redoStack;
+ }
+
+ /**
+ * Executes the given command and then adds the command to the stack
+ * so that it can be undone at a later time.
+ *
+ * @param {object} command The command to execute and record.
+ * @public
+ */
+ execute( command ) {
+ command.execute();
+ let stack = this.getUndoStack();
+ let previous = stack.peek();
+
+ if( !command.equals( previous ) ) {
+ stack.push( command );
+ }
+ }
+
+ /**
+ * Pops the most recently executed command off the stack and calls the
+ * command's routine to undo the command's changes to the data.
+ *
+ * @precondition None
+ * @postcondition The most recent command is popped off the undo stack.
+ * @postcondition The popped command is pushed onto the redo stack.
+ * @public
+ */
+ undo() {
+ let command = this.getUndoStack().pop();
+
+ if( typeof command !== "undefined" ) {
+ command.undo();
+ this.getRedoStack().push( command );
+ }
+ }
+
+ /**
+ * Pops a command off the redo stack and then executes the command, which
+ * pushes it onto the undo stack.
+ *
+ * @precondition None
+ * @postcondition The most recent redo operations is popped from its stack.
+ * @postcondition The redo operation is performed.
+ * @public
+ */
+ redo() {
+ let command = this.getRedoStack().pop();
+
+ if( typeof command !== "undefined" ) {
+ this.execute( command );
+ }
+ }
+ }
+
+ /**
+ * Defines a general command with the ability to restore state.
+ */
+ class Command {
+ constructor( plugin ) {
+ this._plugin = plugin;
+ this.saveState();
+ }
+
+ /**
+ * All commands override this to expose a consistent interface.
+ *
+ * @protected
+ */
+ execute() { }
+
+ /**
+ * Answers whether the given command has the same state as this command.
+ * This compares the command states to avoid edge cases whereby the
+ * user navigates to the same cell using different key combinations.
+ *
+ * @return {boolean} True when the states are the same.
+ * @public
+ */
+ equals( that ) {
+ return typeof that === "undefined" ?
+ false :
+ Object.equals( this.getState(), that.getState() );
+ }
+
+ /**
+ * Restore's the previously saved cell state.
+ *
+ * @public
+ */
+ undo() {
+ let plugin = this.getPlugin();
+ let state = this.getState();
+
+ if( typeof state !== 'undefined' ) {
+ plugin.cellStateRestore( this.getState() );
+ }
+ }
+
+ /**
+ * Returns a unique identifier for the command's state.
+ *
+ * @protected
+ */
+ getId() {
+ return (new Date()).getTime();
+ }
+
+ /**
+ * Stashes the plugin's state so that undo can be called upon it later.
+ *
+ * @protected
+ */
+ saveState() {
+ this.setState( this.getPlugin().cellStateRetrieve() );
+ }
+
+ setState( state ) {
+ this._state = state;
+ }
+
+ getState() {
+ return this._state;
+ }
+
+ getPlugin() {
+ return this._plugin;
+ }
+ }
+
+ /**
+ * Changes the active cell location.
+ */
+ class CommandNavigate extends Command {
+ constructor( plugin, row, col ) {
+ super( plugin );
+ this._row = row;
+ this._col = col;
+ }
+
+ execute() {
+ let plugin = this.getPlugin();
+ plugin._navigate( this._row, this._col );
+ }
+ }
+
+ /**
+ * Copies the active cell value into the paste buffer, then erases it.
+ */
+ class CommandCellCut extends Command {
+ constructor( plugin ) { super( plugin ); }
+
+ execute() {
+ let plugin = this.getPlugin();
+ plugin.editCopy();
+ plugin.editErase();
+ }
+ }
+
+ /**
+ * Initiates cell editing, which records the active cell state immediately
+ * prior to injecting an input field.
+ */
+ class CommandCellEditStart extends Command {
+ constructor( plugin, charCode ) {
+ super( plugin );
+ this._charCode = charCode;
+ }
+
+ execute() {
+ let plugin = this.getPlugin();
+ plugin.bindEditMode();
+ plugin.unbindPrintableKeys();
+
+ let $cell = $(plugin.getActiveCell());
+ let $input = plugin.cellInputCreate( $cell, this._charCode );
+
+ // Keep track of the input field editor.
+ plugin.setCellInput( $input );
+
+ // Replace the cell's HTML value with an input field.
+ $cell.html( $input );
+ $input.focus();
+
+ $input.on( "focusout", function() {
+ plugin.editStop();
+ });
+ }
+ }
+
+ /**
+ * Changes the active cell value.
+ */
+ class CommandCellUpdate extends Command {
+ constructor( plugin, cellValue ) {
+ super( plugin );
+ this._cellValue = cellValue;
+ }
+
+ execute() {
+ this.getPlugin().setActiveCellValue( this._cellValue );
+ }
+ }
+
+ class CommandInsertColumn extends Command {
+ constructor( plugin, name ) {
+ super( plugin );
+
+ this._columnName = name;
+ }
+
+ execute() {
+ let plugin = this.getPlugin();
+ plugin.deactivate();
+
+ let $body = $(plugin.getTableBodyElement()).parent().find( "thead" );
+
+ // TODO: Append the column.
+
+ plugin.activate();
+ }
+ }
+
+ /**
+ * Copies the active row and inserts it after the active row.
+ */
+ class CommandDuplicateRow extends Command {
+ constructor( plugin ) {
+ super( plugin );
+ }
+
+ execute() {
+ let plugin = this.getPlugin();
+ plugin.deactivate();
+
+ let $row = this.getRow();
+ let $clone = $row.clone();
+
+ // Uniquely identify the row so that multiple clones of the same row
+ // will result in different states, and thereby join the undo stack.
+ this.setState({ id: this.getId(), clone: $clone });
+ this.inject( $clone );
+
+ plugin.activate();
+ }
+
+ inject( $clone ) {
+ let $row = this.getRow();
+
+ $row.after( $clone );
+ this.getPlugin().onRowDuplicateAfter( $clone );
+ }
+
+ /**
+ * Removes the row that was previously inserted.
+ */
+ undo() {
+ this.getState().clone.remove();
+ }
+
+ getRow() {
+ let $cell = $(this.getPlugin().getActiveCell());
+ return $cell.closest( "tr" );
+ }
+ }
+
+ /**
+ * Appends a new row to the end of the table.
+ */
+ class CommandAppendRow extends Command {
+ /**
+ * @param {object} content The data to insert as a new row.
+ * @param {object} classes The classes to assign to the new row.
+ */
+ constructor( plugin, content, classes ) {
+ super( plugin );
+ this._content = content;
+ this._classes = classes;
+ }
+
+ saveState() {
+ let plugin = this.getPlugin();
+
+ // Only attempt to save the state if there is at least one row.
+ if( plugin.isActive() ) {
+ super.saveState();
+ }
+ }
+
+ execute() {
+ let self = this;
+ let plugin = self.getPlugin();
+ let $table = $(plugin.getTableBodyElement());
+ let $headers = $table.prev( "thead" ).find( "tr:first > th" );
+ let html = "<tr>";
+
+ // The number of headers and tabular data must align.
+ $headers.each( function( index ) {
+ let content = self._content[ index ];
+ let classes = self._classes[ index ];
+
+ html += "<td";
+
+ if( typeof classes !== 'undefined' && classes.length > 0 ) {
+ html += " class='" + classes + "'";
+ }
+
+ if( typeof content === 'undefined' ) {
+ content = "";
+ }
+
+ html += ">" + content + "</td>";
+ });
+
+ html += "</tr>";
+
+ $table.append( html );
+ }
+ }
+
+ /**
+ * Deletes the active row.
+ */
+ class CommandDeleteRow extends Command {
+ constructor( plugin ) {
+ super( plugin );
+ }
+
+ execute() {
+ let plugin = this.getPlugin();
+ plugin.deactivate();
+
+ let $cell = $(plugin.getActiveCell());
+ let $row = $cell.closest( "tr" );
+ this.setState({ id: this.getId(), row: $row });
+
+ $row.remove();
+
+ plugin.onRowDeleteAfter( $row );
+ plugin.activate();
+ }
+
+ /**
+ * Removes the row that was previously inserted.
+ */
+ undo() {
+ let plugin = this.getPlugin();
+ plugin.editDeleteRow();
+ }
+ }
+
+ $.fn[ PLUGIN_NAME ] = function( options ) {
+ var plugin;
+
+ this.each( function() {
+ if( !$.data( this, PLUGIN_KEY ) ) {
+ plugin = new Plugin( this, options );
+ $.data( this, PLUGIN_KEY, plugin );
+ }
+ });
+
+ return plugin;
+ };
+
+ window.Plugin = Plugin;
+})(jQuery, window, document);
+
js/ivy.min.js
+var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.objectCreate=$jscomp.ASSUME_ES5||"function"==typeof Object.create?Object.create:function(a){var e=function(){};e.prototype=a;return new e};$jscomp.underscoreProtoCanBeSet=function(){var a={a:!0},e={};try{return e.__proto__=a,e.a}catch(b){}return!1};
+$jscomp.setPrototypeOf="function"==typeof Object.setPrototypeOf?Object.setPrototypeOf:$jscomp.underscoreProtoCanBeSet()?function(a,e){a.__proto__=e;if(a.__proto__!==e)throw new TypeError(a+" is not extensible");return a}:null;
+$jscomp.inherits=function(a,e){a.prototype=$jscomp.objectCreate(e.prototype);a.prototype.constructor=a;if($jscomp.setPrototypeOf){var b=$jscomp.setPrototypeOf;b(a,e)}else for(b in e)if("prototype"!=b)if(Object.defineProperties){var d=Object.getOwnPropertyDescriptor(e,b);d&&Object.defineProperty(a,b,d)}else a[b]=e[b];a.superClass_=e.prototype};
+$jscomp.findInternal=function(a,e,b){a instanceof String&&(a=String(a));for(var d=a.length,h=0;h<d;h++){var f=a[h];if(e.call(b,f,h,a))return{i:h,v:f}}return{i:-1,v:void 0}};$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(a,e,b){a!=Array.prototype&&a!=Object.prototype&&(a[e]=b.value)};$jscomp.getGlobal=function(a){return"undefined"!=typeof window&&window===a?a:"undefined"!=typeof global&&null!=global?global:a};$jscomp.global=$jscomp.getGlobal(this);
+$jscomp.polyfill=function(a,e,b,d){if(e){b=$jscomp.global;a=a.split(".");for(d=0;d<a.length-1;d++){var h=a[d];h in b||(b[h]={});b=b[h]}a=a[a.length-1];d=b[a];e=e(d);e!=d&&null!=e&&$jscomp.defineProperty(b,a,{configurable:!0,writable:!0,value:e})}};$jscomp.polyfill("Array.prototype.find",function(a){return a?a:function(a,b){return $jscomp.findInternal(this,a,b).v}},"es6","es3");$jscomp.SYMBOL_PREFIX="jscomp_symbol_";
+$jscomp.initSymbol=function(){$jscomp.initSymbol=function(){};$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol)};$jscomp.Symbol=function(){var a=0;return function(e){return $jscomp.SYMBOL_PREFIX+(e||"")+a++}}();
+$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();var a=$jscomp.global.Symbol.iterator;a||(a=$jscomp.global.Symbol.iterator=$jscomp.global.Symbol("iterator"));"function"!=typeof Array.prototype[a]&&$jscomp.defineProperty(Array.prototype,a,{configurable:!0,writable:!0,value:function(){return $jscomp.arrayIterator(this)}});$jscomp.initSymbolIterator=function(){}};$jscomp.arrayIterator=function(a){var e=0;return $jscomp.iteratorPrototype(function(){return e<a.length?{done:!1,value:a[e++]}:{done:!0}})};
+$jscomp.iteratorPrototype=function(a){$jscomp.initSymbolIterator();a={next:a};a[$jscomp.global.Symbol.iterator]=function(){return this};return a};$jscomp.iteratorFromArray=function(a,e){$jscomp.initSymbolIterator();a instanceof String&&(a+="");var b=0,d={next:function(){if(b<a.length){var h=b++;return{value:e(h,a[h]),done:!1}}d.next=function(){return{done:!0,value:void 0}};return d.next()}};d[Symbol.iterator]=function(){return d};return d};
+$jscomp.polyfill("Array.prototype.keys",function(a){return a?a:function(){return $jscomp.iteratorFromArray(this,function(a){return a})}},"es6","es3");Object.equals=function(a,e){if(!(a instanceof Object&&e instanceof Object))return!1;for(var b in a)if(a.hasOwnProperty(b)&&(!e.hasOwnProperty(b)||a[b]!==e[b]&&!Object.equals(a[b],e[b])))return!1;return!0};Object.defineProperty(Object.prototype,"encode",{value:function(){return encodeURIComponent(JSON.stringify(this))}});
+Object.defineProperty(Object.prototype,"decode",{value:function(){return JSON.parse(decodeURIComponent(this))}});Array.prototype.peek=function(){return this[this.length-1]};Date.prototype.isWeekend=function(){var a=this.getDay();return 0==a||6==a};String.prototype.padLeft=function(a){return String(a+this).slice(-a.length)};
+String.prototype.toTime=function(){var a=this,e=!1,b=!1,d=0,h=0;null!=a?(e=null!==a.match(/p/i),b=null!==a.match(/a/i),a=a.replace(/^00/,"24"),a=parseInt(a.replace(/\D/g,""),10)):a=0;0<a&&24>a?d=a:100<=a&&2359>=a?(d=~~(a/100),h=a%100):2400<=a&&(h=a%100,e=!1);12==d&&!1===b?e=!0:12<d&&(e=!0,d-=12);59<h&&(h=59);return(""+d).padLeft("00")+":"+(""+h).padLeft("00")+" "+(e?"PM":"AM")};Storage.prototype.put=function(a,e){this.setItem(a,LZString.compressToUTF16(e.encode()))};
+Storage.prototype.get=function(a,e){a=this.getItem(a);return null===a?e:LZString.decompressFromUTF16(a).decode()};(function(a,e,b,d){function h(c,b){this.element=c;this.settings=a.extend({},f,b);this.settings.ivy=this;this._defaults=f;this._name="ivy";this._cell=[0,0];this._$cellInput=!1;this._commandExecutor=new k;this.init()}var f={classCellActive:"ivy-active",classCellActiveInput:"ivy-editor",classCellTransient:"ivy-transient",classCellReadOnly:"ivy-readonly",maxPageSize:30,dispatchKeysNavigate:[{k:"enter",f:"navigateDown"},{k:"up",f:"navigateUp"},{k:"down",f:"navigateDown"},{k:"left",f:"navigateLeft"},{k:"right",
+f:"navigateRight"},{k:"ctrl+up",f:"navigateUpSkip"},{k:"ctrl+down",f:"navigateDownSkip"},{k:"ctrl+left",f:"navigateLeftSkip"},{k:"ctrl+right",f:"navigateRightSkip"},{k:"pageup",f:"navigatePageUp"},{k:"pagedown",f:"navigatePageDown"},{k:"home",f:"navigateRowHome"},{k:"end",f:"navigateRowEnd"},{k:"ctrl+home",f:"navigateHome"},{k:"ctrl+end",f:"navigateEnd"},{k:"shift+tab",f:"navigateLeft"},{k:"tab",f:"navigateRight"},{k:"f2",f:"editStart"},{k:"ctrl+x",f:"editCut"},{k:"ctrl+c",f:"editCopy"},{k:"ctrl+ins",
+f:"editCopy"},{k:"del",f:"editErase"},{k:"shift+del",f:"editDeleteRow"},{k:"ins",f:"editDuplicateRow"},{k:"ctrl+i",f:"editDuplicateRow"},{k:"command+i",f:"editDuplicateRow"},{k:"shift+space",f:"editAppendRow"},{k:"ctrl+z",f:"editUndo"},{k:"command+z",f:"editUndo"},{k:"ctrl+shift+z",f:"editRedo"},{k:"ctrl+y",f:"editRedo"},{k:"command+y",f:"editRedo"}],dispatchKeysEdit:[{k:"up",f:"navigateUp"},{k:"down",f:"navigateDown"},{k:"shift+tab",f:"navigateLeft"},{k:"tab",f:"navigateRight"},{k:"enter",f:"editAccept"},
+{k:"esc",f:"editCancel"}],init:function(){},refreshCells:function(){},onCellValueChangeBefore:function(c,a,b){return c},onCellValueChangeAfter:function(c,a){},onRowDuplicateAfter:function(c){},onRowInsertAfter:function(c,a){},onRowAppendAfter:function(c,a){},onRowDeleteAfter:function(c){}};a.extend(h.prototype,{init:function(){this.settings.init();this.setup();this.bindNavigateMode();this.bindPrintableKeys();this.bindNavigationClicks();this.bindPasteHandler();this.activate()},refreshCells:function(){this.settings.refreshCells()},
+setup:function(){Mousetrap.prototype.stopCallback=function(c,a,b){c.preventDefault();return!1}},navigateTableCell:function(c){var a=c.parent().children().index(c);c=c.parent().parent().children().index(c.parent());!1===this.isActiveCell(c,a)&&this.navigate(c,a)},bindNavigationClicks:function(){var c=this,b=a(c.getTableBodyElement());b.on("click","td",function(){c.navigateTableCell(a(this))});b.on("dblclick doubletap","td",function(){c.navigateTableCell(a(this));c.editStart()})},bindPrintableKeys:function(){var c=
+this;a(c.getTableBodyElement()).on("keypress",function(a){var b="number"===typeof a.which?a.which:a.keyCode;"keypress"!==a.type||!b||a.ctrlKey||a.metaKey||(c.editStart(String.fromCharCode(b)),a.stopPropagation(),a.preventDefault())})},unbindPrintableKeys:function(){a(this.getTableBodyElement()).off("keypress")},_bindDispatchKeys:function(c){Mousetrap.reset();for(var a=this,b={},d=0;d<c.length;b={f:b.f},d++){var g=c[d].k;b.f=c[d].f;Mousetrap.bind(g,function(c){return function(b){a[c.f]()}}(b))}},bindNavigateMode:function(){this._bindDispatchKeys(this.settings.dispatchKeysNavigate)},
+bindEditMode:function(){this._bindDispatchKeys(this.settings.dispatchKeysEdit)},bindPasteHandler:function(){var c=this;a(b).on("paste",function(a){if(a.originalEvent){var b=a.originalEvent.clipboardData.getData("text");c.cellUpdate(b);a.stopPropagation();a.preventDefault()}})},_sanitizeCellIndex:function(c,a){return c>a?a:0>c?0:c},getTableBodyElement:function(){return a(this.element)[0]},getCellRow:function(){return this._cell[0]},getCellCol:function(){return this._cell[1]},setCellRow:function(c){this._cell[0]=
+this._sanitizeCellIndex(c,this.getMaxRows())},setCellCol:function(c){this._cell[1]=this._sanitizeCellIndex(c,this.getMaxCols())},getCell:function(c,a){return this.getTableBodyElement().rows[c].cells[a]},getCellLastRow:function(c){var a=this.getMaxRows();return this.getCell(a,c)},getActiveCell:function(){var c=this.getCellRow(),a=this.getCellCol();return this.getCell(c,a)},getMaxRows:function(){return this.getTableBodyElement().rows.length-1},getMaxCols:function(){return this.getTableBodyElement().rows[0].cells.length-
+1},getRowCount:function(){return this.getTableBodyElement().rows.length},isActive:function(){return 0<this.getTableBodyElement().rows.length},isActiveCell:function(c,a){return this.isActiveCellRow(c)&&this.isActiveCellCol(a)},isActiveCellRow:function(c){return c===this.getCellRow()},isActiveCellCol:function(c){return c===this.getCellCol()},isActiveCellProtected:function(){var c=a(this.getActiveCell());return c.hasClass(this.settings.classCellReadOnly)||c.hasClass(this.settings.classCellTransient)},
+activate:function(){this.setCellRow(this.getCellRow());this.setCellCol(this.getCellCol());a(this.getActiveCell()).addClass(this.settings.classCellActive)},deactivate:function(){a(this.getActiveCell()).removeClass(this.settings.classCellActive)},cellStateRetrieve:function(){var c=a(this.getActiveCell());return{cellRow:this.getCellRow(),cellCol:this.getCellCol(),cellValue:c.text()}},cellStateRestore:function(c){this._navigate(c.cellRow,c.cellCol);this.setActiveCellValue(c.cellValue)},getCommandExecutor:function(){return this._commandExecutor},
+execute:function(c){this.getCommandExecutor().execute(c)},focus:function(){a(this.getTableBodyElement()).focus()},_navigate:function(c,a){this.deactivate();this.setCellRow(c);this.setCellCol(a);this.activate();this.focus()},navigate:function(c,a){this.editStop();this.execute(new n(this,c,a))},navigateRow:function(c){this.navigate(this.getCellRow()+c,this.getCellCol())},navigateCol:function(c){this.navigate(this.getCellRow(),this.getCellCol()+c)},navigatePageUp:function(){this.navigateRow(-this.settings.maxPageSize)},
+navigatePageDown:function(){this.navigateRow(+this.settings.maxPageSize)},navigateUp:function(){this.navigateRow(-1)},navigateDown:function(){this.navigateRow(1)},navigateLeft:function(){this.navigateCol(-1)},navigateRight:function(){this.navigateCol(1)},navigateUpSkip:function(){console.log("Navigate up skip");this.navigateRow(-1)},navigateDownSkip:function(){console.log("Navigate down skip");this.navigateRow(1)},navigateLeftSkip:function(){console.log("Navigate left skip");this.navigateCol(-1)},
+navigateRightSkip:function(){console.log("Navigate right skip");this.navigateCol(1)},navigateHome:function(){this.navigate(0,0)},navigateEnd:function(){this.navigate(1E3,1E3)},navigateRowHome:function(){this.navigateCol(-1E3)},navigateRowEnd:function(){this.navigateCol(1E3)},editCut:function(){this.execute(new p(this))},editCopy:function(){var c=a("<input>"),d=a(this.getActiveCell());a("body").append(c);c.val(d.text()).select();b.execCommand("copy");c.remove()},editErase:function(){this.cellUpdate("")},
+cellUpdate:function(c){this.isActiveCellProtected()||this.execute(new u(this,c))},cellInputCreate:function(c,b){c.addClass(this.settings.classCellActiveInput);var d=c.width();c=b?b:c.text();b=a("<input>");b.css({width:d,"max-width":d});b.val(c);return b},cellInputDestroy:function(){var c=this.getCellInput(),a=c.val();c.remove();this.setCellInput(!1);return a},getCellInput:function(){return this._$cellInput},setCellInput:function(c){this._$cellInput=c},editStart:function(c){this.isActiveCellProtected()||
+this.execute(new l(this,c))},editStop:function(){this.getCellInput();var c=!1;this.isEditing()&&(c=this.cellInputDestroy(),a(this.getActiveCell()).removeClass(this.settings.classCellActiveInput),this.setActiveCellValue(c),this.focus(),this.bindNavigateMode(),this.bindPrintableKeys(),c=!0);return c},editAccept:function(){this.editStop();this.navigateDown()},isEditing:function(){return!1!==this.getCellInput()},setCellValue:function(c,b,d){c=this.settings.onCellValueChangeBefore(c,b,d);a(this.getCell(b,
+d)).text(c);this.settings.onCellValueChangeAfter(b,d)},setActiveCellValue:function(c){var a=this.getCellRow(),b=this.getCellCol();this.setCellValue(c,a,b)},setActiveCellValueSilent:function(c){if(!1!==c){var b=this.getActiveCell();a(b).text(c)}},editCancel:function(){this.editStop()&&this.editUndo()},editInsertColumn:function(c){this.execute(new q(this,c))},editDeleteColumn:function(c){this.execute(new CommandDeleteColumn(this,c))},editDuplicateRow:function(){this.execute(new m(this))},editDeleteRow:function(){this.execute(new r(this))},
+editAppendRow:function(c,a){this.execute(new t(this,c,a))},editUndo:function(){this.getCommandExecutor().undo();this.refreshCells()},editRedo:function(){this.getCommandExecutor().redo()},onRowDuplicateAfter:function(c){this.settings.onRowDuplicateAfter(c)},onRowAppendAfter:function(c,a){this.settings.onRowAppendAfter(c,a)},onRowDeleteAfter:function(a){this.settings.onRowDeleteAfter(a)}});var k=function(){this._undoStack=[];this._redoStack=[]};k.prototype.getUndoStack=function(){return this._undoStack};
+k.prototype.getRedoStack=function(){return this._redoStack};k.prototype.execute=function(a){a.execute();var c=this.getUndoStack(),b=c.peek();a.equals(b)||c.push(a)};k.prototype.undo=function(){var a=this.getUndoStack().pop();"undefined"!==typeof a&&(a.undo(),this.getRedoStack().push(a))};k.prototype.redo=function(){var a=this.getRedoStack().pop();"undefined"!==typeof a&&this.execute(a)};var g=function(a){this._plugin=a;this.saveState()};g.prototype.execute=function(){};g.prototype.equals=function(a){return"undefined"===
+typeof a?!1:Object.equals(this.getState(),a.getState())};g.prototype.undo=function(){var a=this.getPlugin();"undefined"!==typeof this.getState()&&a.cellStateRestore(this.getState())};g.prototype.getId=function(){return(new Date).getTime()};g.prototype.saveState=function(){this.setState(this.getPlugin().cellStateRetrieve())};g.prototype.setState=function(a){this._state=a};g.prototype.getState=function(){return this._state};g.prototype.getPlugin=function(){return this._plugin};var n=function(a,b,d){a=
+g.call(this,a)||this;a._row=b;a._col=d;return a};$jscomp.inherits(n,g);n.prototype.execute=function(){this.getPlugin()._navigate(this._row,this._col)};var p=function(a){return g.call(this,a)||this};$jscomp.inherits(p,g);p.prototype.execute=function(){var a=this.getPlugin();a.editCopy();a.editErase()};var l=function(a,b){a=g.call(this,a)||this;a._charCode=b;return a};$jscomp.inherits(l,g);l.prototype.execute=function(){var c=this.getPlugin();c.bindEditMode();c.unbindPrintableKeys();var b=a(c.getActiveCell()),
+d=c.cellInputCreate(b,this._charCode);c.setCellInput(d);b.html(d);d.focus();d.on("focusout",function(){c.editStop()})};var u=function(a,b){a=g.call(this,a)||this;a._cellValue=b;return a};$jscomp.inherits(u,g);u.prototype.execute=function(){this.getPlugin().setActiveCellValue(this._cellValue)};var q=function(a,b){a=g.call(this,a)||this;a._columnName=b;return a};$jscomp.inherits(q,g);q.prototype.execute=function(){var b=this.getPlugin();b.deactivate();a(b.getTableBodyElement()).parent().find("thead");
+b.activate()};var m=function(a){return g.call(this,a)||this};$jscomp.inherits(m,g);m.prototype.execute=function(){var a=this.getPlugin();a.deactivate();var b=this.getRow().clone();this.setState({id:this.getId(),clone:b});this.inject(b);a.activate()};m.prototype.inject=function(a){this.getRow().after(a);this.getPlugin().onRowDuplicateAfter(a)};m.prototype.undo=function(){this.getState().clone.remove()};m.prototype.getRow=function(){return a(this.getPlugin().getActiveCell()).closest("tr")};var t=function(a,
+b,d){a=g.call(this,a)||this;a._content=b;a._classes=d;return a};$jscomp.inherits(t,g);t.prototype.saveState=function(){this.getPlugin().isActive()&&g.prototype.saveState.call(this)};t.prototype.execute=function(){var b=this,d=b.getPlugin();d=a(d.getTableBodyElement());var g="<tr>";d.prev("thead").find("tr:first > th").each(function(a){var c=b._content[a];a=b._classes[a];g+="<td";"undefined"!==typeof a&&0<a.length&&(g+=" class='"+a+"'");"undefined"===typeof c&&(c="");g+=">"+c+"</td>"});g+="</tr>";
+d.append(g)};var r=function(a){return g.call(this,a)||this};$jscomp.inherits(r,g);r.prototype.execute=function(){var b=this.getPlugin();b.deactivate();var d=a(b.getActiveCell()).closest("tr");this.setState({id:this.getId(),row:d});d.remove();b.onRowDeleteAfter(d);b.activate()};r.prototype.undo=function(){this.getPlugin().editDeleteRow()};a.fn.ivy=function(b){var c;this.each(function(){a.data(this,"plugin_ivy")||(c=new h(this,b),a.data(this,"plugin_ivy",c))});return c};e.Plugin=h})(jQuery,window,document);$(document).ready(function(){var a={description:"Control application behaviour.",title:"Preferences",type:"object",properties:{active:{description:"Edit timesheet data for this month.",type:"string",format:"date",title:"Active Month"},weekdays:{description:"Predefine daily timeslots.",type:"array",title:"Weekdays",format:"table",uniqueItems:!0,items:{type:"object",required:"weekday",properties:{weekday:{title:"Weekday",type:"string","enum":[0,1,2,3,4,5,6],options:{enum_titles:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" ")}},
+times:{title:"Times",type:"array",format:"table",items:{type:"object",properties:{began:{type:"string",title:"Began"},ended:{type:"string",title:"Ended"}}}}}}},inclusion:{description:"Change what types of days are included.",type:"object",title:"Include",properties:{weekends:{type:"boolean",title:"Weekends",format:"checkbox"},holidays:{type:"boolean",title:"Holidays",format:"checkbox"}}},saving:{description:"Change timesheet persistence behaviour.",type:"object",title:"Saving",properties:{timeout:{description:"Time between autosaves, in seconds.",
+type:"integer",title:"Autosave",format:"number"}}},columns:{description:"Columns for custom purposes (e.g. ticket number).",type:"array",title:"Columns",format:"tabs",maxItems:3,items:{type:"string",title:"Column"}},formats:{description:"Timesheet data formats; changing these can break the application.",title:"Format",type:"object",properties:{format_date:{description:"Format for timesheet day cells.",type:"string",title:"Date","default":"YYYY-MM-DD"},format_time:{description:"Format for timesheet time cells.",
+type:"string",title:"Time","default":"HH:mm A"},format_prec:{description:"Decimal places to show for time calculations.",type:"integer",title:"Precision","default":2},format_keys:{description:"Internal format used as timesheet key index for local data storage.",type:"string",title:"Primary Key","default":"YYYYMM"}}}}},e=$("#ivy tbody").ivy({init:function(){this.initMenu();this.initHeadings();this.initTimesheet();this.initPreferencesDialog();this.initPreferencesEditor();this.initSuperhero();this.setChanged(!1)},
+initMenu:function(){for(var a=this.getPlugin(),d=[{a:"edit-cut",f:"Cut"},{a:"edit-copy",f:"Copy"},{a:"edit-undo",f:"Undo"},{a:"edit-redo",f:"Redo"},{a:"insert-shift",f:"DuplicateRow"},{a:"delete-row",f:"DeleteRow"},{a:"append-day",f:"AppendRow"}],h=d.length,f={i:0};f.i<h;f={i:f.i},f.i++)$(".app-"+d[f.i].a).on("click",function(a){return function(b){e["edit"+d[a.i].f]()}}(f));this._exporter_csv=$(".ivy-export-csv").exportable({source:a.element,filename:"timesheet.csv"});this._exporter_json=$(".ivy-export-json").exportable({source:a.element,
+filename:"timesheet.json"})},initHeadings:function(){var a=this.getPlugin();a=$(a.getTableBodyElement()).prev("thead").find("tr:first");for(var d=this.getPreferences().columns,e=d.length,f="",k=0;k<e;k++)f+="<th>"+d[k]+"</th>";a.append(f)},initTimesheet:function(){var a=this.getPlugin(),d=a.settings.classCellTransient,e=a.settings.classCellReadOnly,f=JSON.parse(this.loadTimesheet()),k=[];$.each(f,function(){var b=[];b.push(this.day);b.push("");b.push("");b.push(this.began.toTime());b.push(this.ended.toTime());
+delete this.day;delete this.began;delete this.ended;if(0===k.length){k.push(e);k.push(d);k.push(d);for(var f=Object.keys(this).length+2,h=0;h<f;h++)k.push("")}for(var l in this)this.hasOwnProperty(l)&&b.push(this[l]);a.editAppendRow(b,k)});this.fillTimesheet(k)},fillTimesheet:function(a){console.log("fill timesheet");var b=this.getPlugin(),e=this.getPreferences(),f=$(b.getCellLastRow(0)).text();f=moment(f);var k=f.clone().add(1,"month");e=e.formats.format_date;for(f.add(1,"day");f.month()<k.month();)this.isWorkDay(f)?
+(b.editAppendRow([f.format(e),"","","8:00".toTime(),"9:00".toTime()],a),f=this.getNextWorkDay(f)):f.add(1,"day");this.getPlugin().refreshCells()},initPreferencesDialog:function(){$("#settings").dialog({dialogClass:"settings-dialog",autoOpen:!1,maxHeight:.75*$(window).height(),height:"auto",width:"auto",closeOnEscape:!0,position:{my:"right top",at:"right top",of:window},buttons:{Ok:function(){$(this).dialog("close")},Cancel:function(){$(this).dialog("close")}}});$(".app-settings-preferences").on("click",
+function(a){$("#settings").dialog("open")})},initPreferencesEditor:function(){var b=document.getElementById("editor");new JSONEditor(b,{theme:"jqueryui",disable_collapse:!0,disable_edit_json:!0,disable_properties:!0,disable_array_reorder:!0,disable_array_delete_all_rows:!0,disable_array_delete_last_row:!0,required_by_default:!0,no_additional_properties:!0,remove_empty_properties:!0,schema:a,startval:this.getPreferences()})},initSuperhero:function(){var a=this,d=a.getPreferences();$(window).on("unload",
+function(){a.save()});setInterval(function(){a.save()},1E3*d.saving.timeout)},isWorkDay:function(a){return this.getPreferences().inclusion.weekends||!a.toDate().isWeekend()},getNextWorkDay:function(a){this.getPreferences();a=a.clone();do a.add(1,"day");while(!this.isWorkDay(a));return a},refreshCells:function(){for(var a=this.getPlugin().getMaxRows(),d=0;d<=a;d++)this.onCellValueChangeAfter(d,3)},onCellValueChangeBefore:function(a,d,e){if(3===e||4===e)a=a.toTime();return a},onCellValueChangeAfter:function(a,
+d){if(2!==d||1!==d||0!==d){d=this.getPlugin();var b=this.getPreferences(),e=this.updateCellTime(a,3),k=this.updateCellTime(a,4,e,60);e=moment.duration(k.diff(e)).asHours();e.toFixed&&(e=Math.abs(e).toFixed(b.formats.format_prec));$(d.getCell(a,2)).text(e);a=this.findConsecutive(a,0);b=this.sumConsecutive(a);$(d.getCell(a[0],1)).text(b);this.setChanged(!0)}},onRowDuplicateAfter:function(a){var b=this.getPlugin(),e=a.find("td:eq(4)").text();a.find("td:not(:first-child)").empty();a=a.index();b.setCellValue(e,
+a,3);b.refreshCells()},onRowDeleteAfter:function(a){a=this.getPlugin();if(0===a.getRowCount()){var b=a.settings.classCellTransient,e=a.settings.classCellReadOnly,f=moment();f=this.getNextWorkDay(f).format("YYYY-MM-01");a.editAppendRow([f,"","","8:00".toTime(),"9:00".toTime()],[e,b,b,"",""])}a.refreshCells()},updateCellTime:function(a,d,e,f){var b=this.getPlugin(),g=this.getPreferences();a=$(b.getCell(a,d));d=a.text();""==d&&(d=moment(e).add(f,"minutes"),d=d.format(g.formats.format_time),a.text(d));
+return moment.utc(d,g.formats.format_time)},findConsecutive:function(a,d){for(var b=this.getPlugin(),e=a,k=$(b.getCell(a,d)).text(),g=k;k==g&&0<=--e;)g=$(b.getCell(e,d)).text();var n=e+1,p=b.getMaxRows();e=a;for(g=k;k==g&&++e<p;)g=$(b.getCell(e,d)).text();return[n,e-1]},sumConsecutive:function(a){for(var b=this.getPlugin(),e=this.getPreferences(),f=0,k=a[0];k<=a[1];k++)f+=parseFloat($(b.getCell(k,2)).text(),10)||0;f.toFixed&&(f=f.toFixed(e.formats.format_prec));return f},setChanged:function(a){this._changed=
+a},getChanged:function(){return this._changed},getDataStore:function(){return localStorage},put:function(a,d){this.getDataStore().put(a,d)},get:function(a,d){return this.getDataStore().get(a,d)},loadTimesheet:function(){var a=this.getTimesheetKey();return this.get(a)},saveTimesheet:function(){var a=this.getTimesheetKey(),d=this.getTimesheetData();this.put(a,d)},getTimesheetKey:function(){var a=this.getPreferences();return moment(a.active,"YYYY-MM-DD").format(a.formats.format_keys)},getTimesheetData:function(){var a=
+this.getPlugin().settings.classCellTransient;return this.getExporter().export_json("."+a)},getExporter:function(){return this._exporter_json},save:function(){this.getChanged()&&!1===this.getPlugin().isEditing()&&(this.setChanged(!1),this.saveTimesheet())},getDefaultPreferences:function(){return{active:moment().format("YYYY-MM-01"),formats:{format_date:"YYYY-MM-DD",format_time:"hh:mm A",format_prec:2,format_keys:"YYYYMM"},weekdays:[{weekday:1,times:[{began:"745",ended:"930"},{began:"930",ended:"1000"},
+{began:"1000",ended:"345p"}]}],saving:{timeout:5},inclusion:{weekends:!1,holidays:!1},columns:["Description"]}},getPreferences:function(){return this.get("ivy.preferences",this.getDefaultPreferences())},setPreferences:function(a){this.put("ivy.preferences",a)},getPlugin:function(){return this.ivy}})});(function(a,e,b,d){function h(b,d){this.element=b;this.settings=a.extend({},f,d);this._defaults=f;this._name="exportable";this.init()}var f={source:"table",filename:"export.csv",exports:{csv:{file_extension:"csv",media_type_text:"text/csv",media_type_data:"application/csv",col_delimiter:'","',row_delimiter:'"\r\n"',temp_col_delimiter:String.fromCharCode(11),temp_row_delimiter:String.fromCharCode(0)},json:{file_extension:"json",media_type_text:"text/plain",media_type_data:"application/json"}},charset:"utf-8"};
+a.extend(h.prototype,{init:function(){var b=this,d=b.settings.filename,e=d.slice((d.lastIndexOf(".")-1>>>0)+2);a(b.element).on("click tap",function(a){b[e]()})},toSelector:function(a,b){"undefined"===b&&(b="");return a+":not('"+b+"')"},export_csv:function(b){var d=this.settings,e=d.exports,k=e.csv.temp_row_delimiter,f=e.csv.temp_col_delimiter,h=e.csv.row_delimiter;e=e.csv.col_delimiter;var q=this.toSelector("td",b);return'"'+a(d.source).find("tr:has(td)").map(function(b,d){return a(d).find(q).map(function(b,
+c){return a(c).text().replace(/"/g,'""')}).get().join(f)}).get().join(k).split(k).join(h).split(f).join(e)+'"'},csv:function(){var a=this.settings.exports,b=this.export_csv();this.download(b,a.csv)},export_json:function(b){var d=this.settings.source,e=[],f="thead > tr > "+this.toSelector("th",b),k=this.toSelector("td",b);a.each(a(d).parent().find(f),function(b,d){e.push(a(d).text().toLowerCase())});var h=[];a.each(a(d).find("tr"),function(b,d){var g={};a.each(a(this).find(k),function(b,c){g[e[b]]=
+a(this).text().trim()});h.push(g)});return JSON.stringify(h)},json:function(){var a=this.settings.exports,b=this.export_json();this.download(b,a.json)},download:function(b,d){var f=this.settings;e.Blob&&e.URL?(b=new Blob([b],{type:d.meda_type_text+"; charset="+f.charset}),b=URL.createObjectURL(b)):b="data:"+d.media_type_data+"; charset="+f.charset+","+encodeURIComponent(b);a(this.element).attr({download:f.filename,href:b,target:""})},toArray:function(a){return a.split(this.settings.exports.csv.row_delimiter)}});
+a.fn.exportable=function(b){var d;this.each(function(){a.data(this,"plugin_exportable")||(d=new h(this,b),a.data(this,"plugin_exportable",d))});return d};e.Plugin=h})(jQuery,window,document);
js/minify.sh
+#!/bin/bash
+
+# Script directory
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+# Path to Closure Compiler
+COMPILER=$HOME/archive/closure-javascript.jar
+
+# Java requires converting paths from UNIX to Windows under Cygwin
+case "$(uname -s)" in
+ CYGWIN*) COMPILER=$(cygpath -w $COMPILER)
+esac
+
+# Java must be in the PATH
+COMMAND="java -jar $COMPILER"
+
+# Allow overriding default filename from the command line
+cd $SCRIPT_DIR
+
+rm *.min.js > /dev/null 2>&1
+
+# Minify all files in the directory
+for js in *.js; do
+ OPTIONS="--js $js $OPTIONS"
+done
+
+$COMMAND $OPTIONS --js_output_file ivy.min.js
+
js/prototypes.js
+"use strict";
+
+/**
+ * Ensures that two objects are equal.
+ *
+ * @param {object} x The main object to compare.
+ * @param {object} y The object to check against x.
+ */
+Object.equals = function( x, y ) {
+ // 'undefined' shall not pass.
+ if( !(x instanceof Object) || !(y instanceof Object) ) return false;
+
+ // Compare all properties from x to y, recursively for objects.
+ for( let p in x ) {
+ if( !x.hasOwnProperty( p ) ) continue;
+ if( !y.hasOwnProperty( p ) ) return false;
+ if( x[ p ] === y[ p ] ) continue;
+
+ if( !Object.equals( x[ p ], y[ p ] ) ) return false;
+ }
+
+ return true;
+};
+
+/**
+ * Encodes an object into a single string that is suitable for compressing.
+ *
+ * @see http://erik.eae.net/archives/2005/06/06/22.13.54/
+ */
+Object.defineProperty( Object.prototype, 'encode', {
+ value: function() {
+ return encodeURIComponent( JSON.stringify( this ) );
+ }
+});
+
+/**
+ * Decodes an object from a string previously encoded with encode.
+ *
+ * @see http://erik.eae.net/archives/2005/06/06/22.13.54/
+ */
+Object.defineProperty( Object.prototype, 'decode', {
+ value: function() {
+ return JSON.parse( decodeURIComponent( this ) );
+ }
+});
+
+/**
+ * Returns the next item that will be popped off the stack, or "undefined" if
+ * there are no items in the array.
+ */
+Array.prototype.peek = function() {
+ return this[ this.length - 1 ];
+};
+
+/**
+ * Answers true if the date instance is on a Saturday or Sunday.
+ *
+ * @return false for weekdays.
+ */
+Date.prototype.isWeekend = function() {
+ let day = this.getDay();
+ return day == 0 || day == 6;
+};
+
+/**
+ * Pads a string with the given string. This is used to zero-pad the starting
+ * times.
+ *
+ * https://stackoverflow.com/a/14760377
+ */
+String.prototype.padLeft = function( padding ) {
+ return String( padding + this ).slice( -padding.length );
+};
+
+/**
+ * Converts a string representing a user-entered time into a normal format.
+ * The input can be of practically any sensible form to represent a time,
+ * including: 1p, 1300, 123, 12, 2355, 215p, 1a, etc. The output is a
+ * string in 12-hour format (HH:MM a), for example: 01:00 PM.
+ *
+ * https://stackoverflow.com/a/49185071/59087
+ */
+String.prototype.toTime = function() {
+ var time = this;
+ var post_meridiem = false;
+ var ante_meridiem = false;
+ var hours = 0;
+ var minutes = 0;
+
+ if( time != null ) {
+ post_meridiem = time.match( /p/i ) !== null;
+ ante_meridiem = time.match( /a/i ) !== null;
+
+ // Preserve 2400h time by changing leading zeros to 24.
+ time = time.replace( /^00/, "24" );
+
+ // Strip the string down to digits and convert to a number.
+ time = parseInt( time.replace( /\D/g, "" ), 10 );
+ }
+ else {
+ time = 0;
+ }
+
+ if( time > 0 && time < 24 ) {
+ // 1 through 23 become hours, no minutes.
+ hours = time;
+ }
+ else if( time >= 100 && time <= 2359 ) {
+ // 100 through 2359 become hours and two-digit minutes.
+ hours = ~~(time / 100);
+ minutes = time % 100;
+ }
+ else if( time >= 2400 ) {
+ // After 2400, it's midnight again.
+ minutes = (time % 100);
+ post_meridiem = false;
+ }
+
+ if( hours == 12 && ante_meridiem === false ) {
+ post_meridiem = true;
+ }
+ else if( hours > 12 ) {
+ post_meridiem = true;
+ hours -= 12;
+ }
+
+ if( minutes > 59 ) {
+ minutes = 59;
+ }
+
+ var result =
+ ("" + hours).padLeft( "00" ) + ":" + ("" + minutes).padLeft( "00" ) +
+ " " + (post_meridiem ? "PM" : "AM");
+
+ return result;
+};
+
+/**
+ * Persists the given value against the key in the browser's local storage.
+ *
+ * @param {string} key The name associated with the given value.
+ * @param {object} value The value associated with the given key name.
+ */
+Storage.prototype.put = function( key, value ) {
+ this.setItem( key, LZString.compressToUTF16( value.encode() ) );
+};
+
+/**
+ * Retrieves the value associated with the given key from the browser's
+ * local storage.
+ *
+ * @param {string} key The key name associated with a given value.
+ * @param {object} default_value Returned if no value is associated with key.
+ */
+Storage.prototype.get = function( key, default_value ) {
+ let value = this.getItem( key );
+
+ return value === null
+ ? default_value
+ : LZString.decompressFromUTF16( value ).decode();
+};
+
package.json
+{
+ "name": "timeivy",
+ "version": "0.1.0",
+ "description": "Task tracking for hourly billing",
+ "main": "index.js",
+ "scripts": {
+ "dev": "next",
+ "build": "next build",
+ "start": "next start",
+ "deploy": "now"
+ },
+ "repository": "git+https://DaveJarvis@github.com/DaveJarvis/timeivy.git",
+ "keywords": [
+ "invoices",
+ "billing",
+ "time",
+ "task",
+ "tracking"
+ ],
+ "author": "White Magic Software, Ltd.",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/DaveJarvis/timeivy/issues"
+ },
+ "homepage": "https://ivy.time.rs/timeivy",
+ "dependencies": {
+ "next": "latest",
+ "react": "latest",
+ "react-dom": "latest"
+ }
+}
robots.txt
+User-agent: googlebot
+User-agent: google
+User-agent: bingbot
+User-agent: bing
+User-agent: DuckDuckBot/1.0
+User-agent: DuckDuckBot/1.1
+Disallow: /crawler
+
+User-agent: *
+Disallow: /
+
schema/ivy.dbs
-
+<?xml version="1.0" encoding="UTF-8" ?>
+<project name="PostgreSQL" id="Project4383398" database="PostgreSQL" >
+ <schema name="public" catalogname="ivy" schemaname="public" defo="y" >
+ <table name="account" >
+ <comment>Reference to a user&#039;s account.</comment>
+ <column name="id" type="bigint" jt="-5" mandatory="y" />
+ <index name="pk_account" unique="PRIMARY_KEY" >
+ <column name="id" />
+ </index>
+ </table>
+ <table name="address" >
+ <column name="id" type="bigint" jt="-5" mandatory="y" />
+ <index name="pk_address" unique="PRIMARY_KEY" >
+ <column name="id" />
+ </index>
+ </table>
+ <table name="address_city" >
+ <column name="id" type="integer" jt="4" mandatory="y" />
+ <column name="name" type="varchar" length="100" jt="12" mandatory="y" />
+ <column name="unlocode" type="varchar" jt="12" mandatory="y" >
+ <comment><![CDATA[Location code from http://www.jms-logistics.com/en/lookup/]]></comment>
+ </column>
+ <index name="pk_address_city" unique="PRIMARY_KEY" >
+ <column name="id" />
+ </index>
+ </table>
+ <table name="address_country" >
+ <column name="id" type="bigint" jt="-5" mandatory="y" />
+ <index name="pk_address_country" unique="PRIMARY_KEY" >
+ <column name="id" />
+ </index>
+ </table>
+ <table name="address_line" >
+ <column name="id" type="bigint" jt="-5" mandatory="y" />
+ <column name="address_id" type="bigint" length="100" jt="-5" mandatory="y" >
+ <comment><![CDATA[One of many address lines.]]></comment>
+ </column>
+ <index name="pk_address_line" unique="PRIMARY_KEY" >
+ <column name="id" />
+ </index>
+ <index name="idx_address_line" unique="NORMAL" >
+ <column name="address_id" />
+ </index>
+ <fk name="fk_address_line_address" to_schema="public" to_table="address" >
+ <fk_column name="address_id" pk="id" />
+ </fk>
+ </table>
+ <table name="address_postcode" >
+ <column name="id" type="bigint" jt="-5" mandatory="y" />
+ <index name="pk_address_postcode" unique="PRIMARY_KEY" >
+ <column name="id" />
+ </index>
+ </table>
+ <table name="address_region" >
+ <column name="id" type="integer" jt="4" mandatory="y" />
+ <index name="pk_address_region" unique="PRIMARY_KEY" >
+ <column name="id" />
+ </index>
+ </table>
+ <table name="employee" >
+ <comment>Reference to an employee.</comment>
+ <column name="id" type="bigint" jt="-5" mandatory="y" />
+ <index name="pk_employee" unique="PRIMARY_KEY" >
+ <column name="id" />
+ </index>
+ </table>
+ </schema>
+ <connector name="PostgreSQL" database="PostgreSQL" driver_class="org.postgresql.Driver" driver_jar="postgresql-8.4-701.jdbc3.jar" host="localhost" port="5432" instance="ivy" user="ivy" passwd="cDBpczBvbml2eTEyMw==" schema_mapping="" />
+ <layout id="Layout4533884" name="Default" show_relation_columns="y" >
+ <entity schema="public" name="account" color="b2cdf7" x="45" y="45" />
+ <entity schema="public" name="employee" color="b2cdf7" x="45" y="135" />
+ <entity schema="public" name="address" color="b2cdf7" x="45" y="225" />
+ <entity schema="public" name="address_line" color="b2cdf7" x="210" y="225" />
+ <entity schema="public" name="address_city" color="b2cdf7" x="210" y="315" />
+ <entity schema="public" name="address_region" color="b2cdf7" x="210" y="420" />
+ <entity schema="public" name="address_country" color="b2cdf7" x="210" y="495" />
+ <entity schema="public" name="address_postcode" color="b2cdf7" x="210" y="570" />
+ </layout>
+</project>
themes/app.css
+body {
+ font-family: sans-serif;
+ font-size: 12px;
+ margin-top: 2px;
+}
+
+a.ivy {
+ text-decoration: none;
+ color: #6D942F;
+}
+
+kbd {
+ padding-left: 12px;
+}
+
+/** Drop-down menu */
+hr {
+ border: 0 none;
+ height: 1px;
+ color: #ccc;
+ background-color: #ccc;
+}
+
+nav.menu {
+ height: 2em;
+ background-color: rgb(121,181,7);
+ line-height: 2em;
+ padding-left: 1em;
+
+ -webkit-border-radius: 2px;
+ -moz-border-radius: 2px;
+ border-radius: 2px;
+}
+
+nav.menu ul {
+ list-style: none outside none;
+ margin: 0;
+ padding: 0;
+}
+
+nav.menu ul li {
+ display: inline-block;
+ padding-right: 1em;
+}
+
+nav.menu ul li ul {
+ display: none;
+ border: 1px solid black;
+ margin: 0;
+ padding: 2px;
+ background-color: rgb(248, 248, 248);
+
+ -webkit-box-shadow: 2px 2px 4px #ccc;
+ -moz-box-shadow: 2px 2px 4px #ccc;
+ box-shadow: 2px 2px 4px #ccc;
+
+ -webkit-border-radius: 2px;
+ -moz-border-radius: 2px;
+ border-radius: 2px;
+}
+
+nav.menu ul li a {
+ display: block;
+ text-decoration: none;
+ white-space: nowrap;
+
+ color: white;
+}
+
+nav.menu ul li:hover ul {
+ display: block;
+ position: absolute;
+}
+
+nav.menu ul li:hover ul li {
+ display: block;
+ float: none;
+ padding-right: 0;
+}
+
+nav.menu ul li:hover ul li:hover {
+ background-color: rgb(121,181,7);
+}
+
+nav.menu ul li ul li>a {
+ color: #333333;
+ padding-right: 6px;
+ display: inline-block;
+}
+
+nav.menu ul li ul li>kbd {
+ display: inline-block;
+ float: right;
+}
+
+/** Settings */
+.settings-dialog .ui-dialog-titlebar {
+ color: white;
+ background-color: rgb(121,181,7);
+ text-align: center;
+}
+
+.ui-state-active {
+ color: white;
+ background-color: rgb(121,181,7) !important;
+ border: 1px solid rgba(221, 221, 221, .25) !important;
+}
+
+/** Column name for daily time template. */
+th.ui-state-active:last-of-type:after {
+ content: 'Remove';
+}
+
+button.ivy {
+ padding: 0;
+ margin: 0;
+}
+
+form.ivy {
+ margin-left: auto;
+ margin-right: auto;
+ max-width: 300px;
+}
+
+form.ivy fieldset,
+form.ivy p {
+ padding: 6px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+}
+
+form.ivy fieldset {
+ border: none;
+}
+
+form.ivy label {
+ width: 85px;
+ display: inline-block;
+}
+
+form.ivy input, form.ivy textarea, form.ivy select {
+ color: #555;
+ width: 150px;
+ height: 24px;
+ border: 1px solid #E5E5E5;
+ background-color: #FBFBFB;
+ outline: 0;
+ -webkit-box-shadow: inset 1px 1px 2px rgba(238, 238, 238, 0.2);
+ box-shadow: inset 1px 1px 2px rgba(238, 238, 238, 0.2);
+}
+
+form.ivy input[type="submit"] {
+ background-color: rgb(121,181,7);
+ border: none;
+ padding: 6px 6px 6px 6px;
+ color: #FFF;
+ text-shadow: 1px 1px 1px #949494;
+ width: 65px;
+
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+
+form.ivy input[type="submit"]:hover {
+ background-color:#80A24A;
+}
+
themes/ivy.css
+/**
+ * To achieve a 1-pixel border, style only the bottom and border-right
+ * edges. The cells themselves will style the left and top borders.
+ */
+table.ivy {
+ border-collapse: separate;
+ white-space: nowrap;
+ empty-cells: show;
+ border-top: 1px solid rgba( 0, 0, 0, 0 );
+ border-left: 1px solid rgba( 0, 0, 0, 0 );
+ border-right: 1px solid rgb( 204, 204, 204 );
+ border-bottom: 1px solid rgb( 204, 204, 204 );
+ background-clip: padding-box;
+
+ /* Cell selection is handled by the app. */
+ user-select: none;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+}
+
+/**
+ * Suppress dashed border around focused table.
+ */
+table.ivy tbody {
+ outline: none;
+}
+
+/**
+ * Spreadsheets, traditionally, don't have bold headings.
+ */
+table.ivy th {
+ background-color: rgb( 243, 243, 243 );
+ font-weight: normal;
+}
+
+/**
+ * Give some space around and within all cells.
+ */
+table.ivy th, table.ivy td {
+ margin: 4px;
+ padding: 4px;
+}
+
+/**
+ * All cells have a top-left border and transparent right.
+ */
+table.ivy th, table.ivy td {
+ border-top: 1px solid rgb( 204, 204, 204 );
+ border-left: 1px solid rgb( 204, 204, 204 );
+ border-right: 1px solid rgba( 0, 0, 0, 0 );
+}
+
+/**
+* Editable cells have a transparent bottom.
+*/
+table.ivy td {
+ border-bottom: 1px solid rgba( 0, 0, 0, 0 );
+}
+
+/**
+ * Show uneditable items greyed out as visual disabled cue.
+ */
+table.ivy td.ivy-transient, table.ivy td.ivy-readonly {
+ color: gray;
+}
+
+/**
+ * Override the border settings to ensure the active cell is distinct.
+ * Due to the way borders are configured for the left/right/top/bottom per
+ * row and individual cell, setting the border for the active cell here
+ * ensures that all of its edges appear properly.
+ */
+table.ivy td.ivy-active {
+ background-color: rgba( 0, 0, 0, 0.1 );
+ border: 1px solid black;
+}
+
+/**
+ * Glowing pale cerulean blue shadow during editing.
+ */
+table.ivy td.ivy-editor {
+ box-shadow: 0 0 4px rgb( 152, 180, 212 );
+ border: 1px solid rgb( 152, 180, 212 );
+}
+
+/**
+ * Make the input field fully transparent and completely borderless.
+ */
+table.ivy td input {
+ background-color: rgba( 0, 0, 0, 0 );
+ border-width: 0px;
+ border: none;
+ padding: 0;
+ margin: 0;
+ line-height: normal;
+ font-weight: normal;
+ outline: 0;
+ box-shadow: none;
+ -moz-box-shadow: none;
+ -webkit-box-shadow: none;
+ -webkit-appearance: none;
+ overflow: hidden;
+}
+
themes/ivy.min.css
-
+*{box-sizing:border-box}body{color:#2e2e2e;max-width:800px;margin:0 auto}h1,h2,h3{font-family:'Source Sans Pro',sans-serif;color:#aeaeae}h1{font-size:3em;font-weight:normal;text-align:center}h2{font-size:2em}@media only screen and (max-width:400px){.illustration img{width:100%;height:auto;display:block;margin:0 auto}h1{font-size:2em}h2{font-size:1.5em}h3{font-size:1em}}@media only screen and (min-width:401px) and (max-width:960px){}@media only screen and (min-width:961px){.page{width:900px;max-width:900px;margin:0 auto}}table.ivy{border-collapse:separate;white-space:nowrap;empty-cells:show;border-top:1px solid rgba(0,0,0,0);border-left:1px solid rgba(0,0,0,0);border-right:1px solid #ccc;border-bottom:1px solid #ccc;background-clip:padding-box;user-select:none;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}table.ivy tbody{outline:none}table.ivy th{background-color:#f3f3f3;font-weight:normal;margin:4px;padding:4px}table.ivy td{margin:4px;padding:4px}table.ivy th{border-top:1px solid #ccc;border-left:1px solid #ccc;border-right:1px solid rgba(0,0,0,0)}table.ivy td{border-top:1px solid #ccc;border-left:1px solid #ccc;border-right:1px solid rgba(0,0,0,0);border-bottom:1px solid rgba(0,0,0,0)}table.ivy .readonly{color:gray}table.ivy td.active{background-color:rgba(0,0,0,0.1);border:1px solid black}table.ivy td.edit{box-shadow:0 0 4px #98b4d4;border:1px solid #98b4d4}table.ivy td input{background-color:rgba(0,0,0,0);border:none;padding:0;margin:0;line-height:normal;font-weight:normal;outline:0;box-shadow:none;-moz-box-shadow:none;-webkit-box-shadow:none;-webkit-appearance:none;overflow:hidden}body{font-family:sans-serif;font-size:12px;margin-top:2px}a.ivy{text-decoration:none;color:#6d942f}kbd{padding-left:12px}hr{border:0 none;height:1px;color:#ccc;background-color:#ccc}nav.menu{height:2em;background-color:#79b507;line-height:2em;padding-left:1em;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px}nav.menu ul{list-style:none outside none;margin:0;padding:0}nav.menu ul li{display:inline-block;padding-right:1em}nav.menu ul li ul{display:none;border:1px solid black;margin:0;padding:2px;background-color:#f8f8f8;-webkit-box-shadow:2px 2px 4px #ccc;-moz-box-shadow:2px 2px 4px #ccc;box-shadow:2px 2px 4px #ccc;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px}nav.menu ul li a{display:block;text-decoration:none;white-space:nowrap;color:white}nav.menu ul li:hover ul{display:block;position:absolute}nav.menu ul li:hover ul li{display:block;float:none;padding-right:0}nav.menu ul li:hover ul li:hover{background-color:#79b507}nav.menu ul li ul li>a{color:#333;padding-right:6px;display:inline-block}nav.menu ul li ul li>kbd{display:inline-block;float:right}.settings-dialog .ui-dialog-titlebar{color:white;background-color:#79b507;text-align:center}.ui-state-active{color:white;background-color:#79b507!important;border:1px solid rgba(221,221,221,.25)!important}th.ui-state-active:last-of-type:after{content:'Remove'}button.ivy{padding:0;margin:0}form.ivy{margin-left:auto;margin-right:auto;max-width:300px}form.ivy fieldset,form.ivy p{padding:6px;padding-top:0;padding-bottom:0}form.ivy fieldset{border:none}form.ivy label{width:85px;display:inline-block}form.ivy input,form.ivy textarea,form.ivy select{color:#555;width:150px;height:24px;border:1px solid #e5e5e5;background-color:#fbfbfb;outline:0;-webkit-box-shadow:inset 1px 1px 2px rgba(238,238,238,0.2);box-shadow:inset 1px 1px 2px rgba(238,238,238,0.2)}form.ivy input[type="submit"]{background-color:#79b507;border:none;padding:6px;color:#fff;text-shadow:1px 1px 1px #949494;width:65px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}form.ivy input[type="submit"]:hover{background-color:#80a24a}
themes/minify.sh
+#!/bin/bash
+
+# Script directory
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+# Path to minifier
+COMPILER=$HOME/archive/closure-stylesheets.jar
+
+# Java requires converting paths from UNIX to Windows under Cygwin
+case "$(uname -s)" in
+ CYGWIN*) COMPILER=$(cygpath -w $COMPILER)
+esac
+
+# Java must be in the PATH
+COMMAND="java -jar $COMPILER --allowed-unrecognized-property user-select"
+
+# Allow overriding default filename from the command line
+cd $SCRIPT_DIR
+
+rm *.min.css > /dev/null 2>&1
+
+# Minify all files in the directory
+for f in *.css; do
+ OPTIONS="$f $OPTIONS"
+done
+
+$COMMAND $OPTIONS > ivy.min.css
+
themes/simple.css
+* {
+ box-sizing: border-box;
+}
+
+body {
+ color: #2e2e2e;
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+h1, h2, h3 {
+ font-family: 'Source Sans Pro', sans-serif;
+ color: #aeaeae;
+}
+
+h1 {
+ font-size: 3em;
+ font-weight: normal;
+ text-align: center;
+}
+
+h2 {
+ font-size: 2em;
+}
+
+/* Mobile Styles */
+@media only screen and (max-width: 400px) {
+ .illustration img {
+ width: 100%;
+ height: auto;
+ display: block;
+ margin: 0 auto;
+ }
+
+ h1 {
+ font-size: 2em;
+ }
+
+ h2 {
+ font-size: 1.5em;
+ }
+
+ h3 {
+ font-size: 1em;
+ }
+}
+
+/* Tablet Styles */
+@media only screen and (min-width: 401px) and (max-width: 960px) {
+}
+
+/* Desktop Styles */
+@media only screen and (min-width: 961px) {
+ .page {
+ width: 900px;
+ max-width: 900px;
+ margin: 0 auto;
+ }
+}
+
timesheets/wms.csv
+2018-03-07,07:45 AM,09:15 AM,,,Refactor application to use servlet authentication
+2018-03-08,08:45 AM,09:15 AM,,,Apply Cartesian coordinate system to internal logic
+2018-03-09,09:45 AM,07:15 PM,,,Revise unit tests to check for all failing conditions
timesheets/wms.json
+[
+ ["2018-03-07", "07:45:00", "09:15:00", "", "", "Refactor application to use servlet authentication"],
+ ["2018-03-08", "08:45:00", "09:15:00", "", "", "Apply Cartesian coordinate system to internal logic"],
+ ["2018-03-09", "09:45:00", "19:15:00", "", "", "Revise unit tests to check for all failing conditions"]
+]
+

Initial commit

Author Dave Jarvis <email>
Date 2026-02-26 17:16:37 GMT-0800
Commit e09f2ac0ce8d2e06568f6ce8f8dc192187027907
Delta 4553 lines added, 2 lines removed, 4551-line increase