├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── ci ├── concourse-build.yml ├── credentials.yml.example ├── pipeline.yml └── register_pipelines.sh ├── package-lock.json ├── package.json └── src ├── css └── popup.css ├── data └── templates.json ├── html ├── download.html ├── popup.html └── sandbox.html ├── images ├── JTI-1024.png ├── JTI-128.png ├── JTI-16.png ├── JTI-256.png ├── JTI-32.png ├── JTI-48.png ├── JTI-512.png └── JTI-64.png ├── js ├── download.js ├── popup.js └── utils.js ├── jti_background.js ├── jti_content.js ├── lib ├── filesaver │ └── FileSaver.js ├── handlebars │ └── handlebars-1.0.0.beta.6.js ├── jquery │ └── jquery-2.2.3.js └── materialize │ ├── materialize.css │ └── materialize.js └── manifest.json /.eslintignore: -------------------------------------------------------------------------------- 1 | src/lib/* 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'standard', 3 | installedESLint: true, 4 | plugins: ['standard'], 5 | parserOptions: { 6 | ecmaVersion: 6, 7 | ecmaFeatures: { 8 | jsx: true, 9 | }, 10 | }, 11 | env: { 12 | browser: true, 13 | jquery: true, 14 | es6: true, 15 | }, 16 | rules: { 17 | indent: ['error', 4], 18 | semi: ['error', 'always', { omitLastInOneLineBlock: true }], 19 | 'one-var': 'off', 20 | }, 21 | globals: { 22 | utils: false, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | npm-debug.log 4 | jti.zip 5 | jti.crx 6 | key.pem 7 | ci/credentials.yml 8 | ci/credentials.yaml 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Redbrick Technologies, Inc. 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jira Template Injector 2 | 3 | Chrome Web Store [Link](https://chrome.google.com/webstore/detail/jira-template-injector/hmhpegjieopgbdmpocdmfkafjgcdmhha). 4 | If you find it useful please leave a [review](https://chrome.google.com/webstore/detail/jira-template-injector/hmhpegjieopgbdmpocdmfkafjgcdmhha/reviews)! 5 | 6 | ### About: 7 | 8 | This extension automatically inserts a template of your choosing into the JIRA Create Issue Description field. The template injected is relative to the selected "Project" and “Issue Type” within the Create Issue modal on JIRA. 9 | 10 | By default the extension works for cloud hosted JIRA instances (*.atlassian.net domains). If you are using a self hosted JIRA instance or a custom domain, you may add your own custom domains for the extension to check against. When adding custom domains, you do not need to specify the port. Just specifying the domain (XXXX.net or XXXX.com) should cover you for both http and https as well as any port. You may also add any custom input IDs for which you would like your templates to be injected into. 11 | 12 | On initial install a default list of templates are pre populated for you. These can easily be removed/restored at any time. 13 | 14 | You can add templates individually, or bulk add them through a local or remote json file. See [templates.json](https://github.com/rdbrck/jira-description-extension/blob/master/src/data/templates.json) for an example JSON file. 15 | 16 | The JSON format is: 17 | 18 | ```javascript 19 | { 20 | "templates": [ 21 | { 22 | "name":"NAME_OF_TEMPLATE", 23 | "issuetype-field":"issue_type_field", 24 | "projects-field":"comma_separated_project_keys", 25 | "text":"text_to_be_injected" 26 | }, 27 | ... 28 | ], 29 | "options": { 30 | "limit": ["clear","delete","save"], 31 | "domains": ["mydomain.com"], 32 | "inputIDs": ["custominputID"] 33 | } 34 | } 35 | ``` 36 | 37 | ### Help: 38 | 39 | * Why are some buttons disabled? 40 | * By default no buttons are disabled, but if you were provided a url or a json file to load into the extension, it may have contained some options to limit the interface (see Limit Interface Options below) 41 | * If you need to use one of the options disabled you can always reset the extension to it's default by clicking the "Reload default templates" button. **Howerver this will override any existing templates**. It is always a good idea to export your current templates first so that you have a backup 42 | 43 | ### Current Features: 44 | 45 | * Automatically select text between `````` and `````` tags for quick template completion. 46 | * Quickly jump to the next set of `````` elements using the ```Control + back-tick``` key combo. 47 | * Template Priorities. 48 | * The default template (no Issue Type and no Projects specified) will be used for all Issue Types and Projects. 49 | * Templates with Projects and no Issue Type will override the default template. 50 | * Templates with Issue Type and no Projects will override templates with Projects and no Issue Type. 51 | * Templates with Issue Type and Projects will override all other templates. 52 | * Templates are synced across devices. 53 | * Configure once, use on all chrome devices that support extensions! 54 | * Reload default templates with one click. 55 | * Load templates from url (json file). Host a single json file and have everyone use the same templates. 56 | * Load templates from local file (json). Easily share templates with other users. 57 | * Add/Remove/Edit individual templates. 58 | * Add/Remove custom domains. 59 | * Domains (including subdomains) the extension should run on to inject the templates. 60 | * Add/Remove custom input IDs. 61 | * Custom input IDs (fields of the modal) the extension should run on to inject the templates into. 62 | * Limit interface options (To keep templates consistent across users) 63 | * Current limit options are: 64 | * "all" -> disable all interface actions except reload default 65 | * "url" -> disable loading of json from url 66 | * "file" -> disable loading of json from local file 67 | * "clear" -> disable clearing of all templates 68 | * "delete" -> disable deleting single template 69 | * "save" -> disable saving/updating single template 70 | * "add" -> disable add new template menu 71 | * "add-custom" -> disable adding custom template 72 | * "add-default" -> disable adding default template 73 | * "custom-settings" -> disable access to the custom settings panel 74 | * "custom-domains" -> disable adding/removing any custom domains 75 | * "custom-input" -> disable adding/removing any custom input ids 76 | * Export Templates to JSON file. Easily share template JSON file. 77 | 78 | ### Future Features: 79 | 80 | * Expand tag detection to include pre population of various options. 81 | * Example: `````` would pre populate with 2016-04-11. 82 | 83 | ### Images 84 | ![Default Templates Loaded](https://cloud.githubusercontent.com/assets/6020196/17062770/2cb0d46e-4fe9-11e6-9f04-4daabe32537f.png "Default Templates") ![Template Editor](https://cloud.githubusercontent.com/assets/6020196/26463889/ab94d242-413a-11e7-8a01-661b5a370ac9.png "Template Editor") ![Add Template](https://cloud.githubusercontent.com/assets/6020196/26463883/a9e1dfb2-413a-11e7-9785-322872fe11eb.png "Add Template") ![Create Issue Window with auto Select](https://cloud.githubusercontent.com/assets/6020196/17062735/05e6618c-4fe9-11e6-8c9e-3a43c305c761.png "JIRA Create Issue") 85 | 86 | -------------------------------------------------------------------------------- /ci/concourse-build.yml: -------------------------------------------------------------------------------- 1 | platform: linux 2 | image_resource: 3 | type: docker-image 4 | source: 5 | repository: node 6 | inputs: 7 | - name: extension-source 8 | - name: extension-version 9 | outputs: 10 | - name: build-outputs 11 | - name: tmp-version 12 | run: 13 | path: bash 14 | dir: extension-source 15 | args: 16 | - -e 17 | - -c 18 | - | 19 | npm install --silent 20 | PR_NAME="$(git config --get pullrequest.branch || echo '')" 21 | if [ -n "$PR_NAME" ]; then 22 | VERSION="$(git show -s --format=%at)" 23 | ZIP_FILE="jti-${PR_NAME//-/_}-$VERSION" 24 | echo "Building pull request on branch '$PR_NAME' as '$ZIP_FILE'" 25 | else 26 | VERSION=$($(npm bin)/json -f src/manifest.json version) 27 | OLD_VERSION=$(grep -Eo '^([0-9]+(\.[0-9]+)*)' ../extension-version/number) 28 | SUFFIX=$(sed -r -e 's/([0-9]+(\.[0-9]+)*)//' ../extension-version/number) 29 | [ "$SUFFIX" == "-rc.1" ] && [ "$OLD_VERSION" == "$VERSION" ] && (echo "$VERSION was already released. Bump the version number!" && exit 1) 30 | VERSION="$VERSION$SUFFIX" 31 | ZIP_FILE="jti-$VERSION" 32 | echo "Building '$VERSION' as '$ZIP_FILE.zip'" 33 | fi 34 | echo $VERSION > ../tmp-version/version 35 | npm run build 36 | cp jti.zip ../build-outputs/$ZIP_FILE.zip 37 | -------------------------------------------------------------------------------- /ci/credentials.yml.example: -------------------------------------------------------------------------------- 1 | git-private-key: | 2 | -----BEGIN RSA PRIVATE KEY----- 3 | 4 | -----END RSA PRIVATE KEY----- 5 | github-access-token: 6 | 7 | s3-access-key-id: 8 | 9 | s3-secret-access-key: 10 | 11 | s3-region: 12 | 13 | google-client-id: 14 | 15 | google-client-secret: 16 | 17 | google-client-refresh-token: 18 | 19 | google-extension-id: 20 | 21 | google-extension-id-rdbrck: 22 | 23 | -------------------------------------------------------------------------------- /ci/pipeline.yml: -------------------------------------------------------------------------------- 1 | resource_types: 2 | - name: http-api 3 | type: docker-image 4 | source: 5 | repository: aequitas/http-api-resource 6 | - name: pull-request 7 | type: docker-image 8 | source: 9 | repository: jtarchie/pr 10 | 11 | groups: 12 | - name: all 13 | jobs: 14 | - jira-template-injector-pull-request-build 15 | - pull-request-build-artifact-notify 16 | - jira-template-injector-build 17 | - build-artifact-notify 18 | - jira-template-injector-release 19 | - release-artifact-notify 20 | - jira-template-injector-publish 21 | - name: build 22 | jobs: 23 | - jira-template-injector-build 24 | - build-artifact-notify 25 | - name: pull-request-build 26 | jobs: 27 | - jira-template-injector-pull-request-build 28 | - pull-request-build-artifact-notify 29 | - name: release 30 | jobs: 31 | - jira-template-injector-release 32 | - release-artifact-notify 33 | - name: publish 34 | jobs: 35 | - jira-template-injector-publish 36 | 37 | resources: 38 | - name: jti-pull-request 39 | type: pull-request 40 | source: 41 | access_token: {{github-access-token}} 42 | private_key: {{git-private-key}} 43 | repo: rdbrck/jira-template-injector 44 | uri: git@github.com:rdbrck/jira-template-injector 45 | - name: jti-master 46 | type: git 47 | source: 48 | uri: git@github.com:rdbrck/jira-template-injector 49 | private_key: {{git-private-key}} 50 | branch: master 51 | - name: pipeline-tools 52 | type: git 53 | source: 54 | uri: git@github.com:redbrickmedia/pipeline-tools 55 | private_key: {{git-private-key}} 56 | branch: master 57 | - name: jti-artifacts-pr 58 | type: s3 59 | source: 60 | bucket: chrome-extensions-artifacts 61 | regexp: jti/pr/jti-(?P[^-]*)-(?P.*)\.zip 62 | access_key_id: {{s3-access-key-id}} 63 | secret_access_key: {{s3-secret-access-key}} 64 | region_name: {{s3-region}} 65 | - name: jti-artifacts-rc 66 | type: s3 67 | source: 68 | bucket: chrome-extensions-artifacts 69 | regexp: jti/rc/jti-(.*)\.zip 70 | access_key_id: {{s3-access-key-id}} 71 | secret_access_key: {{s3-secret-access-key}} 72 | region_name: {{s3-region}} 73 | - name: jti-artifacts-release 74 | type: s3 75 | source: 76 | bucket: chrome-extensions-artifacts 77 | regexp: jti/release/jti-(.*)\.zip 78 | access_key_id: {{s3-access-key-id}} 79 | secret_access_key: {{s3-secret-access-key}} 80 | region_name: {{s3-region}} 81 | - name: jti-version 82 | type: semver 83 | source: 84 | bucket: chrome-extensions-artifacts 85 | key: jti/current-version 86 | initial_version: 0.0.1 87 | access_key_id: {{s3-access-key-id}} 88 | secret_access_key: {{s3-secret-access-key}} 89 | region_name: {{s3-region}} 90 | - name: alert 91 | type: http-api 92 | source: 93 | uri: https://hooks.slack.com/services/ 94 | method: POST 95 | headers: 96 | Content-type: application/json 97 | json: 98 | text: "{message} <{ATC_EXTERNAL_URL}/pipelines/{BUILD_PIPELINE_NAME}/jobs/{BUILD_JOB_NAME}/builds/{BUILD_NAME}|{BUILD_JOB_NAME}:{BUILD_NAME}>" 99 | # this resource exists because one cannot use {{}} injection properly when passing the "message" parameter to the "alert" resource above 100 | - name: alert-chrome-store-published 101 | type: http-api 102 | source: 103 | uri: https://hooks.slack.com/services/ 104 | method: POST 105 | headers: 106 | Content-type: application/json 107 | json: 108 | text: | 109 | Publishing release to Chrome Store succeeded! <{ATC_EXTERNAL_URL}/pipelines/{BUILD_PIPELINE_NAME}/jobs/{BUILD_JOB_NAME}/builds/{BUILD_NAME}|{BUILD_JOB_NAME}:{BUILD_NAME}> 110 | :busts_in_silhouette: 111 | 112 | jobs: 113 | - name: jira-template-injector-pull-request-build 114 | serial: true 115 | plan: 116 | - do: 117 | - aggregate: 118 | - put: alert 119 | params: {message: "Building Pull Request..."} 120 | - get: jti-pull-request 121 | version: every 122 | trigger: true 123 | - get: jti-version 124 | params: {pre: rc} 125 | - put: jti-pull-request 126 | params: 127 | path: jti-pull-request 128 | status: pending 129 | - do: 130 | - task: build-pr-extension 131 | file: jti-pull-request/ci/concourse-build.yml 132 | input_mapping: 133 | extension-source: jti-pull-request 134 | extension-version: jti-version 135 | - put: jti-artifacts-pr 136 | params: 137 | file: build-outputs/*.zip 138 | on_failure: 139 | aggregate: 140 | - put: alert 141 | params: {message: ":finnadie: Pull Request Build failed "} 142 | attempts: 5 143 | - put: jti-pull-request 144 | params: 145 | path: jti-pull-request 146 | status: failure 147 | attempts: 5 148 | on_success: 149 | aggregate: 150 | - put: alert 151 | params: {message: ":godmode: Pull Request Build succeeded"} 152 | attempts: 5 153 | - put: jti-pull-request 154 | params: 155 | path: jti-pull-request 156 | status: success 157 | attempts: 5 158 | - name: jira-template-injector-build 159 | serial: true 160 | plan: 161 | - do: 162 | - aggregate: 163 | - put: alert 164 | params: {message: "Building..."} 165 | - get: jti-master 166 | trigger: true 167 | - get: jti-version 168 | params: {pre: rc} 169 | - do: 170 | - task: build-extension 171 | file: jti-master/ci/concourse-build.yml 172 | input_mapping: 173 | extension-source: jti-master 174 | extension-version: jti-version 175 | - aggregate: 176 | - put: jti-artifacts-rc 177 | params: 178 | file: build-outputs/*.zip 179 | - put: jti-version 180 | params: {file: tmp-version/version} 181 | on_failure: 182 | put: alert 183 | params: {message: ":finnadie: Build failed "} 184 | attempts: 5 185 | on_success: 186 | put: alert 187 | params: {message: ":godmode: Build succeeded"} 188 | attempts: 5 189 | - name: jira-template-injector-release 190 | serial: true 191 | plan: 192 | - do: 193 | - put: alert 194 | params: {message: "Releasing..."} 195 | - aggregate: 196 | - get: jti-version 197 | passed: [jira-template-injector-build] 198 | params: {bump: final} 199 | trigger: false 200 | - get: jti-artifacts-rc 201 | passed: [jira-template-injector-build] 202 | trigger: false 203 | - get: pipeline-tools 204 | - aggregate: 205 | - task: release-jti-artifacts 206 | file: pipeline-tools/tasks/rename-version.yml 207 | input_mapping: 208 | source: jti-artifacts-rc 209 | version: jti-version 210 | output_mapping: {destination: build-outputs} 211 | params: 212 | BASE_NAME: jti 213 | EXTENSION: zip 214 | - aggregate: 215 | - put: jti-artifacts-release 216 | params: {file: build-outputs/jti-*.zip} 217 | - put: jti-version 218 | params: {file: jti-version/version} 219 | on_failure: 220 | put: alert 221 | params: {message: ":finnadie: Release failed "} 222 | on_success: 223 | put: alert 224 | params: {message: "Release succeeded"} 225 | 226 | - name: pull-request-build-artifact-notify 227 | plan: 228 | - aggregate: 229 | - get: jti-artifacts-pr 230 | passed: [jira-template-injector-pull-request-build] 231 | trigger: true 232 | - get: pipeline-tools 233 | - aggregate: 234 | - task: jti-pr-artifact-ready 235 | file: pipeline-tools/tasks/signed-url-notify.yml 236 | input_mapping: {artifact: jti-artifacts-pr} 237 | params: 238 | AWS_ACCESS_KEY_ID: {{s3-access-key-id}} 239 | AWS_SECRET_ACCESS_KEY: {{s3-secret-access-key}} 240 | BUCKET_NAME: chrome-extensions-artifacts 241 | WEBHOOK_URL: https://hooks.slack.com/services/ 242 | MESSAGE: "_Pull Request_ of *JIRA Template Injector* extension package is ready" 243 | - name: build-artifact-notify 244 | plan: 245 | - aggregate: 246 | - get: jti-artifacts-rc 247 | passed: [jira-template-injector-build] 248 | trigger: true 249 | - get: pipeline-tools 250 | - aggregate: 251 | - task: jti-artifact-ready 252 | file: pipeline-tools/tasks/signed-url-notify.yml 253 | input_mapping: {artifact: jti-artifacts-rc} 254 | params: 255 | AWS_ACCESS_KEY_ID: {{s3-access-key-id}} 256 | AWS_SECRET_ACCESS_KEY: {{s3-secret-access-key}} 257 | BUCKET_NAME: chrome-extensions-artifacts 258 | WEBHOOK_URL: https://hooks.slack.com/services/ 259 | MESSAGE: "_Release Candidate_ of *JIRA Template Injector* extension package is ready" 260 | - name: release-artifact-notify 261 | plan: 262 | - aggregate: 263 | - get: jti-artifacts-release 264 | passed: [jira-template-injector-release] 265 | trigger: true 266 | - get: pipeline-tools 267 | - aggregate: 268 | - task: jti-release-artifact-ready 269 | file: pipeline-tools/tasks/signed-url-notify.yml 270 | input_mapping: {artifact: jti-artifacts-release} 271 | params: 272 | AWS_ACCESS_KEY_ID: {{s3-access-key-id}} 273 | AWS_SECRET_ACCESS_KEY: {{s3-secret-access-key}} 274 | BUCKET_NAME: chrome-extensions-artifacts 275 | WEBHOOK_URL: https://hooks.slack.com/services/ 276 | MESSAGE: ":shipit: _Release_ of *JIRA Template Injector* extension package is ready" 277 | 278 | - name: jira-template-injector-publish 279 | serial: true 280 | plan: 281 | - do: 282 | - put: alert 283 | params: {message: "Publishing release to Chrome Store..."} 284 | - aggregate: 285 | - get: jti-version 286 | - get: jti-artifacts-release 287 | passed: [jira-template-injector-release] 288 | trigger: false 289 | - get: pipeline-tools 290 | - aggregate: 291 | - task: publish-jti 292 | file: pipeline-tools/tasks/chrome-store-publish.yml 293 | input_mapping: 294 | release-artifacts: jti-artifacts-release 295 | version: jti-version 296 | params: 297 | BASE_NAME: jti 298 | CLIENT_ID: {{google-client-id}} 299 | CLIENT_SECRET: {{google-client-secret}} 300 | EXTENSION_ID: {{google-extension-id}} 301 | REFRESH_TOKEN: {{google-client-refresh-token}} 302 | on_failure: 303 | put: alert 304 | params: {message: ":finnadie: Publish release to Chrome Store failed "} 305 | on_success: 306 | put: alert-chrome-store-published 307 | params: 308 | google-extension-id: {{google-extension-id}} -------------------------------------------------------------------------------- /ci/register_pipelines.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | fly -t rdbrck login --concourse-url https://concourse2.rdbrck.com/ 3 | fly -t rdbrck set-pipeline -c pipeline.yml -p jira-template-injector -l credentials.yml 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jti", 3 | "version": "1.2.6", 4 | "description": "A browser extension that automatically populates a JIRA issue's description field with a predefined template based on issue type.", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified.\" && exit 1", 7 | "lint": "eslint src/js/** src/jti_*", 8 | "clean": "rm -f jti.crx jti.zip", 9 | "bump": "versiony src/manifest.json", 10 | "pack": "crx pack src -o jti.crx --zip-output jti.zip", 11 | "build": "npm run lint && npm run pack", 12 | "rebuild": "npm run clean && npm run build" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/rdbrck/jira-template-injector.git" 17 | }, 18 | "keywords": [ 19 | "JIRA", 20 | "JIRA Agile", 21 | "Greenhopper", 22 | "Atlassian" 23 | ], 24 | "author": "Redbrick", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/rdbrck/jira-template-injector/issues" 28 | }, 29 | "devDependencies": { 30 | "crx": "^5.0.1", 31 | "eslint": "^2.13.1", 32 | "eslint-config-standard": "^5.3.1", 33 | "eslint-plugin-promise": "^1.0.8", 34 | "eslint-plugin-standard": "^1.3.2", 35 | "json": "^11.0.0", 36 | "versiony-cli": "^1.3.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/css/popup.css: -------------------------------------------------------------------------------- 1 | @import "https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,300,700,600,900"; 2 | 3 | 4 | /* Copyright 2016 Redbrick Technologies, Inc. */ 5 | /* https://github.com/rdbrck/jira-description-extension/blob/master/LICENSE */ 6 | 7 | html, body { 8 | width: 400px; 9 | min-height: 400px; 10 | overflow: auto; 11 | -ms-overflow-style: none; 12 | } 13 | 14 | body { 15 | font-family: 'Source Sans Pro',Arial,sans-serif; 16 | overflow: hidden; 17 | display: none; 18 | opacity: 0; 19 | transition: opacity 200ms ease; 20 | } 21 | 22 | nav { 23 | background: #6E9DE1; 24 | /* Old browsers */ 25 | background: -moz-linear-gradient(left,#6E9DE1 0%,#3174F1 100%); 26 | /* FF3.6-15 */ 27 | background: -webkit-linear-gradient(left,#6E9DE1 0%,#3174F1 100%); 28 | /* Chrome10-25,Safari5.1-6 */ 29 | background: linear-gradient(to right,#6E9DE1 0%,#3174F1 100%); 30 | /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ 31 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#6E9DE1',endColorstr='#3174F1',GradientType=1); 32 | /* IE6-9 */ 33 | line-height: normal; 34 | box-shadow: none !important; 35 | } 36 | 37 | nav .brand-logo { 38 | position: absolute; 39 | color: #fff; 40 | display: inline-block; 41 | font-size: 24px; 42 | padding: 0; 43 | white-space: nowrap; 44 | line-height: 40px; 45 | } 46 | 47 | main { 48 | margin-top: 15px; 49 | margin-bottom: 40px; 50 | } 51 | 52 | h5 { 53 | font-size: 1.3rem; 54 | } 55 | 56 | .container { 57 | width: 95%; 58 | } 59 | 60 | .section { 61 | padding-top: 0; 62 | padding-bottom: 0; 63 | } 64 | 65 | .row:not(.auto-height) { 66 | height: 37px; 67 | } 68 | 69 | .row { 70 | margin-bottom: 10px; 71 | } 72 | 73 | .valign-wrapper { 74 | height: inherit; 75 | } 76 | 77 | .divider { 78 | margin-bottom: 10px; 79 | overflow: visible; 80 | } 81 | 82 | .invisible-divider { 83 | height: 1px; 84 | margin-bottom: 10px; 85 | } 86 | 87 | /*noinspection CssUnusedSymbol*/ 88 | .toastNotification { 89 | opacity: .8 !important; 90 | min-height: 40px !important; 91 | max-height: 40px; 92 | } 93 | 94 | .collapsible-header.active i { 95 | -ms-transform: rotate(180deg); 96 | /* IE 9 */ 97 | -webkit-transform: rotate(180deg); 98 | /* Chrome, Safari, Opera */ 99 | transform: rotate(180deg); 100 | } 101 | 102 | .input-field { 103 | margin-top: 0; 104 | } 105 | 106 | /*noinspection CssUnusedSymbol,CssUnusedSymbol*/ 107 | .input-field .prefix.active { 108 | color: #3174F1; 109 | } 110 | 111 | /*noinspection CssUnusedSymbol*/ 112 | input[type=text].valid { 113 | border-bottom: 1px solid #3174F1 !important; 114 | box-shadow: 0 1px 0 0 #3174F1 !important; 115 | } 116 | 117 | input[type=text]:focus:not([readonly]) { 118 | border-bottom: 1px solid #3174F1 !important; 119 | box-shadow: 0 1px 0 0 #3174F1 !important; 120 | } 121 | 122 | textarea.materialize-textarea { 123 | padding: 0; 124 | min-height: 0; 125 | } 126 | 127 | textarea.materialize-textarea:focus:not([readonly]) { 128 | border-bottom: 1px solid #3174F1 !important; 129 | box-shadow: 0 1px 0 0 #3174F1 !important; 130 | } 131 | 132 | .modal.bottom-sheet { 133 | overflow: visible; 134 | max-height: 55% !important; 135 | } 136 | 137 | .modal-content { 138 | padding: 5px !important; 139 | } 140 | 141 | .dropdown-content { 142 | width: inherit; 143 | } 144 | 145 | .dropdown-content li>a,.dropdown-content li>span { 146 | color: #000; 147 | } 148 | 149 | .dropdown-content li>a:hover,.dropdown-content li>span { 150 | color: #3174F1; 151 | } 152 | 153 | .zpr { 154 | padding-right: 0 !important; 155 | } 156 | 157 | .zpl { 158 | padding-left: 0 !important; 159 | } 160 | 161 | .text-center { 162 | text-align: center; 163 | } 164 | 165 | #rateSection{ 166 | display: none; 167 | } 168 | 169 | .addTemplateLabel { 170 | padding-left: 0; 171 | } 172 | 173 | .dropdown-button.emptyDropdown { 174 | background-color: #DFDFDF !important; 175 | box-shadow: none; 176 | color: #fff !important; 177 | cursor: default; 178 | } 179 | 180 | .dropdown-button.emptyDropdown * { 181 | pointer-events: none; 182 | } 183 | 184 | .dropdown-button.emptyDropdown:hover { 185 | background-color: #DFDFDF !important; 186 | color: #9F9F9F !important; 187 | } 188 | 189 | [type="checkbox"].filled-in:checked+label:after { 190 | border: 2px solid #3174F1 !important; 191 | background-color: #3174F1 !important; 192 | } 193 | 194 | #subTitle { 195 | font-size: x-small; 196 | margin-bottom: 0; 197 | margin-top: 0; 198 | } 199 | 200 | #mainTitle { 201 | margin-bottom: 0; 202 | margin-top: 8px; 203 | margin-left: -50px; 204 | } 205 | 206 | #jsonURLInput { 207 | margin-bottom: 0 !important; 208 | } 209 | 210 | #templateEditorTitle { 211 | padding-top: 5px; 212 | font-weight: 700; 213 | } 214 | 215 | #addDefaultDropdownRow { 216 | margin-top: 10px; 217 | } 218 | 219 | #addDefaultDropdown { 220 | bottom: 16px; 221 | top: inherit !important; 222 | max-height: calc(100vh - 95px); 223 | } 224 | 225 | #addCustomTemplate { 226 | margin-left: -20px; 227 | } 228 | 229 | /*noinspection CssUnusedSymbol*/ 230 | #issueTypeField { 231 | width: 90%; 232 | } 233 | 234 | #logoIcon { 235 | width: 32px; 236 | height: 32px; 237 | float: left; 238 | margin-right: 10px; 239 | margin-top: 4px; 240 | } 241 | 242 | #help { 243 | height: 25px; 244 | margin-bottom: 0; 245 | padding-top: 5px; 246 | padding-bottom: 5px; 247 | position: absolute; 248 | color: #fff; 249 | right: 10px; 250 | top: 18px; 251 | } 252 | 253 | .custom-settings-options { 254 | margin-top: 10px; 255 | display: none; 256 | } 257 | 258 | .custom-domain-collection { 259 | padding-top: 5px; 260 | padding-bottom: 5px; 261 | padding-right: 5px; 262 | width: 400px; 263 | } 264 | 265 | .custom-inputID-collection { 266 | padding-top: 5px; 267 | padding-bottom: 5px; 268 | padding-right: 5px; 269 | width: 400px; 270 | } 271 | 272 | .remove-margin { 273 | padding-top: 0px; 274 | padding-bottom: 0px; 275 | margin-bottom: 0px; 276 | } 277 | 278 | .spaced { 279 | margin-top: 20px; 280 | margin-bottom: 15px; 281 | } 282 | 283 | .custom-label { 284 | font-size: 1.3rem; 285 | width: 290px; 286 | white-space: nowrap; 287 | overflow: hidden; 288 | text-overflow: ellipsis; 289 | padding-top: 0px; 290 | padding-bottom: 0px; 291 | margin-bottom: 0px; 292 | } 293 | 294 | .collapsible.popout > li { 295 | margin: 10px auto; 296 | box-shadow: none; 297 | border: 1px solid #ddd; 298 | } 299 | 300 | .collapsible.popout > li.active { 301 | box-shadow: none; 302 | } 303 | 304 | .collapsible-header { 305 | font-size: 13px; 306 | } 307 | 308 | .input-field .prefix { 309 | position: absolute; 310 | width: 16px; 311 | font-size: 20px; 312 | transition: color .2s; 313 | top: 15px; 314 | left: 10px; 315 | } 316 | 317 | /*Buttons*/ 318 | .btn,.btn-large,.btn-flat { 319 | border: none; 320 | border-radius: 3px; 321 | display: inline-block; 322 | height: 36px; 323 | line-height: 36px; 324 | outline: 0; 325 | padding: 0 2rem; 326 | text-transform: uppercase; 327 | vertical-align: middle; 328 | -webkit-tap-highlight-color: transparent; 329 | } 330 | 331 | .btn-jti { 332 | background: #3174F1; 333 | background: -moz-linear-gradient(top,#6e9de1 0%,#3174f1 100%); 334 | /* FF3.6-15 */ 335 | background: -webkit-linear-gradient(top,#6e9de1 0%,#3174f1 100%); 336 | /* Chrome10-25,Safari5.1-6 */ 337 | background: linear-gradient(to bottom,#6e9de1 0%,#3174f1 100%); 338 | /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ 339 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#6e9de1',endColorstr='#3174f1',GradientType=0); 340 | /* IE6-9 */ 341 | box-shadow: none; 342 | } 343 | 344 | .btn-jti:hover { 345 | background: #3174F1; 346 | background: -moz-linear-gradient(top,#6e9de1 0%,#3174f1 100%); 347 | /* FF3.6-15 */ 348 | background: -webkit-linear-gradient(top,#6e9de1 0%,#3174f1 100%); 349 | /* Chrome10-25,Safari5.1-6 */ 350 | background: linear-gradient(to bottom,#6e9de1 0%,#3174f1 100%); 351 | /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ 352 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#6e9de1',endColorstr='#3174f1',GradientType=0); 353 | /* IE6-9 */ 354 | box-shadow: none; 355 | } 356 | 357 | a.disabled i { 358 | color: #9F9F9F; 359 | cursor: default; 360 | } 361 | 362 | i.disabled { 363 | color: #9F9F9F; 364 | cursor: default; 365 | } 366 | 367 | .btn-floating i { 368 | font-size: 1.3rem; 369 | } 370 | 371 | .font-bold { 372 | font-weight: bold; 373 | } 374 | -------------------------------------------------------------------------------- /src/data/templates.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates":[ 3 | {"name": "DEFAULT TEMPLATE", "issuetype-field":"", "projects-field": "", "text":""}, 4 | {"name": "BUG TEMPLATE", "issuetype-field":"Bug", "projects-field": "", "text":"*Summary*\nEnter summary of the problem here.\n\n*Steps to Reproduce*\nEnter detailed steps to reproduce here. More detail is better.\n\n*Expected Behaviour*\nEnter what should happen here.\n\n*Additional Details*\nEnter any other details such as examples, links to requirements, etc. Any criteria that might help with fixing the problem. Attach screenshots if possible. More detail is better.\n\n*Workaround*\nIf there is a way to work around the problem, place that information here."}, 5 | {"name": "STORY TEMPLATE", "issuetype-field":"Story", "projects-field": "", "text":"*Story*\nAs a type of user/persona, I want to perform some task, so that I can achieve some goal/benefit/value.\n\n*Details*\nEnter any details, clarifications, answers to questions, or points about implementation here.\n\n*Additional Information*\nEnter any background or references such as Stack Overflow, MSDN, blogs, etc. that may help with developing the feature.\n\n*Acceptance Criteria*\nEnter the conditions of satisfaction here. That is, the conditions that will satisfy the user/persona that the goal/benefit/value has been achieved."}, 6 | {"name": "TASK TEMPLATE", "issuetype-field":"Task", "projects-field": "", "text":""}, 7 | {"name": "SUB TASK TEMPLATE", "issuetype-field":"Sub-task", "projects-field": "", "text":""}, 8 | {"name": "NEW FEATURE TEMPLATE", "issuetype-field":"New Feature", "projects-field": "", "text":"*Summary*\nEnter a summary of the New Feature here.\n\n*Details*\nEnter any details, clarifications, answers to questions, or points about implementation here.\n\n*Additional Information*\nEnter any background or references such as Stack Overflow, MSDN, blogs, etc. that may help with developing the feature."}, 9 | {"name": "IMPROVEMENT TEMPLATE", "issuetype-field":"Improvement", "projects-field": "", "text":"*Summary*\nEnter a summary of the Improvement here.\n\n*Details*\nEnter any details, clarifications, answers to questions, or points about implementation here.\n\n*Additional Information*\nEnter any background or references such as Stack Overflow, MSDN, blogs, etc. that may help with developing the feature."}, 10 | {"name": "EPIC TEMPLATE", "issuetype-field":"Epic", "projects-field": "", "text":"*Epic*\nAs a type of user/persona, I want to perform some task, so that I can achieve some goal/benefit/value.\n\n*Details*\nEnter any details, clarifications, answers to questions, and points about implementation here."} 11 | ], 12 | "options":{ 13 | "limit": [], 14 | "domains":[], 15 | "inputIDs":[] 16 | } 17 | } -------------------------------------------------------------------------------- /src/html/download.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Jira Template Injector 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Generating JSON file .... 17 | 18 | -------------------------------------------------------------------------------- /src/html/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | Jira Template Injector 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 46 | 47 |
48 |
49 |
50 |
51 |
52 | gradeRate It Now ! 53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
Reload default Templates:
61 |
62 | 67 |
68 |
69 |
70 |
71 |
72 |
73 |
Load JSON from URL:
74 |
75 |
76 | 77 |
78 | 83 |
84 |
85 |
86 |
87 |
88 |
89 |
Select file:
90 |
91 |
92 |
93 |
94 |
95 | File 96 | 97 |
98 |
99 | 100 |
101 |
102 |
103 |
104 | 109 |
110 |
111 |
112 |
113 |
114 |
115 |
Clear all Templates:
116 |
117 | 122 |
123 |
124 |
125 |
126 |
127 |
128 |
Manage Custom Settings:
129 |
130 | 135 |
136 |
137 |
138 | 139 |
140 |
    141 |
142 |
143 |
144 |
145 | Export Templates 146 |
147 |
148 |
149 | 150 | 151 | 207 | 208 | 209 | 212 |
213 | 214 | 215 |
216 |
217 | 218 |
219 | 224 |
225 |
Manage Custom Settings
226 |
227 |
228 | 229 |
230 | 231 | 232 |
233 | 238 |
239 |
Custom Domains
240 |
241 |
242 | 243 |
244 |
245 |
246 |
Add Custom Domain:
247 |
248 |
249 | 250 |
251 |
252 | 253 | add 254 | 255 |
256 |
257 |
258 |
259 | 260 |
    261 |
262 | 263 |
264 | 265 | 266 |
267 | 272 |
273 |
Custom Input IDs
274 |
275 |
276 | 277 |
278 |
279 |
280 |
Add Custom Input IDs:
281 |
282 |
283 | 284 |
285 |
286 | 287 | add 288 | 289 |
290 |
291 |
292 |
293 | 294 |
    295 |
296 |
297 |
298 | 299 | 300 | 301 | 302 | 303 | -------------------------------------------------------------------------------- /src/html/sandbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 82 | 83 | 111 | 112 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /src/images/JTI-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdbrck/jira-template-injector/406548de9acdcea3f7725f7ae3c3396ae9332177/src/images/JTI-1024.png -------------------------------------------------------------------------------- /src/images/JTI-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdbrck/jira-template-injector/406548de9acdcea3f7725f7ae3c3396ae9332177/src/images/JTI-128.png -------------------------------------------------------------------------------- /src/images/JTI-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdbrck/jira-template-injector/406548de9acdcea3f7725f7ae3c3396ae9332177/src/images/JTI-16.png -------------------------------------------------------------------------------- /src/images/JTI-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdbrck/jira-template-injector/406548de9acdcea3f7725f7ae3c3396ae9332177/src/images/JTI-256.png -------------------------------------------------------------------------------- /src/images/JTI-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdbrck/jira-template-injector/406548de9acdcea3f7725f7ae3c3396ae9332177/src/images/JTI-32.png -------------------------------------------------------------------------------- /src/images/JTI-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdbrck/jira-template-injector/406548de9acdcea3f7725f7ae3c3396ae9332177/src/images/JTI-48.png -------------------------------------------------------------------------------- /src/images/JTI-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdbrck/jira-template-injector/406548de9acdcea3f7725f7ae3c3396ae9332177/src/images/JTI-512.png -------------------------------------------------------------------------------- /src/images/JTI-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdbrck/jira-template-injector/406548de9acdcea3f7725f7ae3c3396ae9332177/src/images/JTI-64.png -------------------------------------------------------------------------------- /src/js/download.js: -------------------------------------------------------------------------------- 1 | /* global chrome, browser, saveAs */ 2 | 3 | var browserType = 'Chrome'; // eslint-disable-line no-unused-vars 4 | if (navigator.userAgent.indexOf('Firefox') !== -1 || navigator.userAgent.indexOf('Edge') !== -1) { 5 | chrome = browser; // eslint-disable-line no-native-reassign 6 | chrome.storage.sync = browser.storage.local; 7 | if (navigator.userAgent.indexOf('Firefox') !== -1) { 8 | browserType = 'Firefox'; 9 | } 10 | if (navigator.userAgent.indexOf('Edge') !== -1) { 11 | browserType = 'Edge'; 12 | } 13 | } 14 | 15 | (function (view) { 16 | var document = view.document, 17 | getBlob = function () { 18 | return view.Blob; 19 | }; 20 | 21 | chrome.runtime.sendMessage({ 22 | JDTIfunction: 'getData' 23 | }, function (response) { 24 | if (response.status === 'success') { 25 | var data = JSON.stringify(response.data, undefined, 4); 26 | var BB = getBlob(); 27 | saveAs( 28 | new BB([data], {type: 'application/json;charset=' + document.characterSet}) 29 | , 'templates.json' 30 | ); 31 | } else { 32 | console.log('Error fetching data'); 33 | } 34 | }); 35 | }(self)); 36 | -------------------------------------------------------------------------------- /src/js/popup.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2016 Redbrick Technologies, Inc. */ 2 | /* https://github.com/rdbrck/jira-description-extension/blob/master/LICENSE */ 3 | 4 | /* global chrome, browser, saveAs, Materialize */ 5 | 6 | var StorageID = 'Jira-Template-Injector'; 7 | var disabledOptionToast = 8 | 'Option is currently disabled. See ' + 9 | 'Help?' + 10 | ' for details'; 11 | 12 | var browserType = 'Chrome'; // eslint-disable-line no-unused-vars 13 | if (navigator.userAgent.indexOf('Firefox') !== -1 || navigator.userAgent.indexOf('Edge') !== -1) { 14 | chrome = browser; // eslint-disable-line no-native-reassign 15 | chrome.storage.sync = browser.storage.local; 16 | if (navigator.userAgent.indexOf('Firefox') !== -1) { 17 | browserType = 'Firefox'; 18 | } 19 | if (navigator.userAgent.indexOf('Edge') !== -1) { 20 | browserType = 'Edge'; 21 | } 22 | } 23 | 24 | function sortTemplates (templates) { 25 | var templateArray = $.map(templates, function (template, key) { 26 | return template; 27 | }); 28 | 29 | var sorted = utils.sortArrayByProperty(templateArray, 'name'); 30 | 31 | // Move "DEFAULT TEMPLATE" to top of list. 32 | $.each(sorted, function (index, template) { 33 | if (!template['issuetype-field'] && !template['projects-field']) { 34 | let defaultTemplate = sorted.splice(index, 1)[0]; 35 | sorted.unshift(defaultTemplate); 36 | return false; 37 | } 38 | }); 39 | 40 | return sorted; 41 | } 42 | 43 | function openCollapsible (templateID) { 44 | // Click header to open. 45 | $('.collapsible-header[data-templateid="' + templateID + '"]').click(); 46 | } 47 | 48 | function onInitialFocus (event) { 49 | // If this is an anchor tag, remove focus. 50 | if (event.target.tagName === 'A') { 51 | event.target.blur(); 52 | } 53 | // Remove this event listener after it is triggered. 54 | document.removeEventListener('focusin', onInitialFocus); 55 | } 56 | 57 | function loadTemplateEditor (openTemplate = null) { 58 | // remove all tooltips 59 | $('[data-toggle="tooltip"]').tooltip('remove'); 60 | // Dynamically build the template editor from stored json. 61 | chrome.storage.sync.get(StorageID, function (templates) { 62 | // Clear previous templates in the Collapsible Template Editor. 63 | $('#templateEditor').empty(); 64 | // Clear the custom template fields. 65 | $('#customTemplateName').val(''); 66 | $('#customTemplateIssueTypeField').val(''); 67 | $('#customTemplateProjectsField').val(''); 68 | // Clear the add default template dropdown. 69 | $('#addDefaultDropdown').empty(); 70 | // remove all domains, so that the whole list can be re-added in proper order. 71 | $('.custom-domain-collection').remove(); 72 | // remove all input ids, so that the whole list can be re-added in proper order. 73 | $('.custom-inputID-collection').remove(); 74 | 75 | templates = templates[StorageID].templates; 76 | 77 | // Sort Alphabetically except with DEFAULT TEMPLATE at the top. 78 | var templatesArray = sortTemplates(templates); 79 | 80 | // Send a message to sandbox.html to build the collapsible template editor 81 | // Once the template is compiled, a 'message' event will be sent to this window with the html 82 | var sandboxIFrame = document.getElementById('sandbox_window'); 83 | sandboxIFrame.contentWindow.postMessage({ 84 | command: 'renderTemplates', 85 | context: { templates: templatesArray }, 86 | openTemplate: openTemplate 87 | }, '*'); 88 | 89 | // Populate the add default template dropdown - excluding any templates already loaded. 90 | chrome.runtime.sendMessage({JDTIfunction: 'fetchDefault'}, function (response) { 91 | if (response.status === 'success') { 92 | var defaultTemplates = response.data.templates; 93 | var validDefaultTemplates = []; 94 | 95 | // Only show default templates if a template for that (issue type, projects) combination doesn't already exist. 96 | $.each(defaultTemplates, function (defaultIndex, template) { 97 | var valid = true; 98 | $.each(templatesArray, function (index, excludeTemplate) { 99 | // if the issue types are the same and (the templates have no projects specified or they have a project in common), then this template is invalid 100 | if (excludeTemplate['issuetype-field'] === template['issuetype-field']) { 101 | if (!excludeTemplate['projects-field'] && !template['projects-field'] || 102 | utils.commonItemInArrays(excludeTemplate['projects-field'], template['projects-field'])) { 103 | valid = false; 104 | return false; 105 | } 106 | } 107 | }); 108 | if (valid) { 109 | validDefaultTemplates.push(template); 110 | } 111 | }); 112 | 113 | if (!$.isEmptyObject(validDefaultTemplates)) { 114 | $('#addDefaultDropdownButton').removeClass('emptyDropdown').addClass('waves-effect waves-light'); 115 | 116 | $.each(validDefaultTemplates, function (key, template) { 117 | var dropdownData = '
  • ' + template.name + '
  • '; 118 | $('#addDefaultDropdown').append(dropdownData); 119 | }); 120 | } else { 121 | $('#addDefaultDropdownButton').addClass('emptyDropdown').removeClass('waves-effect waves-light'); 122 | } 123 | 124 | // Reload the dropdown. 125 | $('.dropdown-button').dropdown({constrain_width: false}); 126 | } else { 127 | $('#addTemplateDropdown').empty(); 128 | $('#addDefaultDropdownButton').addClass('emptyDropdown').removeClass('waves-effect waves-light'); 129 | Materialize.toast('Error loading default templates please reload the extension', 2000, 'toastNotification'); 130 | } 131 | }); 132 | }); 133 | 134 | // Check the "rate" flag. If the flag is not set, display "rate it now" button 135 | chrome.runtime.sendMessage({JDTIfunction: 'getToggleStatus', toggleType: 'rateClicked'}, function (response) { 136 | if (response.data !== true) { 137 | $('#rateSection').fadeIn(); // Show button 138 | } 139 | }); 140 | 141 | // Load in the custom domains. 142 | chrome.runtime.sendMessage({JDTIfunction: 'getDomains'}, function (response) { 143 | if (response.data) { 144 | // Send a message to sandbox.html to build the domains list 145 | // Once the template is compiled, a 'message' event will be sent to this window with the html 146 | var sandboxIFrameDomains = document.getElementById('sandbox_window'); 147 | sandboxIFrameDomains.contentWindow.postMessage({ 148 | command: 'renderObject', 149 | context: { object: response.data, classAddition: 'Domain' }, 150 | type: 'customDomainsList' 151 | }, '*'); 152 | } 153 | }); 154 | 155 | // Load in the custom input IDs. 156 | chrome.runtime.sendMessage({JDTIfunction: 'getInputIDs'}, function (response) { 157 | if (response.data) { 158 | // send a message to sandbox.html to build the input ids list 159 | // once the template is compiled, a 'message' event will be sent to this window with the html 160 | var sandboxIFrameInputIDs = document.getElementById('sandbox_window'); 161 | sandboxIFrameInputIDs.contentWindow.postMessage({ 162 | command: 'renderObject', 163 | context: { object: response.data, classAddition: 'ID' }, 164 | type: 'customIDsList' 165 | }, '*'); 166 | } 167 | }); 168 | } 169 | 170 | function limitAccess (callback = false) { 171 | // Limit interface actions from parameters passed in through json. 172 | chrome.storage.sync.get(StorageID, function (data) { 173 | if (data[StorageID].options.limit) { 174 | var limits = data[StorageID].options.limit; 175 | $.each(limits, function (key, limit) { 176 | switch (limit) { 177 | case 'all': 178 | $('#jsonURLInput').prop('disabled', true); 179 | $('#download').addClass('disabled'); 180 | $('#fileSelectorButton').addClass('disabled'); 181 | $("input[title='filePath']").prop('disabled', true); 182 | $('#upload').addClass('disabled'); 183 | $('#clear').addClass('disabled'); 184 | $('.removeSingleTemplate').addClass('disabled'); 185 | $('.updateSingleTemplate').addClass('disabled'); 186 | $('#add').addClass('disabled'); 187 | $('#addCustomTemplate').addClass('disabled'); 188 | $('#customTemplateName').prop('disabled', true); 189 | $('#customTemplateIssueTypeField').prop('disabled', true); 190 | $('#customTemplateProjectsField').prop('disabled', true); 191 | $('#addDefaultDropdownButton').addClass('disabled'); 192 | $('#customSettings').addClass('disabled'); 193 | break; 194 | case 'url': 195 | $('#jsonURLInput').prop('disabled', true); 196 | $('#download').addClass('disabled'); 197 | break; 198 | case 'file': 199 | $('#fileSelectorButton').addClass('disabled'); 200 | $('input[title="filePath"]').prop('disabled', true); 201 | $('#upload').addClass('disabled'); 202 | break; 203 | case 'clear': 204 | $('#clear').addClass('disabled'); 205 | break; 206 | case 'delete': 207 | $('.removeSingleTemplate').addClass('disabled'); 208 | break; 209 | case 'save': 210 | $('.updateSingleTemplate').addClass('disabled'); 211 | break; 212 | case 'add': 213 | $('#add').addClass('disabled'); 214 | break; 215 | case 'add-custom': 216 | $('#addCustomTemplate').addClass('disabled'); 217 | $('#customTemplateName').prop('disabled', true); 218 | $('#customTemplateIssueTypeField').prop('disabled', true); 219 | $('#customTemplateProjectsField').prop('disabled', true); 220 | break; 221 | case 'add-default': 222 | $('#addDefaultDropdownButton').addClass('disabled'); 223 | break; 224 | case 'custom-settings': 225 | $('#customSettings').addClass('disabled'); 226 | break; 227 | case 'custom-domains': 228 | $('.custom-Domain-ui').each(function () { 229 | $(this).addClass('disabled'); 230 | }); 231 | break; 232 | case 'custom-input': 233 | $('.custom-ID-ui').each(function () { 234 | $(this).addClass('disabled'); 235 | }); 236 | break; 237 | } 238 | }); 239 | } 240 | 241 | if (callback) { 242 | callback(); 243 | } 244 | }); 245 | } 246 | 247 | $(document).ready(function () { 248 | // set the display:block of the content in a timeout to avoid resizing of popup 249 | setTimeout(function () { 250 | const style = document.querySelector('body').style; 251 | style.display = 'block'; 252 | setTimeout(function () { 253 | style.opacity = 1; 254 | }); 255 | }, 150); 256 | 257 | document.addEventListener('focusin', onInitialFocus); 258 | $('#sandbox_window').load(function () { 259 | loadTemplateEditor(); 260 | }); 261 | 262 | // Click Handlers 263 | $('#reset').click(function () { 264 | chrome.runtime.sendMessage({ 265 | JDTIfunction: 'reset' 266 | }, function (response) { 267 | if (response.status === 'success') { 268 | location.reload(); 269 | Materialize.toast('Default templates successfully loaded', 2000, 'toastNotification'); 270 | } else { 271 | $('#templateEditor').empty(); 272 | if (response.message) { 273 | Materialize.toast(response.message, 2000, 'toastNotification'); 274 | } else { 275 | Materialize.toast('Something went wrong. Please try again.', 2000, 'toastNotification'); 276 | } 277 | } 278 | }); 279 | }); 280 | 281 | $('#customSettings').click(function () { 282 | if (!$(this).hasClass('disabled')) { 283 | $('.custom-settings-options').toggle(); 284 | $('main').toggle(); 285 | } else { 286 | Materialize.toast(disabledOptionToast, 2000, 'toastNotification'); 287 | } 288 | }); 289 | 290 | $('#customSettingsBackButton').click(function () { 291 | $('.custom-settings-options').toggle(); 292 | $('main').toggle(); 293 | }); 294 | 295 | $('#clearCustomIDs').click(function () { 296 | if (!$(this).hasClass('disabled')) { 297 | // remove all added input IDs: 298 | chrome.runtime.sendMessage({ 299 | JDTIfunction: 'removeInputID', 300 | removeAll: true 301 | }, function (response) { 302 | if (response.status === 'success') { 303 | loadTemplateEditor(); 304 | Materialize.toast('Input IDs successfully removed', 2000, 'toastNotification'); 305 | } else { 306 | if (response.message) { 307 | Materialize.toast(response.message, 2000, 'toastNotification'); 308 | } else { 309 | Materialize.toast('Something went wrong. Please try again.', 2000, 'toastNotifcation'); 310 | } 311 | } 312 | }); 313 | } else { 314 | Materialize.toast(disabledOptionToast, 2000, 'toastNotification'); 315 | } 316 | }); 317 | 318 | $('#clearCustomDomains').click(function () { 319 | if (!$(this).hasClass('disabled')) { 320 | // remove all added domains: 321 | chrome.runtime.sendMessage({ 322 | JDTIfunction: 'removeDomain', 323 | domainName: '', 324 | removeAll: true 325 | }, function (response) { 326 | if (response.status === 'success') { 327 | loadTemplateEditor(); 328 | Materialize.toast('Domains successfully removed', 2000, 'toastNotification'); 329 | } else { 330 | if (response.message) { 331 | Materialize.toast(response.message, 2000, 'toastNotification'); 332 | } else { 333 | Materialize.toast('Something went wrong. Please try again.', 2000, 'toastNotification'); 334 | } 335 | } 336 | }); 337 | } else { 338 | Materialize.toast(disabledOptionToast, 2000, 'toastNotification'); 339 | } 340 | }); 341 | 342 | $('#customDomainInput').keyup(function (event) { 343 | if (event.keyCode === 13) { 344 | $('#customDomainInputButton').click(); 345 | } 346 | }); 347 | 348 | $('#customDomainInputButton').click(function () { 349 | if (!$(this).hasClass('disabled')) { 350 | var domainName = $('#customDomainInput').val(); 351 | 352 | chrome.runtime.sendMessage({ 353 | JDTIfunction: 'addDomain', 354 | domainName: domainName 355 | }, function (response) { 356 | if (response.status === 'success') { 357 | $('#customDomainInput').val(''); 358 | loadTemplateEditor(); 359 | Materialize.toast('Domain successfully added', 2000, 'toastNotification'); 360 | } else { 361 | if (response.message) { 362 | Materialize.toast(response.message, 2000, 'toastNotification'); 363 | } else { 364 | Materialize.toast('Something went wrong. Please try again.', 2000, 'toastNotification'); 365 | } 366 | } 367 | }); 368 | } else { 369 | Materialize.toast(disabledOptionToast, 2000, 'toastNotification'); 370 | } 371 | }); 372 | 373 | $('#customIDInput').keyup(function (event) { 374 | if (event.keyCode === 13) { 375 | $('#customIDInputButton').click(); 376 | } 377 | }); 378 | 379 | $('#customIDInputButton').click(function () { 380 | if (!$(this).hasClass('disabled')) { 381 | var IDName = $('#customIDInput').val(); 382 | chrome.runtime.sendMessage({ 383 | JDTIfunction: 'addInputID', 384 | IDName: IDName 385 | }, function (response) { 386 | if (response.status === 'success') { 387 | $('#customIDInput').val(''); 388 | loadTemplateEditor(); 389 | Materialize.toast('Input ID successfully added', 2000, 'toastNotification'); 390 | } else { 391 | if (response.message) { 392 | Materialize.toast(response.message, 2000, 'toastNotification'); 393 | } else { 394 | Materialize.toast('Something went wrong. Please try again.', 2000, 'toastNotification'); 395 | } 396 | } 397 | }); 398 | } else { 399 | Materialize.toast(disabledOptionToast, 2000, 'toastNotification'); 400 | } 401 | }); 402 | 403 | // Because the template editing section is dynamically built, need to monitor document rather then the buttons directly 404 | $(document).on('click', '.custom-Domain-remove-button', function () { 405 | if (!$(this).hasClass('disabled')) { 406 | chrome.runtime.sendMessage({ 407 | JDTIfunction: 'removeDomain', 408 | domainID: event.target.id, 409 | removeAll: false 410 | }, function (response) { 411 | if (response.status === 'success') { 412 | loadTemplateEditor(); 413 | Materialize.toast('Domain successfully removed', 2000, 'toastNotification'); 414 | } else { 415 | if (response.message) { 416 | Materialize.toast(response.message, 2000, 'toastNotification'); 417 | } else { 418 | Materialize.toast('Something went wrong. Please try again.', 2000, 'toastNotification'); 419 | } 420 | } 421 | }); 422 | } else { 423 | Materialize.toast(disabledOptionToast, 2000, 'toastNotification'); 424 | } 425 | }); 426 | 427 | $(document).on('click', '.custom-ID-remove-button', function () { 428 | if (!$(this).hasClass('disabled')) { 429 | chrome.runtime.sendMessage({ 430 | JDTIfunction: 'removeInputID', 431 | inputID: event.target.id, 432 | removeAll: false 433 | }, function (response) { 434 | if (response.status === 'success') { 435 | loadTemplateEditor(); 436 | Materialize.toast('Input ID successfully removed', 2000, 'toastNotification'); 437 | } else { 438 | if (response.message) { 439 | Materialize.toast(response.message, 2000, 'toastNotification'); 440 | } else { 441 | Materialize.toast('Something went wrong. Please try again.', 2000, 'toastNotification'); 442 | } 443 | } 444 | }); 445 | } else { 446 | Materialize.toast(disabledOptionToast, 2000, 'toastNotification'); 447 | } 448 | }); 449 | 450 | $('#rate').click(function () { 451 | chrome.runtime.sendMessage({ 452 | JDTIfunction: 'setToggleStatus', 453 | toggleType: 'rateClicked', 454 | toggleInput: true 455 | }, function (response) { 456 | window.open('https://chrome.google.com/webstore/detail/jira-template-injector/' + chrome.runtime.id + '/reviews?hl=en', '_blank'); // Open extension user reviews page 457 | if (response.status !== 'success') { 458 | if (response.message) { 459 | Materialize.toast(response.message, 2000, 'toastNotification'); 460 | } else { 461 | Materialize.toast('Something went wrong. Please try again.', 2000, 'toastNotification'); 462 | } 463 | } 464 | }); 465 | }); 466 | 467 | $('#upload').click(function () { 468 | if (!$(this).hasClass('disabled')) { 469 | if (!$('#fileSelector')[0].files[0]) { 470 | Materialize.toast('No file selected. Please select a file and try again', 2000, 'toastNotification'); 471 | } else { 472 | var reader = new FileReader(); 473 | // Read file into memory 474 | reader.readAsText($('input#fileSelector')[0].files[0]); 475 | // Handle success and errors 476 | reader.onerror = function () { 477 | Materialize.toast('Error reading file. Please try again', 2000, 'toastNotification'); 478 | }; 479 | reader.onload = function () { 480 | chrome.runtime.sendMessage({ 481 | JDTIfunction: 'upload', 482 | fileContents: reader.result 483 | }, function (response) { 484 | if (response.status === 'success') { 485 | location.reload(); 486 | Materialize.toast('Templates successfully loaded from file', 2000, 'toastNotification'); 487 | } else { 488 | if (response.message) { 489 | Materialize.toast(response.message, 2000, 'toastNotification'); 490 | } else { 491 | Materialize.toast('Something went wrong. Please try again.', 2000, 'toastNotification'); 492 | } 493 | } 494 | }); 495 | }; 496 | } 497 | } else { 498 | Materialize.toast(disabledOptionToast, 2000, 'toastNotification'); 499 | } 500 | }); 501 | 502 | $('#download').click(function () { 503 | if (!$(this).hasClass('disabled')) { 504 | chrome.runtime.sendMessage({ 505 | JDTIfunction: 'download', 506 | 'url': $('#jsonURLInput').val() 507 | }, function (response) { 508 | if (response.status === 'success') { 509 | location.reload(); 510 | Materialize.toast('Templates successfully loaded from URL', 2000, 'toastNotification'); 511 | } else { 512 | if (response.message) { 513 | Materialize.toast(response.message, 2000, 'toastNotification'); 514 | } else { 515 | Materialize.toast('Something went wrong. Please try again.', 2000, 'toastNotification'); 516 | } 517 | } 518 | }); 519 | } else { 520 | Materialize.toast(disabledOptionToast, 2000, 'toastNotification'); 521 | } 522 | }); 523 | 524 | $('#clear').click(function () { 525 | if (!$(this).hasClass('disabled')) { 526 | chrome.runtime.sendMessage({ 527 | JDTIfunction: 'clear' 528 | }, function (response) { 529 | if (response.status === 'success') { 530 | location.reload(); 531 | Materialize.toast('All templates deleted', 2000, 'toastNotification'); 532 | } else { 533 | $('#templateEditor').empty(); 534 | if (response.message) { 535 | Materialize.toast(response.message, 2000, 'toastNotification'); 536 | } else { 537 | Materialize.toast('Something went wrong. Please try again.', 2000, 'toastNotification'); 538 | } 539 | } 540 | }); 541 | } else { 542 | Materialize.toast(disabledOptionToast, 2000, 'toastNotification'); 543 | } 544 | }); 545 | 546 | $(document).on('click', '#add', function (event) { 547 | event.preventDefault(); 548 | if (!$(this).hasClass('disabled')) { 549 | $('#addTemplateModal').openModal(); 550 | } else { 551 | Materialize.toast(disabledOptionToast, 2000, 'toastNotification'); 552 | } 553 | }); 554 | 555 | $('#addCustomTemplate').click(function () { 556 | if (!$(this).hasClass('disabled')) { 557 | var templateName = $('#customTemplateName').val(); 558 | var issueTypeField = $('#customTemplateIssueTypeField').val(); 559 | var projectsField = $('#customTemplateProjectsField').val(); 560 | 561 | chrome.runtime.sendMessage({ 562 | JDTIfunction: 'add', 563 | templateName: templateName, 564 | issueTypeField: issueTypeField, 565 | projectsField: projectsField, 566 | text: '' 567 | }, function (response) { 568 | if (response.status === 'success') { 569 | $('#addTemplateModal').closeModal(); 570 | loadTemplateEditor(response.data); 571 | Materialize.toast('Template successfully added', 2000, 'toastNotification'); 572 | } else { 573 | if (response.message) { 574 | Materialize.toast(response.message, 2000, 'toastNotification'); 575 | } else { 576 | Materialize.toast('Something went wrong. Please try again.', 2000, 'toastNotification'); 577 | } 578 | } 579 | }); 580 | } else { 581 | Materialize.toast(disabledOptionToast, 2000, 'toastNotification'); 582 | } 583 | }); 584 | 585 | $('#addDefaultDropdownButton').click(function () { 586 | if ($('#addDefaultDropdownButton').hasClass('emptyDropdown')) { 587 | Materialize.toast('All default templates have already been added', 2000, 'toastNotification'); 588 | } else if ($('#addDefaultDropdownButton').hasClass('disabled')) { 589 | Materialize.toast(disabledOptionToast, 2000, 'toastNotification'); 590 | } else { 591 | // Dropdown is not initialized on load to support disabling through json options 592 | // If it's not disabled initialize it on click 593 | var attr = $(this).attr('data-activates'); 594 | if (typeof attr === typeof undefined || attr === false) { 595 | $(this).attr('data-activates', 'addDefaultDropdown'); 596 | $('.dropdown-button').dropdown({constrain_width: false}); 597 | $(this).click(); 598 | } 599 | } 600 | }); 601 | 602 | $('#export').click(function () { 603 | if (browserType !== 'Edge') { 604 | chrome.runtime.sendMessage({ 605 | JDTIfunction: 'getData' 606 | }, function (response) { 607 | if (response.status === 'success') { 608 | var data = JSON.stringify(response.data, undefined, 4); 609 | var blob = new Blob([data], {type: 'text/json;charset=utf-8'}); 610 | saveAs(blob, 'templates.json'); 611 | } else { 612 | if (response.message) { 613 | Materialize.toast(response.message, 2000, 'toastNotification'); 614 | } else { 615 | Materialize.toast('Something went wrong. Please try again.', 2000, 'toastNotification'); 616 | } 617 | } 618 | }); 619 | } else { 620 | chrome.tabs.create({'url': chrome.extension.getURL('/html/download.html')}, function (tab) { 621 | // Tab opened. 622 | }); 623 | } 624 | }); 625 | 626 | // When the sandbox compiles a template 627 | $(window).on('message', function (event) { 628 | event = event.originalEvent; 629 | if (event.data.content === 'template-editor') { 630 | if (event.data.html) { 631 | $('#templateEditor').append(event.data.html); 632 | 633 | $('textarea').each(function (index) { 634 | if ($(this).val()) { 635 | $(this).trigger('autoresize'); 636 | } 637 | }); 638 | 639 | if (event.data.openTemplate) { 640 | openCollapsible(event.data.openTemplate); 641 | } 642 | } 643 | } else if (event.data.content === 'settings-list') { 644 | if (event.data.html) { 645 | $(`#${event.data.listID}`).append(event.data.html); 646 | } 647 | } 648 | $('[data-toggle="tooltip"]').tooltip(); 649 | limitAccess(); 650 | }); 651 | 652 | // Because the template editing section is dynamically build, need to monitor document rather then the classes directly 653 | $(document).on('click', 'a.removeSingleTemplate', function () { 654 | if (!$(this).hasClass('disabled')) { 655 | chrome.runtime.sendMessage({ 656 | JDTIfunction: 'delete', 657 | templateID: $(this).attr('template') 658 | }, function (response) { 659 | if (response.status === 'success') { 660 | loadTemplateEditor(); 661 | Materialize.toast('Template successfully removed', 2000, 'toastNotification'); 662 | } else { 663 | if (response.message) { 664 | Materialize.toast(response.message, 2000, 'toastNotification'); 665 | } else { 666 | Materialize.toast('Something went wrong. Please try again.', 2000, 'toastNotification'); 667 | } 668 | } 669 | }); 670 | } else { 671 | Materialize.toast(disabledOptionToast, 2000, 'toastNotification'); 672 | } 673 | }); 674 | 675 | $(document).on('click', 'a.updateSingleTemplate', function () { 676 | if (!$(this).hasClass('disabled')) { 677 | var templateID = $(this).attr('template'); 678 | var form = $('form[template="' + templateID + '"]'); 679 | chrome.runtime.sendMessage({ 680 | JDTIfunction: 'save', 681 | templateID: templateID, 682 | templateName: form.find('[name="nameField"]').val(), 683 | templateIssueType: form.find('[name="issueTypeField"]').val(), 684 | templateProjects: form.find('[name="projectsField"]').val(), 685 | templateText: form.find('[name="textField"]').val() 686 | }, function (response) { 687 | if (response.status === 'success') { 688 | loadTemplateEditor(templateID); 689 | Materialize.toast('Template successfully updated', 2000, 'toastNotification'); 690 | } else { 691 | if (response.message) { 692 | Materialize.toast(response.message, 2000, 'toastNotification'); 693 | } else { 694 | Materialize.toast('Something went wrong. Please try again.', 2000, 'toastNotification'); 695 | } 696 | } 697 | }); 698 | } else { 699 | Materialize.toast(disabledOptionToast, 2000, 'toastNotification'); 700 | } 701 | }); 702 | 703 | $(document).on('click', '.dropdownOption', function () { 704 | // Close the Modal 705 | var templateName = $(this).text(); 706 | var issueTypeField = $(this).data('issuefieldtype'); 707 | var text = ''; 708 | if ($('#loadDefault').prop('checked')) { 709 | text = $(this).data('text'); 710 | } 711 | 712 | chrome.runtime.sendMessage({ 713 | JDTIfunction: 'add', 714 | templateName: templateName, 715 | issueTypeField: issueTypeField, 716 | text: text 717 | }, function (response) { 718 | $('#addTemplateModal').closeModal(); 719 | if (response.status === 'success') { 720 | loadTemplateEditor(response.data); 721 | Materialize.toast('Template successfully added', 2000, 'toastNotification'); 722 | } else { 723 | loadTemplateEditor(); 724 | if (response.message) { 725 | Materialize.toast(response.message, 2000, 'toastNotification'); 726 | } else { 727 | Materialize.toast('Something went wrong. Please try again.', 2000, 'toastNotification'); 728 | } 729 | } 730 | }); 731 | }); 732 | 733 | // Resize textarea on click of collapsible header because doing it earlier doesn't resize it 100$ correctly. 734 | $(document).on('click', '.collapsible-header', function () { 735 | var collapsibleBody = $(this).siblings('.collapsible-body'); 736 | var textArea = collapsibleBody.find('textarea'); 737 | if (textArea.val()) { 738 | textArea.trigger('autoresize'); 739 | } 740 | $('html, body').animate({ 741 | scrollTop: collapsibleBody.find('[name="nameField"]').focus().offset().top - 100 742 | }, 500); 743 | }); 744 | 745 | // Force links to open in new tab 746 | $(document).on('click', '.newTabLinks', function () { 747 | chrome.tabs.create({url: $(this).attr('href')}); 748 | return false; 749 | }); 750 | }); 751 | -------------------------------------------------------------------------------- /src/js/utils.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | class utils { 3 | static commonItemInArrays (array1, array2) { 4 | if (!array1 || !array2) { 5 | return null; 6 | } 7 | 8 | var commonItem = null; 9 | $.each(array1, function (index, value) { 10 | if ($.inArray(value, array2) !== -1) { 11 | commonItem = value; 12 | return false; 13 | } 14 | }); 15 | return commonItem; 16 | } 17 | 18 | // Turn formatted projects field into an array of projects 19 | static parseProjects (projects) { 20 | if (!projects) { 21 | return []; 22 | } 23 | return projects.split(', '); 24 | } 25 | 26 | // Sorts an array of objects in place using a property of the objects 27 | static sortArrayByProperty (array, property) { 28 | return array.sort(function (a, b) { 29 | var propertyA = a[property]; 30 | var propertyB = b[property]; 31 | 32 | if (propertyA < propertyB) { 33 | return -1; 34 | } else if (propertyA > propertyB) { 35 | return 1; 36 | } else { 37 | return 0; 38 | } 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/jti_background.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2016 Redbrick Technologies, Inc. */ 2 | /* https://github.com/rdbrck/jira-description-extension/blob/master/LICENSE */ 3 | 4 | /* global chrome, browser */ 5 | 6 | var browserType = 'Chrome'; // eslint-disable-line no-unused-vars 7 | if (navigator.userAgent.indexOf('Firefox') !== -1 || navigator.userAgent.indexOf('Edge') !== -1) { 8 | chrome = browser; // eslint-disable-line no-native-reassign 9 | chrome.storage.sync = browser.storage.local; 10 | if (navigator.userAgent.indexOf('Firefox') !== -1) { 11 | browserType = 'Firefox'; 12 | } 13 | if (navigator.userAgent.indexOf('Edge') !== -1) { 14 | browserType = 'Edge'; 15 | } 16 | } 17 | 18 | var StorageID = 'Jira-Template-Injector'; 19 | var DefaultDomainList = [ 20 | {'name': 'atlassian.net'} 21 | ]; 22 | var DefaultIDList = [ 23 | {'name': 'description'} 24 | ]; 25 | var StorageToggleID = 'JTI-Toggle'; 26 | var emptyData = {'options': {'limit': [], 'domains': [], 'inputIDs': []}, 'templates': {}}; 27 | var toggles = {'rateClicked': false}; 28 | 29 | function saveTemplates (templateJSON, callback, responseData = null) { 30 | var data = {}; 31 | data[StorageID] = templateJSON; 32 | chrome.storage.sync.set(data, function () { 33 | if (chrome.runtime.lastError) { 34 | callback(false, 'Error saving data. Please try again'); 35 | } else { 36 | callback(true, null, responseData); 37 | } 38 | }); 39 | } 40 | 41 | function getInputIDs (callback) { 42 | var IDListCustom = {}; 43 | // Get the default input IDs 44 | var IDList = $.map(DefaultIDList, function (inputID, index) { 45 | inputID.default = true; 46 | return inputID; 47 | }); 48 | // Get the custom input IDs 49 | chrome.storage.sync.get(StorageID, function (data) { 50 | if (data[StorageID]) { 51 | IDListCustom = $.map(data[StorageID].options.inputIDs, function (inputID, index) { 52 | inputID.default = false; 53 | return inputID; 54 | }); 55 | // Sort them 56 | IDListCustom = utils.sortArrayByProperty(IDListCustom, 'name'); 57 | // combine so that the default entries are always at the top 58 | IDList = IDList.concat(IDListCustom); 59 | } 60 | callback(true, null, IDList); 61 | }); 62 | } 63 | 64 | function getDomains (callback) { 65 | var domainListCustom = {}; 66 | // Get the default domains 67 | var domainList = $.map(DefaultDomainList, function (domain, index) { 68 | domain.default = true; 69 | return domain; 70 | }); 71 | // Get the custom domains 72 | chrome.storage.sync.get(StorageID, function (data) { 73 | if (data[StorageID]) { 74 | domainListCustom = $.map(data[StorageID].options.domains, function (domain, index) { 75 | domain.default = false; 76 | return domain; 77 | }); 78 | // Sort them 79 | domainListCustom = utils.sortArrayByProperty(domainListCustom, 'name'); 80 | // combine so that the default entries are always at top 81 | domainList = domainList.concat(domainListCustom); 82 | } 83 | callback(true, null, domainList); 84 | }); 85 | } 86 | 87 | function fetchJSON (url, callback) { 88 | $.getJSON(url, function (templateJSON) { 89 | callback(true, null, templateJSON); 90 | }) 91 | .error(function () { 92 | callback(false, 'Invalid URL. Please correct the URL and try again', null); 93 | }); 94 | } 95 | 96 | // Get toggle status based on 'toggleType' 97 | function getToggleStatus (toggleType, callback) { 98 | chrome.storage.sync.get(StorageToggleID, function (toggles) { 99 | if (jQuery.isEmptyObject(toggles)) { // If user does not have any toggle settings in storage 100 | callback(false, 'No data is currently loaded'); 101 | } else { 102 | if (toggles[StorageToggleID][toggleType]) { 103 | callback(true, '', toggles[StorageToggleID][toggleType]); 104 | } else { 105 | callback(false, 'No data is currently loaded'); 106 | } 107 | } 108 | }); 109 | } 110 | 111 | // Set toggle status based on 'toggleType' 112 | function setToggleStatus (toggleType, toggleInput, callback) { 113 | var data = {}; 114 | toggles[toggleType] = toggleInput; 115 | data[StorageToggleID] = toggles; 116 | chrome.storage.sync.set(data, function () { 117 | if (chrome.runtime.lastError) { 118 | callback(false, 'Error saving data. Please try again'); 119 | } else { 120 | callback(true); 121 | } 122 | }); 123 | } 124 | 125 | function clearTemplates (callback) { 126 | // Need to save the domains, then re-add them here. 127 | chrome.storage.sync.get(StorageID, function (data) { 128 | var clearedData = emptyData; 129 | clearedData.options.domains = data[StorageID].options.domains; 130 | chrome.storage.sync.clear(function () { 131 | if (chrome.runtime.lastError) { 132 | callback(false, 'Error clearing data. Please try again'); 133 | } else { 134 | saveTemplates(clearedData, callback); 135 | callback(true); 136 | } 137 | }); 138 | }); 139 | } 140 | 141 | function fetchDefaultTemplates (callback) { 142 | var url = chrome.extension.getURL('data/templates.json'); 143 | fetchJSON(url, function (status, message, data) { 144 | callback(status, message, data); 145 | }); 146 | } 147 | 148 | function getData (callback) { 149 | chrome.storage.sync.get(StorageID, function (templates) { 150 | if (templates[StorageID]) { 151 | var templateJSON = dataToJSON(templates[StorageID]); 152 | callback(true, '', templateJSON); 153 | } else { 154 | callback(false, 'No data is currently loaded'); 155 | } 156 | }); 157 | } 158 | 159 | function setDefaultTemplates (callback) { 160 | fetchDefaultTemplates(function (status, message, data) { 161 | if (status) { 162 | saveTemplates(JSONtoData(data), callback); 163 | } else { 164 | callback(false, message); 165 | } 166 | }); 167 | } 168 | 169 | function downloadJSONData (url, callback) { 170 | fetchJSON(url, function (status, message, data) { 171 | if (status) { 172 | saveTemplates(JSONtoData(data), callback); 173 | } else { 174 | callback(false, message); 175 | } 176 | }); 177 | } 178 | 179 | function loadLocalFile (fileContents, callback) { 180 | try { 181 | saveTemplates(JSONtoData(JSON.parse(fileContents)), callback); 182 | } catch (e) { 183 | callback(false, 'Error parsing JSON. Please verify file contents'); 184 | } 185 | } 186 | 187 | function JSONtoData (JSONData) { 188 | // copy data provided in JSON file and other data from emptyData object 189 | var completeData = {}; 190 | $.extend(true, completeData, emptyData, JSONData); 191 | // convert the JSON data to the proper format and return the formatted data 192 | completeData.templates = JSONtoTemplateData(completeData.templates); 193 | completeData.options.domains = JSONtoDomainData(completeData.options.domains); 194 | completeData.options.inputIDs = JSONtoInputIDData(completeData.options.inputIDs); 195 | return completeData; 196 | } 197 | 198 | function dataToJSON (data) { 199 | data.templates = templateDataToJSON(data.templates); 200 | data.options.domains = domainDataToJSON(data.options.domains); 201 | data.options.inputIDs = inputIDDataToJSON(data.options.inputIDs); 202 | return data; 203 | } 204 | 205 | function removeTemplate (templateID, callback) { 206 | chrome.storage.sync.get(StorageID, function (templates) { 207 | if (templates[StorageID]) { 208 | var templateJSON = templates[StorageID]; 209 | delete templateJSON.templates[templateID]; 210 | saveTemplates(templateJSON, callback); 211 | } else { 212 | callback(false, 'No data available to remove'); 213 | } 214 | }); 215 | } 216 | 217 | function updateTemplate (templateID, templateName, templateIssueType, templateProjects, templateText, callback) { 218 | chrome.storage.sync.get(StorageID, function (templates) { 219 | if (templates[StorageID]) { 220 | var templateJSON = templates[StorageID]; 221 | var modifiedTemplate = { 222 | 'id': templateID, 223 | 'name': templateName, 224 | 'issuetype-field': templateIssueType, 225 | 'projects-field': formatProjectsField(templateProjects), 226 | 'text': templateText 227 | }; 228 | 229 | // temporarily remove the template for validation (don't want to compare the template against itself) 230 | // if validation fails, the deletion will not be saved 231 | delete templateJSON.templates[templateID]; 232 | 233 | if (validateTemplate(modifiedTemplate, templateJSON.templates, callback)) { 234 | templateJSON.templates[templateID] = modifiedTemplate; 235 | saveTemplates(templateJSON, callback); 236 | } 237 | } else { 238 | callback(false, 'No data available to update. Please recreate the template'); 239 | } 240 | }); 241 | } 242 | 243 | function addTemplate (templateName, issueTypeField, projectsField, text, callback) { 244 | chrome.storage.sync.get(StorageID, function (templates) { 245 | var templateJSON = {}; 246 | 247 | if (templates[StorageID]) { 248 | templateJSON = templates[StorageID]; 249 | } 250 | 251 | var templateID = getNextID(templateJSON.templates); 252 | var newTemplate = { 253 | 'id': templateID, 254 | 'name': templateName, 255 | 'issuetype-field': issueTypeField, 256 | 'projects-field': formatProjectsField(projectsField), 257 | 'text': text 258 | }; 259 | 260 | if (validateTemplate(newTemplate, templateJSON.templates, callback)) { 261 | templateJSON.templates[templateID] = newTemplate; 262 | saveTemplates(templateJSON, callback, templateID); 263 | } 264 | }); 265 | } 266 | 267 | function addInputID (IDName, callback) { 268 | chrome.storage.sync.get(StorageID, function (data) { 269 | var JSONData = {}; 270 | if (data[StorageID]) { 271 | JSONData = data[StorageID]; 272 | } 273 | 274 | var newID = { 275 | 'id': getNextID(JSONData.options.inputIDs), 276 | 'name': IDName 277 | }; 278 | 279 | validateInputID(IDName, function (message) { 280 | if (message) { 281 | callback(false, message); 282 | } else { 283 | JSONData.options.inputIDs[newID.id] = newID; 284 | saveTemplates(JSONData, function (status, message, data) { 285 | reloadMatchingTabs(); 286 | callback(status, message, data); 287 | }, newID.id); 288 | } 289 | }); 290 | }); 291 | } 292 | 293 | function addDomain (domainName, callback) { 294 | chrome.storage.sync.get(StorageID, function (data) { 295 | var JSONData = {}; 296 | if (data[StorageID]) { 297 | JSONData = data[StorageID]; 298 | } 299 | 300 | var newDomain = { 301 | 'id': getNextID(JSONData.options.domains), 302 | 'name': domainName 303 | }; 304 | 305 | validateDomain(domainName, function (message) { 306 | if (message) { 307 | callback(false, message); 308 | } else { 309 | JSONData.options.domains[newDomain.id] = newDomain; 310 | saveTemplates(JSONData, function (status, message, data) { 311 | reloadMatchingTabs(); 312 | callback(status, message, data); 313 | }, newDomain.id); 314 | } 315 | }); 316 | }); 317 | } 318 | 319 | function removeDomain (domainID, removeAll, callback) { 320 | chrome.storage.sync.get(StorageID, function (data) { 321 | if (data[StorageID]) { 322 | var JSONData = data[StorageID]; 323 | if (removeAll === true) { 324 | JSONData.options.domains = {}; 325 | } else { 326 | delete JSONData.options.domains[domainID]; 327 | } 328 | saveTemplates(JSONData, callback); 329 | } else { 330 | callback(false, 'No data available to remove'); 331 | } 332 | }); 333 | } 334 | 335 | function removeInputID (inputID, removeAll, callback) { 336 | chrome.storage.sync.get(StorageID, function (data) { 337 | if (data[StorageID]) { 338 | var JSONData = data[StorageID]; 339 | if (removeAll === true) { 340 | JSONData.options.inputIDs = {}; 341 | } else { 342 | delete JSONData.options.inputIDs[inputID]; 343 | } 344 | saveTemplates(JSONData, callback); 345 | } else { 346 | callback(false, 'No data available to remove'); 347 | } 348 | }); 349 | } 350 | 351 | function validateDomain (domainName, callback) { 352 | getDomains(function (status, msg, response) { 353 | // Verify that there are no empty domains 354 | let message = null; 355 | if (!domainName) { 356 | message = 'Domain Name is blank'; 357 | } 358 | // Verify that there are no duplicate domains 359 | $.each(response, function (index, domain) { 360 | if (domain.name.localeCompare(domainName) === 0) { 361 | message = `Domain Name: "${domainName}" already exists`; 362 | return false; 363 | } 364 | }); 365 | callback(message); 366 | }); 367 | } 368 | 369 | function validateInputID (IDName, callback) { 370 | getInputIDs(function (status, msg, response) { 371 | // Verify that there are no empty input IDs 372 | let message = null; 373 | if (!IDName) { 374 | message = 'Input ID is blank'; 375 | } 376 | // Verify that there are no duplicate input IDs 377 | $.each(response, function (index, inputID) { 378 | if (inputID.name.localeCompare(IDName) === 0) { 379 | message = `Input ID: "${IDName}" already exists`; 380 | return false; 381 | } 382 | }); 383 | callback(message); 384 | }); 385 | } 386 | 387 | // Make sure that the (issue type, project) combination is unique 388 | function validateTemplate (newTemplate, templates, callback) { 389 | var valid = true; 390 | var newTemplateProjects = utils.parseProjects(newTemplate['projects-field']); 391 | $.each(templates, function (name, template) { 392 | if (newTemplate['issuetype-field'] === template['issuetype-field']) { 393 | // Can't have two default templates (no issue type, no projects) 394 | if (!newTemplate['issuetype-field'] && !newTemplate['projects-field'] && !template['projects-field']) { 395 | callback(false, 'Default template ' + template.name + ' already exists', template.id); 396 | valid = false; 397 | return false; 398 | // Can't have two templates with no issue type that both have the same project in their list of projects 399 | } else if (!newTemplate['issuetype-field']) { 400 | let commonProject = utils.commonItemInArrays(newTemplateProjects, utils.parseProjects(template['projects-field'])); 401 | if (commonProject) { 402 | callback(false, 'Template ' + template.name + ' already exists for project ' + commonProject, template.id); 403 | valid = false; 404 | return false; 405 | } 406 | // Can't have two templates with the same issue type and no projects 407 | } else if (!newTemplate['projects-field'] && !template['projects-field']) { 408 | callback(false, 'Template ' + template.name + ' already exists for issue type ' + newTemplate['issuetype-field'], template.id); 409 | valid = false; 410 | return false; 411 | // Can't have two templates with the same issue type that both have the same project in their list of projects 412 | } else if (newTemplate['projects-field'] && template['projects-field']) { 413 | let commonProject = utils.commonItemInArrays(newTemplateProjects, utils.parseProjects(template['projects-field'])); 414 | if (commonProject) { 415 | callback(false, 'Template ' + template.name + ' already exists for issue type ' + newTemplate['issuetype-field'] + ' and project ' + commonProject, template.id); 416 | valid = false; 417 | return false; 418 | } 419 | } 420 | } 421 | }); 422 | return valid; 423 | } 424 | 425 | function responseMessage (status, message = null, data = null) { 426 | var response = {}; 427 | 428 | if (status) { 429 | response = {status: 'success', message: message, data: data}; 430 | } else { 431 | response = {status: 'error', message: message, data: data}; 432 | } 433 | 434 | return response; 435 | } 436 | 437 | function replaceAllString (originalString, replace, replaceWith) { 438 | return originalString.split(replace).join(replaceWith); 439 | }; 440 | 441 | function matchRegexToJsRegex (match) { 442 | return new RegExp(replaceAllString(match, '*', '\\S*')); 443 | } 444 | 445 | // Parse projects field and save it as a comma separated list, ensuring common format 446 | function formatProjectsField (projectsField) { 447 | if (!projectsField) { 448 | return ''; 449 | } 450 | 451 | // Replace all commas with spaces 452 | projectsField = projectsField.replace(/,/g, ' '); 453 | 454 | // Remove leading and trailing spaces 455 | projectsField = $.trim(projectsField); 456 | 457 | // Replace groups of spaces with a comma and a space 458 | projectsField = projectsField.replace(/\s+/g, ', '); 459 | 460 | return projectsField; 461 | } 462 | 463 | function migrateTemplateKeys (callback = null) { 464 | if (!callback) { 465 | callback = function (status, message) {}; 466 | } 467 | 468 | chrome.storage.sync.get(StorageID, function (templates) { 469 | if (!templates[StorageID]) { 470 | return; 471 | } 472 | 473 | var templateJSON = templates[StorageID]; 474 | 475 | // If data is in old format, migrate it 476 | $.each(templateJSON.templates, function (key, template) { 477 | if (!template.name) { 478 | templateJSON.templates = JSONtoTemplateData(templateJSON.templates); 479 | saveTemplates(templateJSON, callback); 480 | } 481 | return false; 482 | }); 483 | }); 484 | } 485 | 486 | function JSONtoTemplateData (templates) { 487 | var nextID = getNextID(templates); 488 | var formattedTemplates = {}; 489 | 490 | if (templates.constructor === Array) { 491 | $.each(templates, function (index, template) { 492 | template.id = nextID; 493 | formattedTemplates[nextID++] = template; 494 | }); 495 | } else { // support old template format 496 | $.each(templates, function (key, template) { 497 | template.name = key; 498 | template.id = nextID; 499 | formattedTemplates[nextID++] = template; 500 | }); 501 | } 502 | 503 | return formattedTemplates; 504 | } 505 | 506 | function JSONtoDomainData (domains, callback) { 507 | var formattedDomains = {}; 508 | if (domains && domains.constructor === Array) { 509 | var nextID = getNextID(domains); 510 | $.each(domains, function (index, domain) { 511 | validateJSONDomainEntry(domain, callback); 512 | var newDomain = { 513 | 'id': nextID, 514 | 'name': domain 515 | }; 516 | formattedDomains[newDomain.id] = newDomain; 517 | nextID++; 518 | }); 519 | } 520 | 521 | return formattedDomains; 522 | } 523 | 524 | function JSONtoInputIDData (inputIDs, callback) { 525 | var formattedInputIDs = {}; 526 | if (inputIDs && inputIDs.constructor === Array) { 527 | var nextID = getNextID(inputIDs); 528 | $.each(inputIDs, function (index, inputID) { 529 | validateJSONInputIDEntry(inputID, callback); 530 | var newInputID = { 531 | 'id': nextID, 532 | 'name': inputID 533 | }; 534 | formattedInputIDs[newInputID.id] = newInputID; 535 | nextID++; 536 | }); 537 | } 538 | 539 | return formattedInputIDs; 540 | } 541 | 542 | function validateJSONDomainEntry (domain, callback) { 543 | if (!domain || typeof (domain) !== 'string') { 544 | callback(false, 'Error parsing JSON. Please verify file contents'); 545 | } 546 | } 547 | 548 | function validateJSONInputIDEntry (inputID, callback) { 549 | if (!inputID || typeof (inputID) !== 'string') { 550 | callback(false, 'Error parsing JSON. Please verify file contents'); 551 | } 552 | } 553 | 554 | function templateDataToJSON (templates) { 555 | var formattedTemplates = []; 556 | 557 | $.each(templates, function (key, template) { 558 | delete template.id; 559 | formattedTemplates.push(template); 560 | }); 561 | return formattedTemplates; 562 | } 563 | 564 | function domainDataToJSON (domains) { 565 | var formattedDomains = []; 566 | 567 | $.each(domains, function (key, domain) { 568 | formattedDomains.push(domain.name); 569 | }); 570 | 571 | return formattedDomains; 572 | } 573 | 574 | function inputIDDataToJSON (inputIDs) { 575 | var formattedInputIDs = []; 576 | 577 | $.each(inputIDs, function (key, inputID) { 578 | formattedInputIDs.push(inputID.name); 579 | }); 580 | 581 | return formattedInputIDs; 582 | } 583 | 584 | function getNextID (templates) { 585 | var highestID = 0; 586 | $.each(templates, function (key, template) { 587 | var templateID = parseInt(template.id); 588 | if (templateID && templateID > highestID) { 589 | highestID = templateID; 590 | } 591 | }); 592 | 593 | return highestID + 1; 594 | } 595 | 596 | // This file will load the default templates into storage on install or update if no previous versions are already loaded. 597 | chrome.storage.sync.get(StorageID, function (templates) { 598 | // Check if we have any loaded templates in storage. 599 | if (Object.keys(templates).length === 0 && JSON.stringify(templates) === JSON.stringify({})) { 600 | // No data in storage yet - Load default templates. 601 | setDefaultTemplates(function (status, result) {}); 602 | } 603 | }); 604 | 605 | function reloadMatchingTabs () { 606 | var urlRegexs = []; 607 | // Access all of the values in the 'domains', then reload the matching tabs 608 | getDomains(function (status, msg, response) { 609 | $.each(response, function (index, domain) { 610 | urlRegexs.push(matchRegexToJsRegex(domain.name)); 611 | }); 612 | 613 | chrome.tabs.query({windowId: chrome.windows.WINDOW_ID_CURRENT}, function (tabs) { 614 | $.each(tabs, function (tabIndex, tab) { 615 | $.each(urlRegexs, function (regexIndex, regex) { 616 | // So we don't infinitely reload the chrome://extensions page, reloading JTI, reloading... 617 | var chromeRegex = new RegExp('chrome://extensions'); 618 | if (regex.test(tab.url) && (!chromeRegex.test(tab.url))) { 619 | chrome.tabs.reload(tab.id); 620 | } 621 | }); 622 | }); 623 | }); 624 | }); 625 | } 626 | 627 | // Listen for when extension is installed or updated 628 | chrome.runtime.onInstalled.addListener( 629 | function (details) { 630 | if (details.reason === 'update') { 631 | migrateTemplateKeys(); 632 | } 633 | 634 | if (details.reason === 'install' || details.reason === 'update') { 635 | reloadMatchingTabs(); 636 | } 637 | } 638 | ); 639 | 640 | // Listen for Messages. 641 | chrome.runtime.onMessage.addListener( 642 | function (request, sender, sendResponse) { 643 | switch (request.JDTIfunction) { 644 | case 'fetchDefault': 645 | fetchDefaultTemplates(function (status, message = null, data = null) { 646 | var response = responseMessage(status, message, data); 647 | sendResponse(response); 648 | }); 649 | break; 650 | case 'reset': 651 | setDefaultTemplates(function (status, message = null, data = null) { 652 | var response = responseMessage(status, message, data); 653 | sendResponse(response); 654 | }); 655 | break; 656 | case 'upload': 657 | loadLocalFile(request.fileContents, function (status, message = null, data = null) { 658 | var response = responseMessage(status, message, data); 659 | sendResponse(response); 660 | }); 661 | break; 662 | case 'download': 663 | downloadJSONData(request.url, function (status, message = null, data = null) { 664 | var response = responseMessage(status, message, data); 665 | sendResponse(response); 666 | }); 667 | break; 668 | case 'clear': 669 | clearTemplates(function (status, message = null, data = null) { 670 | var response = responseMessage(status, message, data); 671 | sendResponse(response); 672 | }); 673 | break; 674 | case 'delete': 675 | removeTemplate(request.templateID, function (status, message = null, data = null) { 676 | var response = responseMessage(status, message, data); 677 | sendResponse(response); 678 | }); 679 | break; 680 | case 'save': 681 | updateTemplate(request.templateID, request.templateName, request.templateIssueType, request.templateProjects, request.templateText, function (status, message = null, data = null) { 682 | var response = responseMessage(status, message, data); 683 | sendResponse(response); 684 | }); 685 | break; 686 | case 'add': 687 | addTemplate(request.templateName, request.issueTypeField, request.projectsField, request.text, function (status, message = null, data = null) { 688 | var response = responseMessage(status, message, data); 689 | sendResponse(response); 690 | }); 691 | break; 692 | case 'getData': 693 | getData(function (status, message = null, data = null) { 694 | var response = responseMessage(status, message, data); 695 | sendResponse(response); 696 | }); 697 | break; 698 | case 'setToggleStatus': 699 | setToggleStatus(request.toggleType, request.toggleInput, function (status, message = null, data = null) { 700 | var response = responseMessage(status, message, data); 701 | sendResponse(response); 702 | }); 703 | break; 704 | case 'getToggleStatus': 705 | getToggleStatus(request.toggleType, function (status, message = null, data = null) { 706 | var response = responseMessage(status, message, data); 707 | sendResponse(response); 708 | }); 709 | break; 710 | case 'addDomain': 711 | addDomain(request.domainName, function (status, message = null, data = null) { 712 | var response = responseMessage(status, message, data); 713 | sendResponse(response); 714 | }); 715 | break; 716 | case 'addInputID': 717 | addInputID(request.IDName, function (status, message = null, data = null) { 718 | var response = responseMessage(status, message, data); 719 | sendResponse(response); 720 | }); 721 | break; 722 | case 'removeDomain': 723 | removeDomain(request.domainID, request.removeAll, function (status, message = null, data = null) { 724 | var response = responseMessage(status, message, data); 725 | sendResponse(response); 726 | }); 727 | break; 728 | case 'removeInputID': 729 | removeInputID(request.inputID, request.removeAll, function (status, message = null, data = null) { 730 | var response = responseMessage(status, message, data); 731 | sendResponse(response); 732 | }); 733 | break; 734 | case 'getDomains': 735 | getDomains(function (status, message = null, data = null) { 736 | var response = responseMessage(status, message, data); 737 | sendResponse(response); 738 | }); 739 | break; 740 | case 'getInputIDs': 741 | getInputIDs(function (status, message = null, data = null) { 742 | var response = responseMessage(status, message, data); 743 | sendResponse(response); 744 | }); 745 | break; 746 | default: 747 | sendResponse({status: 'error', message: 'Invalid Action'}); 748 | } 749 | return true; 750 | } 751 | ); 752 | -------------------------------------------------------------------------------- /src/jti_content.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2016 Redbrick Technologies, Inc. */ 2 | /* https://github.com/rdbrck/jira-description-extension/blob/master/LICENSE */ 3 | 4 | /* global chrome, browser */ 5 | 6 | var StorageID = 'Jira-Template-Injector'; 7 | var inputIDs = []; 8 | 9 | var browserType = 'Chrome'; // eslint-disable-line no-unused-vars 10 | if (navigator.userAgent.indexOf('Firefox') !== -1 || navigator.userAgent.indexOf('Edge') !== -1) { 11 | chrome = browser; // eslint-disable-line no-native-reassign 12 | chrome.storage.sync = browser.storage.local; 13 | if (navigator.userAgent.indexOf('Firefox') !== -1) { 14 | browserType = 'Firefox'; 15 | } 16 | if (navigator.userAgent.indexOf('Edge') !== -1) { 17 | browserType = 'Edge'; 18 | } 19 | } 20 | 21 | // Handle tag selection. 22 | chrome.runtime.sendMessage({JDTIfunction: 'getInputIDs'}, function (response) { 23 | $.each(response.data, function (index, inputID) { 24 | inputIDs.push(inputID.name); 25 | }); 26 | 27 | $(document).on('click', `#${inputIDs.join(', #')}`, function (inputID) { 28 | var text = $(this).val(), 29 | ctrlDown = false, 30 | backtickKey = 192, 31 | ctrlKey = 17, 32 | cursorStart = $(this).prop('selectionStart'), 33 | cursorFinish = $(this).prop('selectionEnd'), 34 | end = (text.length - 5), 35 | selectStart = null, 36 | selectEnd = null, 37 | i = 0; 38 | 39 | // Only proceed if this is a click. i.e. not a highlight. 40 | if (cursorStart === cursorFinish) { 41 | // Look for opening tag ''. 42 | for (i = cursorStart; i >= 4; i--) { 43 | if (i !== 4) { 44 | if (text.slice((i - 5), i) === '') { 45 | // Found closing tag before opening tag -> We are not withing any valid tags. 46 | break; 47 | } 48 | } 49 | if (text.slice((i - 4), i) === '') { 50 | // Found opening Tag! 51 | selectStart = (i - 4); 52 | break; 53 | } 54 | } 55 | 56 | if (selectStart) { 57 | // Look for closing tag '' 58 | for (i = cursorStart; i <= end; i++) { 59 | if (text.slice(i, (i + 4)) === '') { 60 | // Found another opening bracket before closing bracket. Exit search. 61 | break; 62 | } 63 | if (text.slice(i, (i + 5)) === '') { 64 | // Found closing Tag! 65 | selectEnd = (i + 5); 66 | break; 67 | } 68 | } 69 | if (selectEnd) { 70 | // Select all the text between the two tags. 71 | $(this)[0].setSelectionRange(selectStart, selectEnd); 72 | cursorStart = cursorFinish = selectStart; // Set the cursor position to the select start point. This will ensure we find the next tag when using keyboard shortcut 73 | } else { // This only happens when user clicks on the the closing tag. Set selectStart to null so that it wont break the keyborad functionality 74 | selectStart = null; 75 | } 76 | } 77 | } 78 | 79 | // Detect ctrl or cmd pressed 80 | $(`#${inputID.currentTarget.id}`).keydown(function (e) { 81 | if (e.keyCode === ctrlKey) ctrlDown = true; 82 | }).keyup(function (e) { 83 | if (e.keyCode === ctrlKey) ctrlDown = false; 84 | }); 85 | 86 | // Keypress listener 87 | $(`#${inputID.currentTarget.id}`).keydown(function (e) { 88 | if (ctrlDown && (e.keyCode === backtickKey)) { // If ctrl is pressed 89 | let {start: tagStartIndex, end: tagEndIndex} = getAllIndexes($(this).val()); // Find all and tags in selected template. 90 | if (tagStartIndex.length !== 0 && tagEndIndex.length !== 0) { // Works only if the selected template contains any tag 91 | if (selectStart === null && selectEnd === null) { // Start from first 92 | var startPos = selectNextSelectionRange($(this)[0], cursorStart, tagStartIndex, tagEndIndex); 93 | selectStart = startPos.start; // Set Start Index 94 | selectEnd = startPos.end; // Set End Index 95 | } else { // Select next set 96 | if (tagStartIndex.indexOf(selectStart) === tagStartIndex.length - 1 && tagEndIndex.indexOf(selectEnd) === tagEndIndex.length - 1) { // Currently selecting the last set of , back to first set 97 | $(this)[0].setSelectionRange(tagStartIndex[0], tagEndIndex[0]); 98 | selectStart = tagStartIndex[0]; 99 | selectEnd = tagEndIndex[0]; 100 | } else { 101 | if (tagStartIndex.indexOf(selectStart) === -1 && tagEndIndex.indexOf(selectEnd) === -1) { // Highlighted tag is modified by user. Now we need search for the next . 102 | if (cursorStart < selectStart) cursorStart = selectStart; 103 | startPos = selectNextSelectionRange($(this)[0], cursorStart, tagStartIndex, tagEndIndex); 104 | selectStart = startPos.start; // Set Start Index 105 | selectEnd = startPos.end; // Set End Index 106 | } else { 107 | $(this)[0].setSelectionRange(tagStartIndex[tagStartIndex.indexOf(selectStart) + 1], tagEndIndex[tagEndIndex.indexOf(selectEnd) + 1]); // Find next set of 108 | selectStart = tagStartIndex[tagStartIndex.indexOf(selectStart) + 1]; 109 | selectEnd = tagEndIndex[tagEndIndex.indexOf(selectEnd) + 1]; 110 | } 111 | } 112 | } 113 | cursorStart = cursorFinish = selectStart; // Set the cursor position to the select start point. This will ensure we find the next tag when using keyboard shortcut 114 | } 115 | } 116 | }); 117 | }); 118 | }); 119 | 120 | function selectNextSelectionRange (selector, cursorStart, tagStartIndex, tagEndIndex) { 121 | var startPos = FindNextTI(cursorStart, tagStartIndex, tagEndIndex); // Find the starting tag 122 | selector.setSelectionRange(startPos.start, startPos.end); 123 | return startPos; 124 | } 125 | 126 | // Helper method. Find next based on cursor position 127 | function FindNextTI (CursorPos, tagStart, tagEnd) { 128 | for (var i = 0; i < tagStart.length; i++) { 129 | if (tagStart[i] >= CursorPos) { 130 | return { start: tagStart[i], end: tagEnd[i] }; 131 | } 132 | } 133 | return { start: tagStart[0], end: tagEnd[0] }; 134 | } 135 | 136 | // Helper method. Find index(start and end) of all occurrences of a given substring in a string 137 | function getAllIndexes (str) { 138 | var startIndexes = [], 139 | endIndexes = [], 140 | re = //g, // Start 141 | match = re.exec(str); 142 | while (match) { 143 | startIndexes.push(match.index); 144 | match = re.exec(str); 145 | } 146 | 147 | re = /<\/TI>/g; // End 148 | match = re.exec(str); 149 | while (match) { 150 | endIndexes.push(match.index + 5); 151 | match = re.exec(str); 152 | } 153 | return { start: startIndexes, end: endIndexes }; 154 | } 155 | 156 | function isDefaultDescription (value, callback) { 157 | chrome.storage.sync.get(StorageID, function (templates) { 158 | templates = templates[StorageID].templates; 159 | var match = false; 160 | 161 | // Check if it's empty. 162 | if (value === '') { 163 | match = true; 164 | } 165 | 166 | // Check if we've already loaded a template. 167 | if (!match) { 168 | $.each(templates, function (key, template) { 169 | if (value === template.text) { 170 | match = true; 171 | return false; 172 | } 173 | }); 174 | } 175 | 176 | callback(match); 177 | }); 178 | } 179 | 180 | // Given the project name as formatted in JIRA's dropdown "PROJECT (KEY)", parse out the key 181 | function parseProjectKey (projectElement) { 182 | var project = projectElement.val(); 183 | return project.substring(project.lastIndexOf('(') + 1, project.length - 1); 184 | } 185 | 186 | function injectDescriptionTemplate (descriptionElement) { 187 | // Each issue type for each project can have its own template. 188 | chrome.storage.sync.get(StorageID, function (templates) { 189 | templates = templates[StorageID].templates; 190 | 191 | var templateText = '', 192 | projectElement = $('#project-field'), 193 | issueTypeElement = $('#issuetype-field'); 194 | 195 | if (issueTypeElement !== null && projectElement !== null) { 196 | var projectKey = parseProjectKey(projectElement); 197 | var override = 0; 198 | 199 | $.each(templates, function (key, template) { 200 | // Default template (no issue type, no project) 201 | if (!template['issuetype-field'] && !template['projects-field']) { 202 | if (override < 1) { 203 | override = 1; 204 | templateText = template.text; 205 | } 206 | // Override if project, no issue type 207 | } else if (!template['issuetype-field'] && $.inArray(projectKey, utils.parseProjects(template['projects-field'])) !== -1) { 208 | if (override < 2) { 209 | override = 2; 210 | templateText = template.text; 211 | } 212 | // Override if issue type, no project 213 | } else if (!template['projects-field'] && template['issuetype-field'] === issueTypeElement.val()) { 214 | if (override < 3) { 215 | override = 3; 216 | templateText = template.text; 217 | } 218 | // Override if issue type and project 219 | } else if (template['issuetype-field'] === issueTypeElement.val() && 220 | $.inArray(projectKey, utils.parseProjects(template['projects-field'])) !== -1) { 221 | templateText = template.text; 222 | return false; 223 | } 224 | }); 225 | 226 | descriptionElement.value = templateText; 227 | } else { 228 | if (issueTypeElement === null) { 229 | console.error('*** Error: Element Id "issuetype-field" not found.'); 230 | } else if (projectElement === null) { 231 | console.error('*** Error: Element Id "project-field" not found.'); 232 | } 233 | } 234 | }); 235 | } 236 | 237 | function descriptionChangeEvent (changeEvent) { 238 | // The description field has been changed, turn the dirtyDialogMessage back on and remove the listener. 239 | changeEvent.target.className = changeEvent.target.className.replace(' ajs-dirty-warning-exempt', ''); 240 | changeEvent.target.removeEventListener('change', descriptionChangeEvent); 241 | } 242 | 243 | function observeDocumentBody (mutation) { 244 | if (document.getElementById('create-issue-dialog') !== null || document.getElementById('create-subtask-dialog') !== null) { // Only interested in document changes related to Create Issue Dialog box or Create Sub-task Dialog box. 245 | if (inputIDs.includes(mutation.target.id)) { // Only interested in select input id fields. 246 | var descriptionElement = mutation.target; 247 | isDefaultDescription(descriptionElement.value, function (result) { 248 | if (result) { // Only inject if description field has not been modified by the user. 249 | injectDescriptionTemplate(descriptionElement); 250 | if (descriptionElement.className.indexOf('ajs-dirty-warning-exempt') === -1) { // Default template injection should not pop up dirtyDialogMessage. 251 | descriptionElement.className += ' ajs-dirty-warning-exempt'; 252 | descriptionElement.addEventListener('change', descriptionChangeEvent); 253 | } 254 | } 255 | }); 256 | } 257 | } 258 | } 259 | 260 | // Create observer to monitor for description field if the domain is a monitored one 261 | chrome.runtime.sendMessage({JDTIfunction: 'getDomains'}, function (response) { 262 | $.each(response.data, function (index, domain) { 263 | var pattern = new RegExp(domain.name); 264 | if (pattern.test(window.location.href)) { 265 | var observer = new MutationObserver(function (mutations) { 266 | mutations.forEach(observeDocumentBody); 267 | }); 268 | observer.observe(document.body, { subtree: true, attributes: true, attributeFilter: ['resolved'] }); 269 | } 270 | }); 271 | }); 272 | -------------------------------------------------------------------------------- /src/lib/filesaver/FileSaver.js: -------------------------------------------------------------------------------- 1 | /* FileSaver.js 2 | * A saveAs() FileSaver implementation. 3 | * 1.3.7 4 | * 2018-03-16 14:39:40 5 | * 6 | * By Eli Grey, https://eligrey.com 7 | * License: MIT 8 | * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md 9 | */ 10 | 11 | /*global self */ 12 | /*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */ 13 | 14 | /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/src/FileSaver.js */ 15 | 16 | var saveAs = saveAs || (function(view) { 17 | "use strict"; 18 | // IE <10 is explicitly unsupported 19 | if (typeof view === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) { 20 | return; 21 | } 22 | var 23 | doc = view.document 24 | // only get URL when necessary in case Blob.js hasn't overridden it yet 25 | , get_URL = function() { 26 | return view.URL || view.webkitURL || view; 27 | } 28 | , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") 29 | , can_use_save_link = "download" in save_link 30 | , click = function(node) { 31 | var event = new MouseEvent("click"); 32 | node.dispatchEvent(event); 33 | } 34 | , is_safari = /constructor/i.test(view.HTMLElement) || view.safari 35 | , is_chrome_ios =/CriOS\/[\d]+/.test(navigator.userAgent) 36 | , setImmediate = view.setImmediate || view.setTimeout 37 | , throw_outside = function(ex) { 38 | setImmediate(function() { 39 | throw ex; 40 | }, 0); 41 | } 42 | , force_saveable_type = "application/octet-stream" 43 | // the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to 44 | , arbitrary_revoke_timeout = 1000 * 40 // in ms 45 | , revoke = function(file) { 46 | var revoker = function() { 47 | if (typeof file === "string") { // file is an object URL 48 | get_URL().revokeObjectURL(file); 49 | } else { // file is a File 50 | file.remove(); 51 | } 52 | }; 53 | setTimeout(revoker, arbitrary_revoke_timeout); 54 | } 55 | , dispatch = function(filesaver, event_types, event) { 56 | event_types = [].concat(event_types); 57 | var i = event_types.length; 58 | while (i--) { 59 | var listener = filesaver["on" + event_types[i]]; 60 | if (typeof listener === "function") { 61 | try { 62 | listener.call(filesaver, event || filesaver); 63 | } catch (ex) { 64 | throw_outside(ex); 65 | } 66 | } 67 | } 68 | } 69 | , auto_bom = function(blob) { 70 | // prepend BOM for UTF-8 XML and text/* types (including HTML) 71 | // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF 72 | if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { 73 | return new Blob([String.fromCharCode(0xFEFF), blob], {type: blob.type}); 74 | } 75 | return blob; 76 | } 77 | , FileSaver = function(blob, name, no_auto_bom) { 78 | if (!no_auto_bom) { 79 | blob = auto_bom(blob); 80 | } 81 | // First try a.download, then web filesystem, then object URLs 82 | var 83 | filesaver = this 84 | , type = blob.type 85 | , force = type === force_saveable_type 86 | , object_url 87 | , dispatch_all = function() { 88 | dispatch(filesaver, "writestart progress write writeend".split(" ")); 89 | } 90 | // on any filesys errors revert to saving with object URLs 91 | , fs_error = function() { 92 | if ((is_chrome_ios || (force && is_safari)) && view.FileReader) { 93 | // Safari doesn't allow downloading of blob urls 94 | var reader = new FileReader(); 95 | reader.onloadend = function() { 96 | var url = is_chrome_ios ? reader.result : reader.result.replace(/^data:[^;]*;/, 'data:attachment/file;'); 97 | var popup = view.open(url, '_blank'); 98 | if(!popup) view.location.href = url; 99 | url=undefined; // release reference before dispatching 100 | filesaver.readyState = filesaver.DONE; 101 | dispatch_all(); 102 | }; 103 | reader.readAsDataURL(blob); 104 | filesaver.readyState = filesaver.INIT; 105 | return; 106 | } 107 | // don't create more object URLs than needed 108 | if (!object_url) { 109 | object_url = get_URL().createObjectURL(blob); 110 | } 111 | if (force) { 112 | view.location.href = object_url; 113 | } else { 114 | var opened = view.open(object_url, "_blank"); 115 | if (!opened) { 116 | // Apple does not allow window.open, see https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/WorkingwithWindowsandTabs/WorkingwithWindowsandTabs.html 117 | view.location.href = object_url; 118 | } 119 | } 120 | filesaver.readyState = filesaver.DONE; 121 | dispatch_all(); 122 | revoke(object_url); 123 | } 124 | ; 125 | filesaver.readyState = filesaver.INIT; 126 | 127 | if (can_use_save_link) { 128 | object_url = get_URL().createObjectURL(blob); 129 | setImmediate(function() { 130 | save_link.href = object_url; 131 | save_link.download = name; 132 | click(save_link); 133 | dispatch_all(); 134 | revoke(object_url); 135 | filesaver.readyState = filesaver.DONE; 136 | }, 0); 137 | return; 138 | } 139 | 140 | fs_error(); 141 | } 142 | , FS_proto = FileSaver.prototype 143 | , saveAs = function(blob, name, no_auto_bom) { 144 | return new FileSaver(blob, name || blob.name || "download", no_auto_bom); 145 | } 146 | ; 147 | 148 | // IE 10+ (native saveAs) 149 | if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) { 150 | return function(blob, name, no_auto_bom) { 151 | name = name || blob.name || "download"; 152 | 153 | if (!no_auto_bom) { 154 | blob = auto_bom(blob); 155 | } 156 | return navigator.msSaveOrOpenBlob(blob, name); 157 | }; 158 | } 159 | 160 | save_link.target = "_blank"; 161 | 162 | FS_proto.abort = function(){}; 163 | FS_proto.readyState = FS_proto.INIT = 0; 164 | FS_proto.WRITING = 1; 165 | FS_proto.DONE = 2; 166 | 167 | FS_proto.error = 168 | FS_proto.onwritestart = 169 | FS_proto.onprogress = 170 | FS_proto.onwrite = 171 | FS_proto.onabort = 172 | FS_proto.onerror = 173 | FS_proto.onwriteend = 174 | null; 175 | 176 | return saveAs; 177 | }( 178 | typeof self !== "undefined" && self 179 | || typeof window !== "undefined" && window 180 | || this 181 | )); -------------------------------------------------------------------------------- /src/lib/handlebars/handlebars-1.0.0.beta.6.js: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2011 by Yehuda Katz 2 | // Licensing details in LICENSE.handlebars 3 | 4 | // lib/handlebars/base.js 5 | var Handlebars = {}; 6 | 7 | Handlebars.VERSION = "1.0.beta.6"; 8 | 9 | Handlebars.helpers = {}; 10 | Handlebars.partials = {}; 11 | 12 | Handlebars.registerHelper = function(name, fn, inverse) { 13 | if(inverse) { fn.not = inverse; } 14 | this.helpers[name] = fn; 15 | }; 16 | 17 | Handlebars.registerPartial = function(name, str) { 18 | this.partials[name] = str; 19 | }; 20 | 21 | Handlebars.registerHelper('helperMissing', function(arg) { 22 | if(arguments.length === 2) { 23 | return undefined; 24 | } else { 25 | throw new Error("Could not find property '" + arg + "'"); 26 | } 27 | }); 28 | 29 | var toString = Object.prototype.toString, functionType = "[object Function]"; 30 | 31 | Handlebars.registerHelper('blockHelperMissing', function(context, options) { 32 | var inverse = options.inverse || function() {}, fn = options.fn; 33 | 34 | 35 | var ret = ""; 36 | var type = toString.call(context); 37 | 38 | if(type === functionType) { context = context.call(this); } 39 | 40 | if(context === true) { 41 | return fn(this); 42 | } else if(context === false || context == null) { 43 | return inverse(this); 44 | } else if(type === "[object Array]") { 45 | if(context.length > 0) { 46 | for(var i=0, j=context.length; i 0) { 63 | for(var i=0, j=context.length; i 2) { 238 | expected.push("'" + this.terminals_[p] + "'"); 239 | } 240 | var errStr = ""; 241 | if (this.lexer.showPosition) { 242 | errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + this.terminals_[symbol] + "'"; 243 | } else { 244 | errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'"); 245 | } 246 | this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected}); 247 | } 248 | } 249 | if (action[0] instanceof Array && action.length > 1) { 250 | throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol); 251 | } 252 | switch (action[0]) { 253 | case 1: 254 | stack.push(symbol); 255 | vstack.push(this.lexer.yytext); 256 | lstack.push(this.lexer.yylloc); 257 | stack.push(action[1]); 258 | symbol = null; 259 | if (!preErrorSymbol) { 260 | yyleng = this.lexer.yyleng; 261 | yytext = this.lexer.yytext; 262 | yylineno = this.lexer.yylineno; 263 | yyloc = this.lexer.yylloc; 264 | if (recovering > 0) 265 | recovering--; 266 | } else { 267 | symbol = preErrorSymbol; 268 | preErrorSymbol = null; 269 | } 270 | break; 271 | case 2: 272 | len = this.productions_[action[1]][1]; 273 | yyval.$ = vstack[vstack.length - len]; 274 | yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column}; 275 | r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); 276 | if (typeof r !== "undefined") { 277 | return r; 278 | } 279 | if (len) { 280 | stack = stack.slice(0, -1 * len * 2); 281 | vstack = vstack.slice(0, -1 * len); 282 | lstack = lstack.slice(0, -1 * len); 283 | } 284 | stack.push(this.productions_[action[1]][0]); 285 | vstack.push(yyval.$); 286 | lstack.push(yyval._$); 287 | newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; 288 | stack.push(newState); 289 | break; 290 | case 3: 291 | return true; 292 | } 293 | } 294 | return true; 295 | } 296 | };/* Jison generated lexer */ 297 | var lexer = (function(){ 298 | 299 | var lexer = ({EOF:1, 300 | parseError:function parseError(str, hash) { 301 | if (this.yy.parseError) { 302 | this.yy.parseError(str, hash); 303 | } else { 304 | throw new Error(str); 305 | } 306 | }, 307 | setInput:function (input) { 308 | this._input = input; 309 | this._more = this._less = this.done = false; 310 | this.yylineno = this.yyleng = 0; 311 | this.yytext = this.matched = this.match = ''; 312 | this.conditionStack = ['INITIAL']; 313 | this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0}; 314 | return this; 315 | }, 316 | input:function () { 317 | var ch = this._input[0]; 318 | this.yytext+=ch; 319 | this.yyleng++; 320 | this.match+=ch; 321 | this.matched+=ch; 322 | var lines = ch.match(/\n/); 323 | if (lines) this.yylineno++; 324 | this._input = this._input.slice(1); 325 | return ch; 326 | }, 327 | unput:function (ch) { 328 | this._input = ch + this._input; 329 | return this; 330 | }, 331 | more:function () { 332 | this._more = true; 333 | return this; 334 | }, 335 | pastInput:function () { 336 | var past = this.matched.substr(0, this.matched.length - this.match.length); 337 | return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); 338 | }, 339 | upcomingInput:function () { 340 | var next = this.match; 341 | if (next.length < 20) { 342 | next += this._input.substr(0, 20-next.length); 343 | } 344 | return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, ""); 345 | }, 346 | showPosition:function () { 347 | var pre = this.pastInput(); 348 | var c = new Array(pre.length + 1).join("-"); 349 | return pre + this.upcomingInput() + "\n" + c+"^"; 350 | }, 351 | next:function () { 352 | if (this.done) { 353 | return this.EOF; 354 | } 355 | if (!this._input) this.done = true; 356 | 357 | var token, 358 | match, 359 | col, 360 | lines; 361 | if (!this._more) { 362 | this.yytext = ''; 363 | this.match = ''; 364 | } 365 | var rules = this._currentRules(); 366 | for (var i=0;i < rules.length; i++) { 367 | match = this._input.match(this.rules[rules[i]]); 368 | if (match) { 369 | lines = match[0].match(/\n.*/g); 370 | if (lines) this.yylineno += lines.length; 371 | this.yylloc = {first_line: this.yylloc.last_line, 372 | last_line: this.yylineno+1, 373 | first_column: this.yylloc.last_column, 374 | last_column: lines ? lines[lines.length-1].length-1 : this.yylloc.last_column + match[0].length} 375 | this.yytext += match[0]; 376 | this.match += match[0]; 377 | this.matches = match; 378 | this.yyleng = this.yytext.length; 379 | this._more = false; 380 | this._input = this._input.slice(match[0].length); 381 | this.matched += match[0]; 382 | token = this.performAction.call(this, this.yy, this, rules[i],this.conditionStack[this.conditionStack.length-1]); 383 | if (token) return token; 384 | else return; 385 | } 386 | } 387 | if (this._input === "") { 388 | return this.EOF; 389 | } else { 390 | this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), 391 | {text: "", token: null, line: this.yylineno}); 392 | } 393 | }, 394 | lex:function lex() { 395 | var r = this.next(); 396 | if (typeof r !== 'undefined') { 397 | return r; 398 | } else { 399 | return this.lex(); 400 | } 401 | }, 402 | begin:function begin(condition) { 403 | this.conditionStack.push(condition); 404 | }, 405 | popState:function popState() { 406 | return this.conditionStack.pop(); 407 | }, 408 | _currentRules:function _currentRules() { 409 | return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules; 410 | }, 411 | topState:function () { 412 | return this.conditionStack[this.conditionStack.length-2]; 413 | }, 414 | pushState:function begin(condition) { 415 | this.begin(condition); 416 | }}); 417 | lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { 418 | 419 | var YYSTATE=YY_START 420 | switch($avoiding_name_collisions) { 421 | case 0: 422 | if(yy_.yytext.slice(-1) !== "\\") this.begin("mu"); 423 | if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1), this.begin("emu"); 424 | if(yy_.yytext) return 14; 425 | 426 | break; 427 | case 1: return 14; 428 | break; 429 | case 2: this.popState(); return 14; 430 | break; 431 | case 3: return 24; 432 | break; 433 | case 4: return 16; 434 | break; 435 | case 5: return 20; 436 | break; 437 | case 6: return 19; 438 | break; 439 | case 7: return 19; 440 | break; 441 | case 8: return 23; 442 | break; 443 | case 9: return 23; 444 | break; 445 | case 10: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return 15; 446 | break; 447 | case 11: return 22; 448 | break; 449 | case 12: return 34; 450 | break; 451 | case 13: return 33; 452 | break; 453 | case 14: return 33; 454 | break; 455 | case 15: return 36; 456 | break; 457 | case 16: /*ignore whitespace*/ 458 | break; 459 | case 17: this.popState(); return 18; 460 | break; 461 | case 18: this.popState(); return 18; 462 | break; 463 | case 19: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 28; 464 | break; 465 | case 20: return 30; 466 | break; 467 | case 21: return 30; 468 | break; 469 | case 22: return 29; 470 | break; 471 | case 23: return 33; 472 | break; 473 | case 24: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 33; 474 | break; 475 | case 25: return 'INVALID'; 476 | break; 477 | case 26: return 5; 478 | break; 479 | } 480 | }; 481 | lexer.rules = [/^[^\x00]*?(?=(\{\{))/,/^[^\x00]+/,/^[^\x00]{2,}?(?=(\{\{))/,/^\{\{>/,/^\{\{#/,/^\{\{\//,/^\{\{\^/,/^\{\{\s*else\b/,/^\{\{\{/,/^\{\{&/,/^\{\{![\s\S]*?\}\}/,/^\{\{/,/^=/,/^\.(?=[} ])/,/^\.\./,/^[\/.]/,/^\s+/,/^\}\}\}/,/^\}\}/,/^"(\\["]|[^"])*"/,/^true(?=[}\s])/,/^false(?=[}\s])/,/^[0-9]+(?=[}\s])/,/^[a-zA-Z0-9_$-]+(?=[=}\s\/.])/,/^\[[^\]]*\]/,/^./,/^$/]; 482 | lexer.conditions = {"mu":{"rules":[3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26],"inclusive":false},"emu":{"rules":[2],"inclusive":false},"INITIAL":{"rules":[0,1,26],"inclusive":true}};return lexer;})() 483 | parser.lexer = lexer; 484 | return parser; 485 | })(); 486 | if (typeof require !== 'undefined' && typeof exports !== 'undefined') { 487 | exports.parser = handlebars; 488 | exports.parse = function () { return handlebars.parse.apply(handlebars, arguments); } 489 | exports.main = function commonjsMain(args) { 490 | if (!args[1]) 491 | throw new Error('Usage: '+args[0]+' FILE'); 492 | if (typeof process !== 'undefined') { 493 | var source = require('fs').readFileSync(require('path').join(process.cwd(), args[1]), "utf8"); 494 | } else { 495 | var cwd = require("file").path(require("file").cwd()); 496 | var source = cwd.join(args[1]).read({charset: "utf-8"}); 497 | } 498 | return exports.parser.parse(source); 499 | } 500 | if (typeof module !== 'undefined' && require.main === module) { 501 | exports.main(typeof process !== 'undefined' ? process.argv.slice(1) : require("system").args); 502 | } 503 | }; 504 | ; 505 | // lib/handlebars/compiler/base.js 506 | Handlebars.Parser = handlebars; 507 | 508 | Handlebars.parse = function(string) { 509 | Handlebars.Parser.yy = Handlebars.AST; 510 | return Handlebars.Parser.parse(string); 511 | }; 512 | 513 | Handlebars.print = function(ast) { 514 | return new Handlebars.PrintVisitor().accept(ast); 515 | }; 516 | 517 | Handlebars.logger = { 518 | DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3, 519 | 520 | // override in the host environment 521 | log: function(level, str) {} 522 | }; 523 | 524 | Handlebars.log = function(level, str) { Handlebars.logger.log(level, str); }; 525 | ; 526 | // lib/handlebars/compiler/ast.js 527 | (function() { 528 | 529 | Handlebars.AST = {}; 530 | 531 | Handlebars.AST.ProgramNode = function(statements, inverse) { 532 | this.type = "program"; 533 | this.statements = statements; 534 | if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); } 535 | }; 536 | 537 | Handlebars.AST.MustacheNode = function(params, hash, unescaped) { 538 | this.type = "mustache"; 539 | this.id = params[0]; 540 | this.params = params.slice(1); 541 | this.hash = hash; 542 | this.escaped = !unescaped; 543 | }; 544 | 545 | Handlebars.AST.PartialNode = function(id, context) { 546 | this.type = "partial"; 547 | 548 | // TODO: disallow complex IDs 549 | 550 | this.id = id; 551 | this.context = context; 552 | }; 553 | 554 | var verifyMatch = function(open, close) { 555 | if(open.original !== close.original) { 556 | throw new Handlebars.Exception(open.original + " doesn't match " + close.original); 557 | } 558 | }; 559 | 560 | Handlebars.AST.BlockNode = function(mustache, program, close) { 561 | verifyMatch(mustache.id, close); 562 | this.type = "block"; 563 | this.mustache = mustache; 564 | this.program = program; 565 | }; 566 | 567 | Handlebars.AST.InverseNode = function(mustache, program, close) { 568 | verifyMatch(mustache.id, close); 569 | this.type = "inverse"; 570 | this.mustache = mustache; 571 | this.program = program; 572 | }; 573 | 574 | Handlebars.AST.ContentNode = function(string) { 575 | this.type = "content"; 576 | this.string = string; 577 | }; 578 | 579 | Handlebars.AST.HashNode = function(pairs) { 580 | this.type = "hash"; 581 | this.pairs = pairs; 582 | }; 583 | 584 | Handlebars.AST.IdNode = function(parts) { 585 | this.type = "ID"; 586 | this.original = parts.join("."); 587 | 588 | var dig = [], depth = 0; 589 | 590 | for(var i=0,l=parts.length; i": ">", 649 | '"': """, 650 | "'": "'", 651 | "`": "`" 652 | }; 653 | 654 | var badChars = /&(?!\w+;)|[<>"'`]/g; 655 | var possible = /[&<>"'`]/; 656 | 657 | var escapeChar = function(chr) { 658 | return escape[chr] || "&"; 659 | }; 660 | 661 | Handlebars.Utils = { 662 | escapeExpression: function(string) { 663 | // don't escape SafeStrings, since they're already safe 664 | if (string instanceof Handlebars.SafeString) { 665 | return string.toString(); 666 | } else if (string == null || string === false) { 667 | return ""; 668 | } 669 | 670 | if(!possible.test(string)) { return string; } 671 | return string.replace(badChars, escapeChar); 672 | }, 673 | 674 | isEmpty: function(value) { 675 | if (typeof value === "undefined") { 676 | return true; 677 | } else if (value === null) { 678 | return true; 679 | } else if (value === false) { 680 | return true; 681 | } else if(Object.prototype.toString.call(value) === "[object Array]" && value.length === 0) { 682 | return true; 683 | } else { 684 | return false; 685 | } 686 | } 687 | }; 688 | })();; 689 | // lib/handlebars/compiler/compiler.js 690 | Handlebars.Compiler = function() {}; 691 | Handlebars.JavaScriptCompiler = function() {}; 692 | 693 | (function(Compiler, JavaScriptCompiler) { 694 | Compiler.OPCODE_MAP = { 695 | appendContent: 1, 696 | getContext: 2, 697 | lookupWithHelpers: 3, 698 | lookup: 4, 699 | append: 5, 700 | invokeMustache: 6, 701 | appendEscaped: 7, 702 | pushString: 8, 703 | truthyOrFallback: 9, 704 | functionOrFallback: 10, 705 | invokeProgram: 11, 706 | invokePartial: 12, 707 | push: 13, 708 | assignToHash: 15, 709 | pushStringParam: 16 710 | }; 711 | 712 | Compiler.MULTI_PARAM_OPCODES = { 713 | appendContent: 1, 714 | getContext: 1, 715 | lookupWithHelpers: 2, 716 | lookup: 1, 717 | invokeMustache: 3, 718 | pushString: 1, 719 | truthyOrFallback: 1, 720 | functionOrFallback: 1, 721 | invokeProgram: 3, 722 | invokePartial: 1, 723 | push: 1, 724 | assignToHash: 1, 725 | pushStringParam: 1 726 | }; 727 | 728 | Compiler.DISASSEMBLE_MAP = {}; 729 | 730 | for(var prop in Compiler.OPCODE_MAP) { 731 | var value = Compiler.OPCODE_MAP[prop]; 732 | Compiler.DISASSEMBLE_MAP[value] = prop; 733 | } 734 | 735 | Compiler.multiParamSize = function(code) { 736 | return Compiler.MULTI_PARAM_OPCODES[Compiler.DISASSEMBLE_MAP[code]]; 737 | }; 738 | 739 | Compiler.prototype = { 740 | compiler: Compiler, 741 | 742 | disassemble: function() { 743 | var opcodes = this.opcodes, opcode, nextCode; 744 | var out = [], str, name, value; 745 | 746 | for(var i=0, l=opcodes.length; i 0) { 1131 | this.source[1] = this.source[1] + ", " + locals.join(", "); 1132 | } 1133 | 1134 | // Generate minimizer alias mappings 1135 | if (!this.isChild) { 1136 | var aliases = [] 1137 | for (var alias in this.context.aliases) { 1138 | this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias]; 1139 | } 1140 | } 1141 | 1142 | if (this.source[1]) { 1143 | this.source[1] = "var " + this.source[1].substring(2) + ";"; 1144 | } 1145 | 1146 | // Merge children 1147 | if (!this.isChild) { 1148 | this.source[1] += '\n' + this.context.programs.join('\n') + '\n'; 1149 | } 1150 | 1151 | if (!this.environment.isSimple) { 1152 | this.source.push("return buffer;"); 1153 | } 1154 | 1155 | var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"]; 1156 | 1157 | for(var i=0, l=this.environment.depths.list.length; i this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } 1409 | return "stack" + this.stackSlot; 1410 | }, 1411 | 1412 | popStack: function() { 1413 | return "stack" + this.stackSlot--; 1414 | }, 1415 | 1416 | topStack: function() { 1417 | return "stack" + this.stackSlot; 1418 | }, 1419 | 1420 | quotedString: function(str) { 1421 | return '"' + str 1422 | .replace(/\\/g, '\\\\') 1423 | .replace(/"/g, '\\"') 1424 | .replace(/\n/g, '\\n') 1425 | .replace(/\r/g, '\\r') + '"'; 1426 | } 1427 | }; 1428 | 1429 | var reservedWords = ( 1430 | "break else new var" + 1431 | " case finally return void" + 1432 | " catch for switch while" + 1433 | " continue function this with" + 1434 | " default if throw" + 1435 | " delete in try" + 1436 | " do instanceof typeof" + 1437 | " abstract enum int short" + 1438 | " boolean export interface static" + 1439 | " byte extends long super" + 1440 | " char final native synchronized" + 1441 | " class float package throws" + 1442 | " const goto private transient" + 1443 | " debugger implements protected volatile" + 1444 | " double import public let yield" 1445 | ).split(" "); 1446 | 1447 | var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {}; 1448 | 1449 | for(var i=0, l=reservedWords.length; i"], 34 | "js": ["lib/jquery/jquery-2.2.3.js", "js/utils.js", "jti_content.js"], 35 | "run_at": "document_end" 36 | } 37 | ], 38 | "sandbox": { 39 | "pages": ["html/sandbox.html"] 40 | }, 41 | "permissions": [ 42 | "storage", 43 | "tabs", 44 | "*://*/*.json", 45 | "https://*.pingstatsnet.com/" 46 | ] 47 | } 48 | --------------------------------------------------------------------------------