├── .gitignore ├── LICENSE ├── README.md ├── gulpfile.js ├── package.json └── src ├── background.js ├── contentScript.js ├── editor.js ├── logo.png ├── manifest.json ├── options.css ├── options.html └── options.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | bin/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Monica Dinculescu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚨🚨🚨 This repo is unmaintained! 🚨🚨🚨 2 | Hai! Thanks for stopping by! Since GitHub shipped a similar feature for realsies, [Saved replies](https://help.github.com/articles/working-with-saved-replies/), there's no real point for me to maintain this extension. I've had requests from a couple of people not to nuke this repo since they'd still like to use this, so feel free to continue using it. Or not. It's really up to you. Here's a cactus :cactus: 3 | 4 | # github-canned-responses 5 | 6 | Sometimes you've made mistakes and you have to triage 700 GitHub issues and Pull Requests. A lot of them will need the same answer, and your wrists will start hurting from all the switch-to-edit-copy-pasting. This extension helps with that. 7 | 8 | ## How to get it 9 | [Install it from the Chrome Web Store](https://chrome.google.com/webstore/detail/github-canned-responses/lhehmppafakahahobaibfcomknkhoina) 10 | 11 | [Install it from the Firefox Add-on Store](https://addons.mozilla.org/en-US/firefox/addon/github-canned-responses/) 12 | 13 | ## What is it 14 | The extension adds a little button inside GitHub's 15 | comment editing view, that allows you to choose one of the existing canned responses, and insert it in the current comment. It looks like this, and it works on both Issues and PRs pages (but at the moment not the `files` view of a PR): 16 | 17 | animated gif of the extension in action 18 | 19 | ## Adding your own canned responses 20 | The extension comes with a few default canned responses, but you can edit them and add your own! In the dropdown, press the edit link, and you'll get something like this: 21 | 22 | screenshot of the answer editing page 23 | 24 | ## Happy triaging, friends! <3 25 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var jeditor = require('gulp-json-editor'); 5 | var merge = require('merge-stream'); 6 | var zip = require('gulp-zip'); 7 | 8 | var xpiName = 'github-canned-responses.xpi'; 9 | var zipName = 'github-canned-responses.zip'; 10 | 11 | gulp.task('default', function () { 12 | var files = ['src/*', '!src/manifest.json']; 13 | var manifest = gulp.src('src/manifest.json'); 14 | var dest = gulp.dest('bin/'); 15 | 16 | merge(gulp.src(files), manifest) 17 | .pipe(zip(zipName)) 18 | .pipe(dest); 19 | 20 | manifest = manifest.pipe(jeditor({ 21 | 'applications': { 22 | 'gecko': { 23 | 'id': 'github-canned-responses@example' 24 | } 25 | } 26 | })); 27 | 28 | merge(gulp.src(files), manifest) 29 | .pipe(zip(xpiName)) 30 | .pipe(dest); 31 | }); 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-canned-responses", 3 | "version": "1.0.8", 4 | "description": "Choose from a set of canned responses when commenting on your GitHub PRs or issues", 5 | "main": "background.js", 6 | "dependencies": { 7 | "gulp": "^3.9.0", 8 | "gulp-json-editor": "^2.2.1", 9 | "gulp-zip": "^3.1.0", 10 | "merge-stream": "^1.0.0" 11 | }, 12 | "devDependencies": {}, 13 | "scripts": { 14 | "build": "gulp" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/notwaldorf/github-canned-responses.git" 19 | }, 20 | "author": "Monica Dinculescu", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/notwaldorf/github-canned-responses/issues" 24 | }, 25 | "homepage": "https://github.com/notwaldorf/github-canned-responses#readme" 26 | } 27 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | var defaultAnswers = [ 2 | { 3 | name: 'Issue: thanks! help fix?', 4 | description: "Thanks a lot for filing this issue! Would you like to write a patch for this? We'd be more than happy to walk you through the steps involved." 5 | }, 6 | { 7 | name: 'Issue: thanks! looking!', 8 | description: "Thanks a lot for filing this issue! We'll triage and take a look at it as soon as possible!" 9 | }, 10 | { 11 | name: 'Issue: looks inactive', 12 | description: "This issue is fairly old and there hasn't been much activity on it. Closing, but please re-open if it still occurs." 13 | }, 14 | { 15 | name: 'Issue: closing, no repro steps', 16 | description: "This issue has no reproducible steps. Please re-open this issue if it still occurs, with a JSBin containing a set of reproducible steps. Check this element's CONTRIBUTING.md for an example." 17 | }, 18 | { 19 | name: 'Issue: provide repro steps', 20 | description: "Please provide a JSBin containing a set of reproducible steps. Check this element's CONTRIBUTING.md for an example." 21 | }, 22 | { 23 | name: 'Issue: cannot reproduce', 24 | description: "Not reproducible in the latest release. Please re-open this issue if it still occurs, with a JSBin containing a set of reproducible steps. Check this element's CONTRIBUTING.md for an example." 25 | }, 26 | { 27 | name: 'PR: thanks! looking!', 28 | description: "Thanks for your contribution! We'll triage and take a look at it as soon as possible!" 29 | }, 30 | { 31 | name: 'PR: needs test', 32 | description: "Please add a test case that tests the problem this PR is fixing." 33 | } 34 | ]; 35 | 36 | function getAnswersListFromStorage() { 37 | // Load the answers from local storage. 38 | var localStorageKey = '__GH_CANNED_ANSWERS__EXT__'; 39 | var saved = localStorage.getItem(localStorageKey); 40 | var answers; 41 | 42 | if (!saved || saved === '') { 43 | localStorage.setItem(localStorageKey, JSON.stringify(defaultAnswers)); 44 | answers = defaultAnswers; 45 | } else { 46 | answers = JSON.parse(localStorage.getItem(localStorageKey)); 47 | } 48 | return answers; 49 | } 50 | 51 | chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { 52 | if (request.action === 'load') { 53 | var answers = getAnswersListFromStorage(); 54 | sendResponse({'answers': answers}); 55 | } 56 | if (request.action === 'save') { 57 | // Save the answers to local storage. 58 | var localStorageKey = '__GH_CANNED_ANSWERS__EXT__'; 59 | localStorage.setItem(localStorageKey, JSON.stringify(request.answers)); 60 | sendResponse(); 61 | } 62 | }); 63 | -------------------------------------------------------------------------------- /src/contentScript.js: -------------------------------------------------------------------------------- 1 | var __gcrExtAnswers; 2 | 3 | (function() { 4 | "use strict"; 5 | 6 | // This following code is taken from 7 | // https://github.com/thieman/github-selfies/blob/master/chrome/selfie.js 8 | var allowedPaths = [ 9 | // New issues 10 | /github.com\/[\w\-]+\/[\w\-]+\/issues\/new/, 11 | // Existing issues (comment) 12 | /github.com\/[\w\-]+\/[\w\-]+\/issues\/\d+/, 13 | // New pull request 14 | /github.com\/[\w\-]+\/[\w\-]+\/compare/, 15 | // Existing pull requests (comment) 16 | /github.com\/[\w\-]+\/[\w\-]+\/pull\/\d+/ 17 | ]; 18 | 19 | // Inject the code from fn into the page, in an IIFE. 20 | function inject(fn) { 21 | var script = document.createElement('script'); 22 | var parent = document.documentElement; 23 | script.textContent = '('+ fn +')();'; 24 | parent.appendChild(script); 25 | parent.removeChild(script); 26 | } 27 | 28 | // Post a message whenever history.pushState is called. GitHub uses 29 | // pushState to implement page transitions without full page loads. 30 | // This needs to be injected because content scripts run in a sandbox. 31 | inject(function() { 32 | var pushState = history.pushState; 33 | history.pushState = function on_pushState() { 34 | window.postMessage('extension:pageUpdated', '*'); 35 | return pushState.apply(this, arguments); 36 | }; 37 | var replaceState = history.replaceState; 38 | history.replaceState = function on_replaceState() { 39 | window.postMessage('extension:pageUpdated', '*'); 40 | return replaceState.apply(this, arguments); 41 | }; 42 | }); 43 | 44 | // Do something when the extension is loaded into the page, 45 | // and whenever we push/pop new pages. 46 | window.addEventListener("message", function(event) { 47 | if (event.data === 'extension:pageUpdated') { 48 | addAnswerButton(); 49 | } 50 | }); 51 | 52 | window.addEventListener("popstate", load); 53 | load(); 54 | 55 | // End of code from https://github.com/thieman/github-selfies/blob/master/chrome/selfie.js 56 | 57 | function load() { 58 | chrome.runtime.sendMessage({action: 'load'}, function(response) { 59 | __gcrExtAnswers = response.answers; 60 | addAnswerButton(); 61 | }); 62 | } 63 | 64 | function any(array, predicate) { 65 | for (var i = 0; i < array.length; i++) { 66 | if (predicate(array[i])) { 67 | return true; 68 | } 69 | } 70 | return false; 71 | } 72 | 73 | function addAnswerButton() { 74 | if (!any(allowedPaths, (path) => path.test(window.location.href))) { 75 | // NOPE. 76 | return; 77 | } 78 | 79 | // If there's already a button nuke it so we can start fresh. 80 | var existingButtons = document.querySelectorAll('.github-canned-response-item'); 81 | if (existingButtons && existingButtons.length !== 0) { 82 | for (var i = 0; i < existingButtons.length; i++) { 83 | existingButtons[i].parentNode.removeChild(existingButtons[i]); 84 | } 85 | } 86 | 87 | var targets = document.querySelectorAll('.js-toolbar.toolbar-commenting'); 88 | 89 | for (var i = 0; i < targets.length; i++) { 90 | var target = createNodeWithClass('div', 'toolbar-group github-canned-response-item'); 91 | targets[i].insertBefore(target, targets[i].childNodes[0]); 92 | 93 | var item = createNodeWithClass('div', 'select-menu js-menu-container js-select-menu label-select-menu'); 94 | target.appendChild(item); 95 | 96 | var button = createButton(); 97 | item.appendChild(button); 98 | 99 | if (targets[i]) { 100 | var dropdown = createDropdown(__gcrExtAnswers, targets[i]); 101 | item.appendChild(dropdown); 102 | } 103 | } 104 | } 105 | 106 | function createNodeWithClass(nodeType, className) { 107 | var element = document.createElement(nodeType); 108 | element.className = className; 109 | return element; 110 | } 111 | 112 | function createButton() { 113 | var button = createNodeWithClass('button', 'toolbar-item tooltipped tooltipped-n js-menu-target menu-target'); 114 | 115 | button.setAttribute('aria-label', 'Insert canned response'); 116 | button.style.display = 'inline-block'; 117 | button.type = 'button'; 118 | 119 | // Github just shipped svg icons! 120 | var svg = createSVG(18, 16, 'octicon-mail-read', "M6 5H4v-1h2v1z m3 1H4v1h5v-1z m5-0.48v8.48c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V5.52c0-0.33 0.16-0.63 0.42-0.81l1.58-1.13v-0.58c0-0.55 0.45-1 1-1h1.2L7 0l2.8 2h1.2c0.55 0 1 0.45 1 1v0.58l1.58 1.13c0.27 0.19 0.42 0.48 0.42 0.81zM3 7.5l4 2.5 4-2.5V3H3v4.5zM1 13.5l4.5-3L1 7.5v6z m11 0.5L7 11 2 14h10z m1-6.5L8.5 10.5l4.5 3V7.5z"); 121 | button.appendChild(svg); 122 | var span = createNodeWithClass('span', 'dropdown-caret'); 123 | button.appendChild(span); 124 | 125 | return button; 126 | } 127 | 128 | function createDropdown(answers, toolbar) { 129 | // This should use the fuzzy search instead (see labels) 130 | var outer = createNodeWithClass('div', 'select-menu-modal-holder js-menu-content js-navigation-container'); 131 | var inner = createNodeWithClass('div', 'select-menu-modal'); 132 | outer.appendChild(inner); 133 | 134 | // I hate the DOM. 135 | var header = createNodeWithClass('div', 'select-menu-header'); 136 | var headerSpan = createNodeWithClass('span', 'select-menu-title'); 137 | var spanText = document.createElement('text'); 138 | spanText.innerHTML = 'Canned responses '; 139 | 140 | var editButton = createNodeWithClass('button', 'btn-link github-canned-response-edit'); 141 | editButton.type = 'button'; 142 | editButton.innerHTML = '(edit or add new)'; 143 | editButton.addEventListener('click', showEditView); 144 | 145 | headerSpan.appendChild(spanText); 146 | headerSpan.appendChild(editButton); 147 | header.appendChild(headerSpan); 148 | inner.appendChild(header); 149 | 150 | var main = createNodeWithClass('div', 'js-select-menu-deferred-content'); 151 | inner.appendChild(main); 152 | 153 | var filter = createNodeWithClass('div', 'select-menu-filters'); 154 | var filterText = createNodeWithClass('div', 'select-menu-text-filter'); 155 | var filterInput = createNodeWithClass('input', 'js-filterable-field js-navigation-enable form-control'); 156 | var uniqueID = toolbar.closest('form').querySelector('textarea').id; 157 | filterInput.id = 'gcr-ext-filter-field' + uniqueID; 158 | filterInput.type = 'text'; 159 | filterInput.placeholder = 'Filter responses'; 160 | filterInput.autocomplete = 'off'; 161 | filterInput.setAttribute('aria-label', 'Type or choose a response'); 162 | 163 | filterText.appendChild(filterInput); 164 | filter.appendChild(filterText); 165 | main.appendChild(filter); 166 | 167 | var itemList = createNodeWithClass('div', 'select-menu-list'); 168 | itemList.setAttribute('data-filterable-for', 'gcr-ext-filter-field' + uniqueID); 169 | itemList.setAttribute('data-filterable-type', 'fuzzy'); 170 | 171 | main.appendChild(itemList); 172 | 173 | for (var i = 0; i < answers.length; i++) { 174 | var item = createDropdownItem(answers[i].name); 175 | itemList.appendChild(item); 176 | item.toolbar = toolbar; 177 | item.answer = answers[i].description; 178 | item.addEventListener('click', insertAnswer); 179 | 180 | // Gigantic hack because the PR page is not setting up mouse events correctly. 181 | item.addEventListener('mouseenter', function() { 182 | this.className += ' navigation-focus'; 183 | }); 184 | item.addEventListener('mouseleave', function() { 185 | this.className = this.className.replace(/ navigation-focus/g, ''); 186 | }); 187 | } 188 | 189 | return outer; 190 | } 191 | 192 | function createDropdownItem(text) { 193 | var item = createNodeWithClass('div', 'select-menu-item js-navigation-item'); 194 | item.textContent = text; 195 | return item; 196 | } 197 | 198 | function createSVG(height, width, octiconName, octiconPath) { 199 | var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 200 | svg.setAttribute('class', 'octicon ' + octiconName); 201 | svg.style.height = height + 'px'; 202 | svg.style.width = width + 'px'; 203 | svg.setAttribute('viewBox', '0 0 ' + height + ' ' + width); 204 | 205 | var path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 206 | path.setAttributeNS(null, 'd', octiconPath); 207 | svg.appendChild(path); 208 | 209 | return svg; 210 | } 211 | 212 | function insertAnswer(event) { 213 | var item = event.target; 214 | var textarea = item.toolbar.parentNode.parentNode.querySelector('textarea'); 215 | textarea.value += item.answer + '\n'; 216 | 217 | // Scroll down. 218 | textarea.focus(); 219 | textarea.scrollTop = textarea.scrollHeight; 220 | } 221 | 222 | function showEditView() { 223 | 224 | var dialog = createNodeWithClass('div', 'gcr-ext-editor-dialog'); 225 | dialog.id = 'gcr-ext-editor'; 226 | 227 | // lol. This is from options.html. 228 | // TODO: Replace this with ES6 civilized strings when you're less scared 229 | // about breaking everything. 230 | dialog.innerHTML = '
This is an easy title to remember this canned response by

And this is the actual content that will be inserted

'; 231 | 232 | var closeBar = dialog.querySelector('.gcr-ext-editor-close'); 233 | 234 | var closeText = createNodeWithClass('span', 'select-menu-title'); 235 | closeText.innerHTML = 'Edit or add canned responses'; 236 | closeText.style.float = 'left'; 237 | closeText.style.padding = '5px 10px'; 238 | closeText.style.color = 'black'; 239 | closeText.style.fontWeight = 'bold'; 240 | 241 | var closeButton = createNodeWithClass('button', 'btn-link delete-button'); 242 | closeButton.type = 'button'; 243 | closeButton.style.padding = '5px 10px'; 244 | closeButton.style.float = 'right'; 245 | var svg = createSVG(16, 16, 'octicon-x', 'M7.48 8l3.75 3.75-1.48 1.48-3.75-3.75-3.75 3.75-1.48-1.48 3.75-3.75L0.77 4.25l1.48-1.48 3.75 3.75 3.75-3.75 1.48 1.48-3.75 3.75z'); 246 | closeButton.appendChild(svg); 247 | closeButton.addEventListener('click', function() { 248 | document.body.removeChild(dialog); 249 | }); 250 | 251 | closeBar.appendChild(closeText); 252 | closeBar.appendChild(closeButton); 253 | document.body.appendChild(dialog); 254 | 255 | window.gcrExtEditorSaveAnswers = function() { 256 | chrome.runtime.sendMessage({action: 'save', answers:__gcrExtAnswers}, function(response) { 257 | addAnswerButton(); 258 | }); 259 | }; 260 | 261 | gcrExtEditorSetup(); 262 | gcrExtEditorUpdateAnswersList(); 263 | } 264 | })(); 265 | -------------------------------------------------------------------------------- /src/editor.js: -------------------------------------------------------------------------------- 1 | function gcrExtEditorUpdateAnswersList() { 2 | list.innerHTML = ''; 3 | for (var i = 0; i < __gcrExtAnswers.length; i++ ) { 4 | var li = gcrExtEditorCreateItem(__gcrExtAnswers[i].name, __gcrExtAnswers[i].description); 5 | li.answerId = i; 6 | list.appendChild(li); 7 | } 8 | } 9 | 10 | function gcrExtEditorSetup() { 11 | list = document.getElementById('gcrExtAnswerList'); 12 | 13 | document.querySelector('#gcrExtNewButton').addEventListener('click', function(event) { 14 | var name = document.getElementById('gcrExtNewTitle').value; 15 | var text = document.getElementById('gcrExtNewText').value; 16 | 17 | if (name.trim() === '' || text.trim() === '') { 18 | document.querySelector('#gcrExtNewConfirm').hidden = true; 19 | document.querySelector('#gcrExtNewError').hidden = false; 20 | return; 21 | } 22 | 23 | document.querySelector('#gcrExtNewConfirm').hidden = true; 24 | document.querySelector('#gcrExtNewError').hidden = true; 25 | 26 | var answerId = __gcrExtAnswers.length; 27 | __gcrExtAnswers.push({name: name, description: text}); 28 | 29 | // Save to local storage. 30 | gcrExtEditorSaveAnswers(); 31 | 32 | // Add to the UI list. 33 | var li = gcrExtEditorCreateItem(name, text); 34 | li.answerId = answerId; 35 | list.appendChild(li); 36 | 37 | document.querySelector('#gcrExtNewConfirm').hidden = false; 38 | document.getElementById('gcrExtNewTitle').value = ''; 39 | document.getElementById('gcrExtNewText').value = ''; 40 | 41 | // Clear it after a bit. 42 | setTimeout(function() { 43 | document.querySelector('#gcrExtNewConfirm').hidden = true; 44 | }, 2000); 45 | }); 46 | 47 | document.querySelector('.gcr-ext-editor-list').addEventListener('click', function(event) { 48 | if (event.target.tagName.toLowerCase() !== 'button') 49 | return; 50 | 51 | var button = event.target; 52 | var item = button.parentNode; 53 | var title = item.querySelector('.gcr-ext-editor-answer-title'); 54 | var text = item.querySelector('.gcr-ext-editor-answer-text'); 55 | 56 | // This is pretty lame. 57 | if (button.textContent.toLowerCase() === 'edit') { 58 | title.disabled = text.disabled = false; 59 | button.textContent = 'Save'; 60 | title.focus(); 61 | } else if (button.textContent.toLowerCase() === 'save') { 62 | title.disabled = text.disabled = true; 63 | button.textContent = 'Edit'; 64 | 65 | // Save locally. 66 | var answerId = item.answerId; 67 | __gcrExtAnswers[item.answerId].name = title.value; 68 | __gcrExtAnswers[item.answerId].description = text.value; 69 | 70 | // Save to local storage. 71 | gcrExtEditorSaveAnswers(); 72 | } else if (button.textContent.toLowerCase() === 'delete') { 73 | __gcrExtAnswers.splice(item.answerId, 1); 74 | // Save to local storage. 75 | gcrExtEditorSaveAnswers(); 76 | gcrExtEditorUpdateAnswersList(); 77 | } 78 | }); 79 | } 80 | 81 | 82 | function gcrExtEditorCreateItem(name, text) { 83 | var li = document.createElement('li'); 84 | 85 | var title = document.createElement('input'); 86 | title.className = 'gcr-ext-editor-answer-title gcr-ext-editor-single-line'; 87 | title.value = name; 88 | title.disabled = true; 89 | 90 | var desc = document.createElement('textarea'); 91 | desc.className = 'gcr-ext-editor-answer-text'; 92 | desc.textContent = text; 93 | desc.disabled = true; 94 | 95 | var edit = document.createElement('button'); 96 | edit.className = 'btn btn-sm btn-primary'; 97 | edit.textContent = 'Edit'; 98 | 99 | var del = document.createElement('button'); 100 | del.className = 'btn btn-sm'; 101 | del.textContent = 'Delete'; 102 | del.style.marginLeft = '10px'; 103 | 104 | li.appendChild(title); 105 | li.appendChild(desc); 106 | li.appendChild(edit); 107 | li.appendChild(del); 108 | return li; 109 | } 110 | -------------------------------------------------------------------------------- /src/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notwaldorf/github-canned-responses/6bc654db4a78ce8e01a5cca5d28f6c84153fcd07/src/logo.png -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name" : "GitHub Canned Responses", 4 | "description" : "Choose from a set of canned responses when commenting on your GitHub PRs or issues", 5 | "version" : "1.0.8", 6 | "icons": { 7 | "128": "logo.png" 8 | }, 9 | "permissions": [ 10 | "activeTab" 11 | ], 12 | "options_page": "options.html", 13 | "content_scripts": [ 14 | { 15 | "js": ["editor.js", "contentScript.js"], 16 | "css": ["options.css"], 17 | "matches": ["*://github.com/*"] 18 | } 19 | ], 20 | "background": { 21 | "scripts": ["background.js"] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/options.css: -------------------------------------------------------------------------------- 1 | .gcr-ext-editor-dialog { 2 | box-sizing: border-box; 3 | position: fixed; 4 | width: 800px; 5 | height: 600px; 6 | top: calc(50% - 300px); 7 | left: calc(50% - 400px); 8 | z-index: 1000; 9 | background: #fff; 10 | border-radius: 3px; 11 | overflow: hidden; 12 | 13 | /* GitHub modal popup styles */ 14 | color: #666; 15 | background-color: #fff; 16 | background-clip: padding-box; 17 | border: 1px solid rgba(200,200,200,0.4); 18 | border-radius: 3px; 19 | box-shadow: 0 3px 12px rgba(0,0,0,0.15); 20 | } 21 | 22 | .gcr-ext-editor-uppercase { text-transform: uppercase; } 23 | .gcr-ext-editor-wide { letter-spacing: 1px; } 24 | .gcr-ext-editor-thin { font-weight: 300; } 25 | 26 | .gcr-ext-editor-list ul { padding: 0; list-style-type: none; } 27 | .gcr-ext-editor-list li { margin-bottom: 30px; } 28 | 29 | .gcr-ext-editor-close { 30 | position: absolute; 31 | top: 0px; 32 | left: 0px; 33 | right: 0px; 34 | height: 30px; 35 | background: #f5f5f5; 36 | border-bottom: 1px solid #e5e5e5; 37 | z-index: 1; 38 | } 39 | .gcr-ext-editor-dialog-inner { 40 | margin-top: 30px; 41 | position: absolute; 42 | overflow: scroll; 43 | top: 0; 44 | bottom: 0; 45 | left: 0; 46 | right: 0; 47 | } 48 | 49 | .gcr-ext-editor-header { 50 | background: #f5f5f5; 51 | border-bottom: 1px solid #e5e5e5; 52 | } 53 | 54 | .gcr-ext-editor-list { 55 | max-width: 700px; 56 | margin: 20px auto; 57 | } 58 | 59 | .gcr-ext-editor-horizontal { 60 | display: -ms-flexbox; 61 | display: -webkit-flex; 62 | display: flex; 63 | -ms-flex-direction: row; 64 | -webkit-flex-direction: row; 65 | flex-direction: row; 66 | max-width: 700px; 67 | margin: 0px auto; 68 | padding: 30px 0 10px 0; 69 | } 70 | 71 | .gcr-ext-editor-answer-title { 72 | color: #2D243B; 73 | font-weight: 300; 74 | font-size: 24px; 75 | margin-bottom: 0px; 76 | word-wrap: break-word; 77 | } 78 | 79 | .gcr-ext-editor-answer-text { 80 | font-size: 16px; 81 | color: #666; 82 | line-height: 1.6; 83 | } 84 | 85 | .gcr-ext-editor-horizontal textarea, 86 | .gcr-ext-editor-horizontal input, 87 | .gcr-ext-editor-list textarea, 88 | .gcr-ext-editor-list input { 89 | display: block; 90 | width: 100%; 91 | position: relative; 92 | -webkit-appearance: none; 93 | border: 1px solid #ccc; 94 | padding: 2px 5px; 95 | } 96 | 97 | .gcr-ext-editor-list textarea[disabled], 98 | .gcr-ext-editor-list input[disabled] { 99 | border: none; 100 | resize: none; 101 | box-shadow: none; 102 | padding: 3px 6px; 103 | background: transparent; 104 | } 105 | 106 | .gcr-ext-editor-horizontal textarea, 107 | .gcr-ext-editor-list textarea { 108 | margin: 10px 0; 109 | font-family: inherit; 110 | } 111 | 112 | .gcr-ext-editor-answer-half { 113 | width: 400px !important; 114 | margin-right: 20px; 115 | } 116 | 117 | .gcr-ext-editor-status-message { 118 | font-size: 14px; 119 | color: #bd2c00; 120 | padding-left: 10px; 121 | } 122 | 123 | .gcr-ext-editor-del { 124 | margin-left: 20px; 125 | } 126 | -------------------------------------------------------------------------------- /src/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Canned response settings 6 | 7 | 8 | 64 | 65 | 66 |
67 |
68 |
69 | 70 | 71 |
72 |
73 |
74 | This is an easy title to remember this canned response by

75 |
76 | And this is the actual content that will be inserted

77 | 78 | 79 | 80 |
81 |
82 |
83 |
84 | 85 |
86 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | var __gcrExtAnswers = getAnswersListFromStorage(); 2 | var localStorageKey = '__GH_CANNED_ANSWERS__EXT__'; 3 | 4 | var gcrExtEditorSaveAnswers = function() { 5 | localStorage.setItem(localStorageKey, JSON.stringify(__gcrExtAnswers)); 6 | } 7 | 8 | gcrExtEditorSetup(); 9 | gcrExtEditorUpdateAnswersList(); 10 | --------------------------------------------------------------------------------