| +# 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 | " | ||
| + | ||
| +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. | ||
| + | ||
| +# Time Ivy | ||
| + | ||
| +Time tracking software for hourly billing. | ||
| + | ||
| +# Spreadsheet User Interface | ||
| + | ||
| +The spreadsheet interface offers keyboard bindings similar to classic | ||
| +desktop spreadsheet applications. | ||
| + | ||
| +# 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 | ||
| +<!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> | ||
| +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" | ||
| + } | ||
| + } | ||
| +} | ||
| +<!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> | ||
| +<?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> |
| +/** | ||
| + * 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); | ||
| + | ||
| +/** | ||
| + * 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; | ||
| + } | ||
| + }); | ||
| +}); | ||
| + | ||
| +/** | ||
| + * 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); | ||
| + | ||
| +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); | ||
| +#!/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 | ||
| + | ||
| +"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(); | ||
| +}; | ||
| + | ||
| +{ | ||
| + "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" | ||
| + } | ||
| +} | ||
| +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: / | ||
| + | ||
| - | ||
| +<?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'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> |
| +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; | ||
| +} | ||
| + | ||
| +/** | ||
| + * 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; | ||
| +} | ||
| + | ||
| - | ||
| +*{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} |
| +#!/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 | ||
| + | ||
| +* { | ||
| + 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; | ||
| + } | ||
| +} | ||
| + | ||
| +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 | ||
| +[ | ||
| + ["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"] | ||
| +] | ||
| + | ||
| 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 |