├── .gitignore ├── .jshintrc ├── .travis.yml ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE-GPL-2.0 ├── README.md ├── RELEASE.md ├── autobuild.json ├── package.json ├── src ├── icon.png ├── js │ ├── _banner.footer.js │ ├── _banner.header.js │ ├── dreditor.js │ ├── extensions │ │ ├── cache.js │ │ ├── debug.js │ │ ├── form.js │ │ ├── sort.js │ │ ├── storage.js │ │ └── update.js │ ├── init.js │ └── plugins │ │ ├── comment.number.js │ │ ├── form.backup.js │ │ ├── form.sticky.js │ │ ├── inline.image.js │ │ ├── issue.clone.js │ │ ├── issue.count.js │ │ ├── issue.js │ │ ├── issue.markasread.js │ │ ├── issue.summary.js │ │ ├── issue.summary.template.js │ │ ├── issues.filter.js │ │ ├── patch.name.suggestion.js │ │ ├── patch.review.js │ │ ├── pift.js │ │ ├── projects.collapse.js │ │ └── syntax.autocomplete.js └── less │ └── dreditor.less ├── templates ├── chrome │ └── manifest.json ├── firefox │ ├── lib │ │ └── main.js │ └── package.json └── safari │ ├── Info.plist │ ├── Settings.plist │ └── update.plist └── tests ├── README.md ├── index.html ├── lib ├── qunit-1.12.0.css └── qunit-1.12.0.js └── src └── js ├── extensions ├── cache.html └── storage.html └── plugins └── form.backup.html /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /build/ 3 | /release/ 4 | /tmp/ 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "boss": true, 12 | "eqnull": true, 13 | "browser": true, 14 | "globals": { 15 | "$": true, 16 | "jQuery": true, 17 | "Drupal": true, 18 | "sortOrder": true, 19 | "dreditor_loader": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | before_script: 5 | - npm install -g grunt-cli 6 | notifications: 7 | irc: 8 | channels: 9 | - "irc.freenode.org#dreditor" 10 | template: 11 | - "#%{build_number} %{branch} %{result}: %{build_url}" 12 | on_success: always 13 | on_failure: always 14 | use_notice: true 15 | skip_join: true 16 | email: 17 | recipients: 18 | - dreditor@googlegroups.com 19 | on_success: never 20 | on_failure: always 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Dreditor runs native JavaScript (and [jQuery]) code. Development and building 4 | of browser extensions is powered by [Node.js] and [Grunt]. 5 | 6 | ## Setup 7 | 8 | Setting up a local development environment is simple; it's all automated: 9 | 10 | 1. Install [Node.js] - ensure to install the bundled Node Package Manager 11 | ([npm]), too. 12 | 1. Install [Grunt] by running the following shell command: 13 | 14 | ```sh 15 | npm install -g grunt-cli 16 | ``` 17 | 1. Confirm that the Grunt CLI is installed and works: 18 | 19 | ```sh 20 | grunt --version 21 | ``` 22 | 1. Clone the Dreditor repository: 23 | 24 | ```sh 25 | git clone https://github.com/unicorn-fail/dreditor.git 26 | ``` 27 | 1. Change into the new repository directory and install all dependencies: 28 | 29 | ```sh 30 | cd dreditor 31 | npm install 32 | grunt install 33 | ``` 34 | 1. Start a first Dreditor build by running: 35 | 36 | ```sh 37 | grunt 38 | ``` 39 | 40 | The `Gruntfile.js` in the top-level directory controls the compilation and build 41 | process. 42 | 43 | To see a list of available grunt tasks, run: 44 | 45 | ```sh 46 | $ grunt --help 47 | … 48 | Available tasks 49 | … 50 | install Installs dependencies. 51 | default Compiles code. 52 | dev:ff Compiles code to build a Firefox extension. (see watch:ff) 53 | watch:ff Enables real-time development for Firefox. 54 | test Runs tests. 55 | travis-ci Compiles code and runs tests. 56 | build Compiles code and builds all extensions. 57 | build:chrome Builds the Chrome extension. 58 | build:firefox Builds the Firefox extension. 59 | build:safari Builds the Safari extension. 60 | autoload:ff Loads the XPI extension into Firefox. 61 | ``` 62 | 63 | For more information about Grunt, see its 64 | [getting started guide](http://gruntjs.com/getting-started). 65 | 66 | 67 | 68 | ## Development 69 | 70 | All set? Let's get ready to rumble! 71 | 72 | ### File Structure 73 | 74 | | Directory | Content 75 | |:--------------------- |:------------------------------------------------ 76 | | *Main source code:* | 77 | | `/src/js/extensions` | Base components, libraries, and utility functions. 78 | | `/src/js/plugins` | Individual features split into one file per feature. 79 | | `/src/less` | [Less](http://lesscss.org) CSS. 80 | | `/tests` | [QUnit] tests. 81 | | `/templates` | Templates for building browser extensions. (rarely touched) 82 | | — | 83 | | *Build artifacts:* | 84 | | `/build` | Code compiled by grunt; e.g., `dreditor.js` 85 | | `/release` | Fully packaged browser extensions. 86 | 87 | 88 | ### Hacking 89 | 90 | #### Just code 91 | 92 | 1. Create a new topic/feature branch. 93 | _Please do not work in the `1.x` branch directly._ 94 | 95 | 1. Start watching file changes: 96 | 97 | ```sh 98 | $ grunt watch 99 | ``` 100 | 1. Write code. 101 | _Check the console output for warnings and errors._ 102 | 103 | 1. ~~Write automated tests.~~ 104 | _Later… see Automated testing chapter below._ 105 | 106 | 1. Build an extension and manually test your changes. 107 | _See Manual testing chapter below._ 108 | 109 | 1. Push your branch into your fork and create a pull request. 110 | 111 | For debugging use `$.debug()` or globals like `window.console` or `window.alert`. 112 | 113 | 114 | #### Live testing 115 | 116 | Some browsers have built-in support for automatically refreshing an extension 117 | via the command line. 118 | 119 | → Check the _Manual testing_ chapter below to set up your browser. 120 | 121 | For example, for Firefox, just simply run this: 122 | 123 | ```sh 124 | $ grunt watch:ff 125 | ``` 126 | 127 | This will immediately perform an initial build (to simplify switching between 128 | branches), and upon any file change, a new extension is immediately built and 129 | loaded into your browser. 130 | 131 | → Simply reload a page on https://drupal.org/ and your changes are immediately 132 | active! 133 | 134 | _(Just reload, no need to force-refresh!)_ 135 | 136 | 137 | ### Coding standards 138 | 139 | Dreditor mostly follows Drupal's [JavaScript](https://drupal.org/node/172169) 140 | and [CSS](https://drupal.org/node/1886770) coding standards. Quick summary: 141 | 142 | * Two spaces for indentation. No tabs. Use `"\t"` if you need a literal tab 143 | character in a string. 144 | _Exception:_ Markdown uses 4 spaces for indentation for maximum parser 145 | compatibility. 146 | * No trailing white-space. 147 | _Exception:_ Markdown uses 2 trailing spaces to enforce a linebreak. 148 | * Don't go overboard with white-space. 149 | * No more than [one assignment](http://benalman.com/news/2012/05/multiple-var-statements-javascript/) 150 | per `var` statement. 151 | * Delimit strings with single-quotes `'`, not double-quotes `"`. 152 | * Prefer `if` and `else` over non-obvious `? : ` ternary operators and complex 153 | `||` or `&&` expressions. 154 | * Comment your code. Place comments _before_ the line of code, _not_ at the 155 | _end_ of the line. 156 | * **When in doubt, stay consistent.** Follow the conventions you see in the 157 | existing code. 158 | 159 | 160 | ### Automated testing 161 | 162 | When submitting a pull request, [Travis CI] will automatically… 163 | 164 | 1. Perform an automated build. 165 | 1. [JSHint] all code to check for errors. 166 | 1. Run [Qunit] tests. 167 | 168 | _Work In Progress…_ — More information on [qunit testing] coming soon. 169 | 170 | 171 | 172 | ### Feature Branches and Pull Requests 173 | 174 | Normally, branches and PRs are created from user-specific forks/repositories. 175 | 176 | However, the maintainers MAY create public feature branches in the Dreditor 177 | repository under the following conditions: 178 | 179 | 1. The code is known to be incomplete and needs more work. 180 | 181 | In this case, a public feature/topic branch in the Dreditor repository 182 | _explicitly encourages_ co-maintainers to liberally improve the code through 183 | additional commits. However: 184 | 185 | Commits MUST NOT be amended and the branch MUST NOT be rebased. Contributors 186 | MAY create PRs against the branch. 187 | 188 | 1. The branch represents a major refactoring/rewrite feature/topic on Dreditor's 189 | roadmap. 190 | 191 | In this case, the public feature/topic branch exists in order to be developed 192 | in parallel to the current stable/mainline. If/when merged into the mainline, 193 | the merge of the feature/topic branch will denote a new major or minor 194 | version (e.g., v1 → v2). 195 | 196 | Maintainers and contributors SHOULD create PRs against such major refactoring 197 | branches, since each change proposal SHOULD be reviewed independently. 198 | 199 | Major feature/topic branches SHOULD have a maintainable scope. Therefore, 200 | they MAY be [criss-]cross-merged selectively into other major feature/topic 201 | branches, if necessary. 202 | 203 | Sub-topics of major features/topics MAY be developed in public 'child' 204 | feature/topic branches, but it is RECOMMENDED to architect and design changes 205 | in a way to make them work independently to begin with. 206 | 207 | In any case, every public feature/topic branch in the Dreditor repository MUST 208 | have a corresponding pull request (or issue) that holds the main discussion. 209 | A public feature/topic branch without a corresponding pull request (or issue) 210 | MAY be deleted without further notice. A public feature/topic branch SHOULD be 211 | deleted after merging it into the mainline. 212 | 213 | 214 | ## Manual testing 215 | 216 | **Note:** 217 | 218 | 1. Installing a development build of Dreditor will **replace** the extension 219 | installed from dreditor.org. 220 | 1. Whenever loading a custom build into your browser, make sure that you have 221 | **only one** Dreditor extension enabled at the same time. 222 | 223 | ### Chrome 224 | 225 | 1. Go to [`chrome://extensions`](chrome://extensions) 226 | 1. Enable _Developer mode_. 227 | 1. Click on _Load unpacked extension…_ 228 | 1. Browse to the `/build/chrome` directory and click `Select`. 229 | 1. Manually refresh the extensions page after each code change. 230 | 231 | ### Firefox 232 | 233 | Requires the [Firefox Add-on SDK](https://developer.mozilla.org/en-US/Add-ons/SDK), 234 | which should have been installed by the initial Setup already. 235 | 236 | 1. Install the [Extension Auto-Installer Add-on](https://addons.mozilla.org/en-US/firefox/addon/autoinstaller/). 237 | 238 | 1. Ensure that `wget` is installed. (Test with `wget --version`) 239 | 240 | ```sh 241 | # OSX 242 | brew|port install wget 243 | # Ubuntu 244 | sudo apt-get install wget 245 | ``` 246 | 247 | 1. Run: `grunt watch:ff` 248 | 249 | Alternatively, to manually load a single build without the autoinstaller: 250 | 251 | 1. From the _Tools_ menu, choose _Add-ons_. 252 | 1. Use the gear menu and choose _Install Add-on From File…_ 253 | 1. Browse to `/release/firefox` and select the `dreditor.xpi` file. 254 | 255 | 256 | ### Safari 257 | 258 | Requires a (free) [Safari Developer Certificate](https://developer.apple.com/register/index.action). 259 | 260 | 1. Open the _Preferences_ menu, choose the _Advanced_ tab, and enable 261 | _Show Develop menu in menu bar_. 262 | 1. From the _Develop_ menu, choose _Show Extension Builder_. 263 | 1. Click the + button in the bottom left corner of the window and choose 264 | _Add Extension…_ 265 | 1. Browse to the `/build/dreditor.safariextension` **directory** and click 266 | _Select_. 267 | 1. Assuming a valid Safari Developer Certificate, click the _Install_ button in 268 | the top right. 269 | * Upon first use of your Safari Developer Certificate, you will be asked to 270 | grant access. 271 | 1. Click the _Reload_ button in the Extension Builder window after each code 272 | change. 273 | 274 | 275 | 276 | [jQuery]: http://jquery.com 277 | [Node.js]: http://nodejs.org 278 | [npm]: http://npmjs.org 279 | [Grunt]: http://gruntjs.com 280 | [Less]: http://lesscss.org 281 | [QUnit]: http://qunitjs.com 282 | [Travis CI]: https://travis-ci.org/unicorn-fail/dreditor 283 | [JSHint]: http://www.jshint.com 284 | [qunit testing]: http://jordankasper.com/blog/2013/04/automated-javascript-tests-using-grunt-phantomjs-and-qunit/ 285 | 286 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | /*global require:false*/ 3 | /*global process:false*/ 4 | module.exports = function(grunt) { 5 | // Require normal dependencies. 6 | var dependencies = ['dependencies', 'devDependencies']; 7 | 8 | // Add in optionalDependencies if no --no-optional flag is present. 9 | var optionalDependencies = !grunt.option('no-optional'); 10 | if (optionalDependencies) { 11 | dependencies.push('optionalDependencies'); 12 | } 13 | 14 | // Load all grunt tasks matching the `grunt-*` pattern. 15 | require('load-grunt-tasks')(grunt, { scope: dependencies }); 16 | 17 | // Initialize grunt configuration. 18 | grunt.initConfig({ 19 | // Metadata. 20 | pkg: grunt.file.readJSON('package.json'), 21 | banner: '/**\n' + 22 | ' * <%= pkg.title || pkg.name %> <%= pkg.version %>\n' + 23 | '<%= pkg.homepage ? " * " + pkg.homepage + "\\n" : "" %>' + 24 | ' * <%= pkg.description %>\n' + 25 | ' * Licensed under <%= _.pluck(pkg.licenses, "type").join(", ") %>\n *\n' + 26 | ' * Maintainers:' + 27 | '<% _.forEach(pkg.maintainers, function(maintainer) {%>\n * <%= maintainer.name %> - <%= maintainer.url %><% }); %>\n *\n' + 28 | ' * Last build: <%= grunt.template.today("yyyy-mm-dd h:MM:ss TT Z") %>\n' + 29 | ' */\n', 30 | // Task configuration. 31 | clean: { 32 | files: [ 33 | 'build/*', 34 | 'release/*' 35 | ] 36 | }, 37 | less: { 38 | options: { 39 | cleancss: true 40 | }, 41 | files: { 42 | src: 'src/less/**/*.less', 43 | dest: 'build/<%= pkg.name %>.css' 44 | } 45 | }, 46 | css2js: { 47 | convert: { 48 | src: 'build/<%= pkg.name %>.css', 49 | dest: 'build/<%= pkg.name %>.css.js' 50 | } 51 | }, 52 | concat: { 53 | options: { 54 | banner: '<%= banner %>' + grunt.file.read('src/js/_banner.header.js'), 55 | footer: grunt.file.read('src/js/_banner.footer.js'), 56 | stripBanners: true 57 | }, 58 | build: { 59 | src: [ 60 | 'src/js/**/*.js', 61 | '!src/js/_banner.header.js', 62 | '!src/js/_banner.footer.js', 63 | '!src/js/init.js', 64 | 'build/<%= pkg.name %>.css.js', 65 | 'src/js/init.js' 66 | ], 67 | dest: 'build/<%= pkg.name %>.js' 68 | } 69 | }, 70 | jshint: { 71 | options: { 72 | reporter: optionalDependencies ? require('jshint-stylish') : undefined 73 | }, 74 | package: { 75 | options: { 76 | jshintrc: '.jshintrc' 77 | }, 78 | src: 'package.json' 79 | }, 80 | gruntfile: { 81 | options: { 82 | jshintrc: '.jshintrc' 83 | }, 84 | src: 'Gruntfile.js' 85 | }, 86 | js: { 87 | options: { 88 | jshintrc: '.jshintrc' 89 | }, 90 | src: 'src/js/**/*.js' 91 | } 92 | }, 93 | sed: { 94 | name: { 95 | path: 'build/', 96 | pattern: '%PKG.NAME%', 97 | replacement: '<%= pkg.name %>', 98 | recursive: true 99 | }, 100 | title: { 101 | path: 'build/', 102 | pattern: '%PKG.TITLE%', 103 | replacement: '<%= pkg.title || pkg.name %>', 104 | recursive: true 105 | }, 106 | description: { 107 | path: 'build/', 108 | pattern: '%PKG.DESCRIPTION%', 109 | replacement: '<%= pkg.description %>', 110 | recursive: true 111 | }, 112 | homepage: { 113 | path: 'build/', 114 | pattern: '%PKG.HOMEPAGE%', 115 | replacement: '<%= pkg.homepage || "" %>', 116 | recursive: true 117 | }, 118 | author: { 119 | path: 'build/', 120 | pattern: '%PKG.AUTHOR%', 121 | replacement: '<%= pkg.author.name %>', 122 | recursive: true 123 | }, 124 | icon: { 125 | path: 'build/', 126 | pattern: '%PKG.ICON%', 127 | replacement: '<%= pkg.icon || "icon.png" %>', 128 | recursive: true 129 | }, 130 | license: { 131 | path: 'build/', 132 | pattern: '%PKG.LICENSE%', 133 | replacement: '<%= _.pluck(pkg.licenses, "type").join(", ") %>', 134 | recursive: true 135 | }, 136 | version: { 137 | path: 'build/', 138 | pattern: '%PKG.VERSION%', 139 | replacement: '<%= pkg.version %>', 140 | recursive: true 141 | } 142 | }, 143 | uglify: { 144 | options: { 145 | banner: '<%= banner %>', 146 | beautify: true 147 | }, 148 | default: { 149 | src: '<%= concat.build.dest %>', 150 | dest: 'build/<%= pkg.name %>.js' 151 | } 152 | }, 153 | copy: { 154 | chrome: { 155 | files: [ 156 | { 157 | expand: true, 158 | cwd: 'templates/chrome/', 159 | src: ['**'], 160 | dest: 'build/chrome/' 161 | }, 162 | { 163 | expand: true, 164 | cwd: 'src/', 165 | src: ['icon.png'], 166 | dest: 'build/chrome/' 167 | }, 168 | { 169 | expand: true, 170 | cwd: 'build/', 171 | src: ['<%= pkg.name %>.js'], 172 | dest: 'build/chrome/' 173 | } 174 | ] 175 | }, 176 | firefox: { 177 | files: [ 178 | { 179 | expand: true, 180 | cwd: 'templates/firefox/', 181 | src: ['**'], 182 | dest: 'build/firefox/' 183 | }, 184 | { 185 | expand: true, 186 | cwd: 'src/', 187 | src: ['icon.png'], 188 | dest: 'build/firefox/' 189 | }, 190 | { 191 | expand: true, 192 | cwd: 'build/', 193 | src: ['<%= pkg.name %>.js'], 194 | dest: 'build/firefox/data/' 195 | } 196 | ] 197 | }, 198 | safari: { 199 | files: [ 200 | { 201 | expand: true, 202 | cwd: 'templates/safari/', 203 | src: ['Info.plist', 'Settings.plist'], 204 | dest: 'build/<%= pkg.name %>.safariextension/' 205 | }, 206 | { 207 | expand: true, 208 | cwd: 'templates/safari/', 209 | src: ['update.plist'], 210 | dest: 'build/' 211 | }, 212 | { 213 | expand: true, 214 | cwd: 'src/', 215 | src: ['icon.png'], 216 | dest: 'build/<%= pkg.name %>.safariextension/' 217 | }, 218 | { 219 | expand: true, 220 | cwd: 'build/', 221 | src: ['<%= pkg.name %>.js'], 222 | dest: 'build/<%= pkg.name %>.safariextension/' 223 | } 224 | ] 225 | } 226 | }, 227 | watch: { 228 | files: [ 229 | // Force-exclude artifacts. 230 | // Despite not being included in the list of files, the watch task can 231 | // be intermittently interrupted by a build:* task with: 232 | // >> File "release" added. 233 | // which may even cause an infinite loop. Seemingly a bug in watch; 234 | // possibly limited to Windows/NTFS/msys. Exclusions must be defined 235 | // first; all arguments are processed/merged sequentially. 236 | '!build', 237 | '!build/**', 238 | '!release', 239 | '!release/**', 240 | '<%= jshint.package.src %>', 241 | '<%= jshint.gruntfile.src %>', 242 | '<%= jshint.js.src %>', 243 | '<%= less.files.src %>', 244 | '<%= qunit.all %>' 245 | ], 246 | tasks: ['default'], 247 | options: { 248 | interrupt: true 249 | } 250 | }, 251 | release: { 252 | options: { 253 | add: false, 254 | commit: false, 255 | tag: false, 256 | push: false, 257 | pushTags: false, 258 | npm: false 259 | } 260 | }, 261 | compress: { 262 | chrome: { 263 | options: { 264 | archive: 'release/chrome/<%= pkg.name %>.zip', 265 | mode: 'zip' 266 | }, 267 | expand: true, 268 | cwd: 'build/chrome/', 269 | src: ['**/*'], 270 | dest: '/' 271 | } 272 | }, 273 | "mozilla-addon-sdk": { 274 | 'release': { 275 | options: { 276 | revision: "1.16" 277 | } 278 | } 279 | }, 280 | "mozilla-cfx-xpi": { 281 | 'release': { 282 | options: { 283 | "mozilla-addon-sdk": "release", 284 | extension_dir: "build/firefox", 285 | dist_dir: "release/firefox", 286 | // --output-file is an experimental option, not guaranteed to exist. 287 | // @see https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/cfx#Experimental_Options_3 288 | arguments: "--output-file=<%= pkg.name %>.xpi" 289 | } 290 | } 291 | }, 292 | 'qunit' : { 293 | all: "tests/**/*.html" 294 | }, 295 | exec: { 296 | build_safari_ext: { 297 | stdout: false, 298 | stderr: false, 299 | cmd: function () { 300 | var args = [ 301 | 'build-safari-ext', 302 | this.template.process('<%= pkg.name %>'), 303 | this.template.process(process.cwd() + '/build/<%= pkg.name %>.safariextension'), 304 | process.cwd() + '/release/safari' 305 | ]; 306 | return args.join(' '); 307 | }, 308 | callback: function (error) { 309 | if (error) { 310 | grunt.log.warn('Unable to create ' + String(grunt.template.process('release/safari/<%= pkg.name %>.safariextension')).red); 311 | } 312 | else { 313 | grunt.log.writeln('Created ' + String(grunt.template.process('release/safari/<%= pkg.name %>.safariextension')).cyan); 314 | } 315 | } 316 | } 317 | } 318 | }); 319 | 320 | // Install tasks. 321 | grunt.registerTask('install', 'Installs dependencies.', 322 | ['mozilla-addon-sdk']); 323 | 324 | // Default tasks. 325 | grunt.registerTask('default', 'Compiles code.', 326 | ['clean', 'less', 'css2js', 'jshint', 'concat', 'copy', 'sed']); 327 | 328 | // Realtime development tasks. 329 | // Enjoy: `grunt watch:ff` 330 | // These tasks are highly tailored subsets of the default task having the goal 331 | // of *instant* reloading of a newly built extension into a particular browser. 332 | // The performance target is ~500ms; i.e., the time it takes a human to switch 333 | // from the code editor to the browser. 334 | // Note that grunt watch is not a multi-task; it supports multiple targets, 335 | // but it does not support multiple tasks/sets; when running `grunt watch`, 336 | // all targets are watched, and all tasks of all matching targets are executed 337 | // upon a change. We do not want to tamper with the default `grunt watch` task, 338 | // nor do we want to build all extensions at once (for performance reasons). 339 | // The recommended informal workaround is to dynamically swap out the default 340 | // config of the watch task ad-hoc. 341 | // @see https://github.com/gruntjs/grunt-contrib-watch/issues/71#issuecomment-26152333 342 | // Firefox. 343 | grunt.registerTask('dev:ff', 'Compiles code to build a Firefox extension. (see watch:ff)', 344 | ['less', 'css2js', 'jshint:js', 'concat', 'copy:firefox', 'sed']); 345 | grunt.registerTask('watch:ff', 'Enables real-time development for Firefox.', function () { 346 | var config = grunt.config('watch'); 347 | config.tasks = ['dev:ff', 'build:firefox', 'autoload:ff']; 348 | // Auto-run once upon invocation. 349 | config.options.atBegin = true; 350 | config.options.spawn = false; 351 | grunt.config('watch', config); 352 | grunt.task.run('watch'); 353 | }); 354 | 355 | // Test tasks. 356 | grunt.registerTask('test', 'Runs tests.', 357 | ['qunit']); 358 | grunt.registerTask('travis-ci', 'Compiles code and runs tests.', 359 | ['default', 'test']); 360 | 361 | // Build tasks. 362 | grunt.registerTask('build', 'Compiles code and builds all extensions.', 363 | ['default', 'uglify', 'build:chrome', 'build:firefox', 'build:safari']); 364 | grunt.registerTask('build:chrome', 'Builds the Chrome extension.', 365 | ['compress:chrome']); 366 | grunt.registerTask('build:firefox', 'Builds the Firefox extension.', 367 | ['mozilla-cfx-xpi']); 368 | grunt.registerTask('build:safari', 'Builds the Safari extension.', 369 | ['exec:build_safari_ext']); 370 | 371 | // Autoload tasks. 372 | // Firefox. 373 | // @see https://addons.mozilla.org/en-US/firefox/addon/autoinstaller/ 374 | grunt.registerTask('autoload:ff', 'Loads the XPI extension into Firefox.', function () { 375 | var done = this.async(); 376 | var xpi = 'release/firefox/' + grunt.template.process('<%= pkg.name %>.xpi'); 377 | grunt.util.spawn({ 378 | cmd: 'wget', 379 | args: [ 380 | '--post-file=' + xpi, 381 | 'http://localhost:8888' 382 | ], 383 | opts: !grunt.option('debug') ? {} : { 384 | stdio: 'inherit' 385 | } 386 | }, 387 | function (error, result, code) { 388 | if (code !== 8) { 389 | return grunt.warn('Auto-loading ' + xpi + ' failed:\n\n' + 390 | code + ': ' + error + '\n\n' + 391 | 'Ensure you have the AutoInstaller extension installed in Firefox:\n' + 392 | 'https://addons.mozilla.org/en-US/firefox/addon/autoinstaller/\n\n' 393 | ); 394 | } 395 | grunt.log.ok('Auto-loaded ' + xpi + ' into Firefox.'); 396 | done(); 397 | }); 398 | }); 399 | 400 | }; 401 | -------------------------------------------------------------------------------- /LICENSE-GPL-2.0: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ABANDONED PROJECT 2 | 3 | > See: 4 | 5 | Dreditor has been decommissioned due to the lack of availability of current maintainers and the fact that Drupal.org 6 | itself is becoming more feature rich at a much faster rate than Dreditor itself now. 7 | 8 | This decision was primarily made due to the (original) effort it would take to create an "official" Firefox extension 9 | ([#256](https://github.com/unicorn-fail/dreditor/issues/256)) amongst a few major issues that were not solvable as a 10 | browser extension and/or would require a complete rewrite anyway: 11 | 12 | - No mobile support (mobile browsers don't have extensions). 13 | - Extremely inefficient (large patch files could cause browser to hang). 14 | - Newer, more frequent, Drupal.org deployments often break assumptions of expected markup made by this project causing 15 | maintainers to live in an unrealistic state of having to "drop everything to hotfix and release". 16 | 17 | If you already have the extension installed, it may continue working for a while until (inevitably) the markup on 18 | Drupal.org changes enough that the extension is unable to function. 19 | 20 | We understand that many of you love this extension, we did too. This isn't the end, in fact, it's a much brighter 21 | beginning. All the things we love about Dreditor can, and should, be moved into Drupal.org natively. 22 | 23 | To reiterate: this project is dead and should remain dead. This project's code will no longer be updated, nor will 24 | anymore releases be made. 25 | 26 | Any and all effort to "add features" or "fix" things should be done on Drupal.org itself by searching for existing 27 | issues or creating a new issue in the Drupal.org issue queue: 28 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release standards and procedures 2 | 3 | ## Versioning 4 | 5 | Dreditor follows the [Semantic Versioning](http://semver.org/) standard. 6 | 7 | Given a version number `MAJOR.MINOR.PATCH`, increment the: 8 | 9 | * MAJOR version when the new release contains incompatible API changes. 10 | 11 | ```diff 12 | -1.2.5 13 | +2.0.0 14 | ``` 15 | * MINOR version when the new release adds new functionality in a 16 | backwards-compatible manner. 17 | 18 | ```diff 19 | -1.2.5 20 | +1.3.0 21 | ``` 22 | * PATCH version when the new release contains backwards-compatible bug fixes. 23 | 24 | ```diff 25 | -1.2.5 26 | +1.2.6 27 | ``` 28 | 29 | Additional suffixes for pre-releases or build numbers may be appended, separated 30 | by a dash/hyphen. 31 | 32 | ## Release Process 33 | 34 | Examples assume that: 35 | * Current version is `1.2.5` and new version is `1.2.6`. 36 | * `origin` refers to [unicorn-fail/dreditor] 37 | 38 | 1. Ensure that you have the latest code and HEAD is clean: 39 | 40 | ```sh 41 | $ git checkout 1.x 42 | $ git reset --hard 43 | $ git pull origin 1.x 44 | ``` 45 | 46 | 1. Confirm that Dreditor works correctly: 47 | 48 | ```sh 49 | $ grunt build 50 | $ grunt test 51 | ``` 52 | 53 | 1. Increase the version number: 54 | 55 | ```sh 56 | $ grunt release 57 | ``` 58 | 59 | This bumps the PATCH version in `package.json`. Alternatively: 60 | 61 | * To bump the MINOR version: `grunt release:minor` 62 | * To bump the MAJOR version: `grunt release:major` 63 | 64 | ```diff 65 | $ git diff 66 | diff --git a/package.json b/package.json 67 | index 2432958..1a299ec 100644 68 | --- a/package.json 69 | +++ b/package.json 70 | @@ -5 +5 @@ 71 | - "version": "1.2.5", 72 | + "version": "1.2.6", 73 | ``` 74 | 75 | 1. Commit the version change: 76 | 77 | ```sh 78 | $ git add package.json 79 | $ git commit -m "Dreditor 1.2.6" 80 | ``` 81 | 82 | 1. Create an annotated tag for the release: 83 | 84 | ```sh 85 | $ git tag -m 1.2.6 1.2.6 86 | ``` 87 | 88 | 1. Push the new version commit and tag: 89 | 90 | ```sh 91 | $ git push origin 1.x --tags 92 | ``` 93 | 94 | 1. Verify that the new release and all builds appear on the [build page]. 95 | 96 | 1. Test whether the new release installs and works correctly in each browser. 97 | 98 | All performed operations up till here can be reverted in case something went 99 | wrong. 100 | 101 | ## Publishing releases 102 | 103 | Proceed with publishing the newly built extensions. 104 | 105 | Examples assume that: 106 | * Current version is `1.2.5` and new version is `1.2.6`. 107 | * `origin` refers to [unicorn-fail/dreditor.org] 108 | 109 | 110 | ### Chrome 111 | 112 | 1. Download the packaged Chrome extension from the [build page]. 113 | 114 | 1. Go to https://chrome.google.com/webstore/developer/dashboard 115 | 116 | 1. Click the _Edit_ operation link for _Dreditor_. 117 | 118 | 1. Click the _Upload Updated Package_ button + upload the downloaded package. 119 | 120 | 1. Click the _Publish changes_ button at the bottom. 121 | 122 | ### Firefox and Safari 123 | 124 | 1. Change to your local clone of [unicorn-fail/dreditor.org]. 125 | 126 | 1. Ensure that you have the latest code and HEAD is clean: 127 | 128 | ```sh 129 | $ git checkout 7.x 130 | $ git reset --hard 131 | $ git pull origin 7.x 132 | ``` 133 | 134 | 1. Download the packaged extension for each browser from the [build page] and 135 | replace the corresponding `dreditor.*` file in the root directory. 136 | 137 | 1. Create a new "release" on https://github.com/unicorn-fail/dreditor/releases for the tag created 138 | above and upload the same binary files to that tagged release. 139 | 140 | 1. Edit the `update.plist` file in the root directory to replace the version 141 | with the new version. 142 | 143 | ```diff 144 | $ git diff 145 | diff --git a/dreditor.safariextz b/dreditor.safariextz 146 | index 2e8dd2d..ee6524f 100644 147 | Binary files a/dreditor.safariextz and b/dreditor.safariextz differ 148 | diff --git a/dreditor.xpi b/dreditor.xpi 149 | index 742cdf1..0e6542e 100644 150 | Binary files a/dreditor.xpi and b/dreditor.xpi differ 151 | diff --git a/update.plist b/update.plist 152 | index 9d040be..f1b51b5 100644 153 | --- a/update.plist 154 | +++ b/update.plist 155 | @@ -12,5 +12,5 @@ 156 | CFBundleVersion 157 | - 1.2.5 158 | + 1.2.6 159 | CFBundleShortVersionString 160 | - 1.2.5 161 | + 1.2.6 162 | URL 163 | ``` 164 | 165 | 1. Commit and push the new releases to [unicorn-fail/dreditor.org]: 166 | 167 | ```sh 168 | $ git add -u 169 | $ git commit -m "Dreditor 1.2.6" 170 | $ git push origin 7.x 171 | ``` 172 | 173 | 1. Deploy changes on the server: 174 | 175 | ```sh 176 | $ sudo su 177 | $ cd /var/www/sites/dreditor.org 178 | $ git pull 179 | ``` 180 | 181 | 182 | [build page]: https://dreditor.org/development/build#tags 183 | [unicorn-fail/dreditor]: https://github.com/unicorn-fail/dreditor 184 | [unicorn-fail/dreditor.org]: https://github.com/unicorn-fail/dreditor.org 185 | -------------------------------------------------------------------------------- /autobuild.json: -------------------------------------------------------------------------------- 1 | [ 2 | "node --version", 3 | "npm --version", 4 | "npm install --color=always --no-optional", 5 | "grunt --version", 6 | "grunt install --no-optional", 7 | "grunt build --no-optional" 8 | ] 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dreditor", 3 | "title": "Dreditor", 4 | "description": "An extension for Drupal.org that enhances user experience and functionality. Original author: Daniel F. Kudwien (sun).", 5 | "version": "1.2.17", 6 | "homepage": "https://dreditor.org", 7 | "author": { 8 | "name": "Mark Carver", 9 | "url": "https://drupal.org/user/501638" 10 | }, 11 | "icon": "icon.png", 12 | "maintainers": [ 13 | { 14 | "name": "Mark Carver", 15 | "url": "https://drupal.org/user/501638" 16 | }, 17 | { 18 | "name": "Scott Reeves (Cottser)", 19 | "url": "https://drupal.org/user/1167326" 20 | }, 21 | { 22 | "name": "Daniel F. Kudwien (sun)", 23 | "url": "https://drupal.org/user/54136" 24 | } 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "git@github.com:unicorn-fail/dreditor.git" 29 | }, 30 | "bugs": "https://github.com/unicorn-fail/dreditor/issues", 31 | "licenses": [ 32 | { 33 | "type": "GPL-2.0", 34 | "url": "https://github.com/unicorn-fail/dreditor/blob/2.x/LICENSE-GPL-2.0" 35 | } 36 | ], 37 | "keywords": [], 38 | "engines": { 39 | "node": ">= 0.8.0" 40 | }, 41 | "scripts": { 42 | "test": "grunt travis-ci --verbose" 43 | }, 44 | "dependencies": { 45 | "clean-css": "2.0.8", 46 | "grunt": "0.4.2", 47 | "grunt-contrib-clean": "0.5.0", 48 | "grunt-contrib-compress": "0.9.1", 49 | "grunt-contrib-concat": "0.4.0", 50 | "grunt-contrib-copy": "0.5.0", 51 | "grunt-contrib-jshint": "0.10.0", 52 | "grunt-contrib-less": "0.11.0", 53 | "grunt-css2js": "0.2.4", 54 | "grunt-sed": "0.1.1", 55 | "load-grunt-tasks": "0.4.0" 56 | }, 57 | "devDependencies": { 58 | "grunt-contrib-uglify": "0.4.0", 59 | "grunt-exec": "0.4.5", 60 | "grunt-mozilla-addon-sdk": "0.3.2" 61 | }, 62 | "optionalDependencies": { 63 | "grunt-contrib-qunit": "0.2.1", 64 | "grunt-contrib-watch": "0.6.1", 65 | "grunt-release": "0.7.0", 66 | "jshint-stylish": "0.2.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unicorn-fail/dreditor/539c8a8ff6b249d1a64fd63756eb7ae1be14e08a/src/icon.png -------------------------------------------------------------------------------- /src/js/_banner.footer.js: -------------------------------------------------------------------------------- 1 | /*jshint ignore:start*/ 2 | // End of Content Scope Runner. 3 | }; 4 | /*jshint ignore:end*/ 5 | 6 | // If not already running in the page, inject this script into the page. 7 | if (typeof __PAGE_SCOPE_RUN__ === 'undefined') { 8 | // Define a closure/function in the global scope in order to reference the 9 | // function caller (the function that executes the user script itself). 10 | (function page_scope_runner() { 11 | // Retrieve the source of dreditor_loader, inject and run. 12 | var self_src = '(' + dreditor_loader.toString() + ')(jQuery);'; 13 | 14 | // Add the source to a new SCRIPT DOM element; prepend it with the 15 | // __PAGE_SCOPE_RUN__ marker. 16 | // Intentionally no scope-wrapping here. 17 | var script = document.createElement('script'); 18 | script.setAttribute('type', 'text/javascript'); 19 | script.textContent = "var __PAGE_SCOPE_RUN__ = true;\n" + self_src; 20 | 21 | // Inject the SCRIPT element into the page. 22 | var head = document.getElementsByTagName('head')[0]; 23 | head.appendChild(script); 24 | })(); 25 | 26 | // End execution. This code path is only reached in a GreaseMonkey/user 27 | // script environment. User script environment implementations differ; not all 28 | // browsers (e.g., Opera) understand a return statement here, and it would 29 | // also prevent inclusion of this script in unit tests. Therefore, the entire 30 | // script needs to be wrapped in a condition. 31 | } 32 | // Drupal is undefined when drupal.org is down. 33 | else if (typeof Drupal === 'undefined') { 34 | } 35 | // Execute the script as part of the content page. 36 | else { 37 | dreditor_loader(jQuery); /*jshint ignore:line*/ 38 | } 39 | -------------------------------------------------------------------------------- /src/js/_banner.header.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Content Scope Runner. 3 | * 4 | * While Firefox/GreaseMonkey supports advanced DOM manipulations, Chrome does 5 | * not. For maximum browser compatibility, this user script injects itself into 6 | * the page it is executed on. 7 | * 8 | * Support and available features for user scripts highly varies across browser 9 | * vendors. Some browsers (e.g., Firefox) require to install a browser extension 10 | * (GreaseMonkey) in order to install and execute user scripts. Some others 11 | * have built-in support for user scripts, but do not support all features of 12 | * GreaseMonkey (variable storage, cross-domain XHR, etc). In the special case 13 | * of Chrome, user scripts are executed before the DOM has been fully loaded and 14 | * initialized; they can only access and manipulate the plain DOM document as 15 | * is, but none of the scripts on the actual page are loaded yet. 16 | * 17 | * Bear in mind, with Content Scope Runner, unsafeWindow and all other 18 | * GreaseMonkey specific features are not available. 19 | * 20 | * The global __PAGE_SCOPE_RUN__ variable is prepended to the user script to 21 | * control execution. Make sure this variable does not clash with actual page 22 | * variables. 23 | * 24 | * @see http://userscripts.org/scripts/show/68059 25 | * @see http://wiki.greasespot.net/Content_Scope_Runner 26 | * 27 | * @todo FIXME upstream: 28 | * - Bogus SCRIPT type attribute. 29 | * - data attribute throws MIME type warning in Chrome; textContent approach 30 | * of earlier versions is correct. 31 | * - Append to HEAD. 32 | * - Removal/clean-up is completely invalid. 33 | * - setTimeout() approach seems useless? 34 | * - Code comments. 35 | */ 36 | /*jshint ignore:start*/ 37 | var dreditor_loader = function ($) { 38 | /*jshint ignore:end*/ 39 | -------------------------------------------------------------------------------- /src/js/dreditor.js: -------------------------------------------------------------------------------- 1 | Drupal.dreditor = { 2 | version: '%PKG.VERSION%', 3 | behaviors: {}, 4 | setup: function () { 5 | var self = this; 6 | 7 | // Reset scroll position. 8 | delete self.scrollTop; 9 | 10 | // Prevent repeated setup (not supported yet). 11 | if (self.$dreditor) { 12 | self.show(); 13 | return; 14 | } 15 | // Setup Dreditor overlay. 16 | self.$wrapper = $('
').css({ height: 0 }); 17 | // Add Dreditor content area. 18 | self.$dreditor = $('
').appendTo(self.$wrapper); 19 | self.$wrapper.appendTo('body'); 20 | 21 | // Setup Dreditor context. 22 | Drupal.dreditor.context = self.$dreditor.get(0); 23 | 24 | // Add sidebar. 25 | var $bar = $('
').prependTo(self.$dreditor); 26 | // Add ul#menu to sidebar by default for convenience. 27 | $('

Diff outline

').appendTo($bar); 28 | $('').appendTo($bar); 29 | 30 | // Allow bar to be resizable. 31 | self.resizable($bar); 32 | 33 | // Add the content region. 34 | $('
').appendTo(self.$dreditor); 35 | 36 | // Add global Dreditor buttons container. 37 | var $actions = $('
'); 38 | // Add hide/show button to temporarily dismiss Dreditor. 39 | $('') 40 | .click(function () { 41 | if (self.visible) { 42 | self.hide(); 43 | } 44 | else { 45 | self.show(); 46 | } 47 | }) 48 | .appendTo($actions); 49 | // Add cancel button to tear down Dreditor. 50 | $('') 51 | .click(function () { 52 | if (Drupal.dreditor.patchReview.comment.comments.length === 0 || window.confirm('Do you really want to cancel Dreditor and discard your changes?')) { 53 | Drupal.dreditor.tearDown(); 54 | } 55 | return; 56 | }) 57 | .appendTo($actions); 58 | $actions.appendTo(self.$dreditor); 59 | 60 | // Allow to hide Dreditor using the ESC key. 61 | $(document).bind('keyup', { dreditor: self }, self.escapeKeyHandler); 62 | 63 | // Setup application. 64 | var args = arguments; 65 | // Cut out the application name (2nd argument). 66 | this.application = Array.prototype.splice.call(args, 1, 1)[0]; 67 | // Remove global window context; new context is added by attachBehaviors(). 68 | args = Array.prototype.slice.call(args, 1); 69 | this.attachBehaviors(args); 70 | 71 | // Display Dreditor. 72 | self.show(); 73 | }, 74 | 75 | resizable: function ($bar) { 76 | var self = this; 77 | var $resizer = $bar.find('.resizer'); 78 | var minWidth = 230; 79 | var maxWidth = self.$dreditor.width() / 2; 80 | var currentWidth = Drupal.storage.load('barWidth') || minWidth; 81 | var resizing = false; 82 | 83 | // Ensure that the maximum width is calculated on window resize. 84 | $(window).bind('resize', function () { 85 | maxWidth = self.$dreditor.width() / 2; 86 | }); 87 | 88 | // Limit widths to minimum and current maximum. 89 | var checkWidth = function (width) { 90 | if (width < minWidth) { 91 | width = minWidth; 92 | } 93 | if (width > maxWidth) { 94 | width = maxWidth; 95 | } 96 | return width; 97 | }; 98 | 99 | // Initialize the current width of the bar. 100 | $bar.width(checkWidth(currentWidth)); 101 | 102 | // Bind the trigger for actually instantiating a resize event. 103 | $resizer 104 | .bind('mousedown', function () { 105 | if (!resizing) { 106 | resizing = true; 107 | $resizer.addClass('resizing'); 108 | self.$dreditor.addClass('resizing'); 109 | } 110 | }); 111 | 112 | // Bind the mouse movements to the entire $dreditor div to accommodate 113 | // fast mouse movements. 114 | self.$dreditor 115 | .bind('mousemove', function (e) { 116 | if (resizing) { 117 | currentWidth = checkWidth(e.clientX); 118 | $bar.width(currentWidth); 119 | } 120 | }) 121 | .bind('mouseup', function () { 122 | if (resizing) { 123 | resizing = false; 124 | $resizer.removeClass('resizing'); 125 | self.$dreditor.removeClass('resizing'); 126 | Drupal.storage.save('barWidth', currentWidth); 127 | } 128 | }); 129 | }, 130 | 131 | tearDown: function (animate) { 132 | animate = typeof animate !== 'undefined' ? animate : true; 133 | var self = this; 134 | 135 | // Remove the ESC keyup event handler that was bound in self.setup(). 136 | $(document).unbind('keyup', self.escapeKeyHandler); 137 | if (animate) { 138 | self.$wrapper.animate({ height: 0 }, 300, function(){ 139 | $(this).hide(); 140 | $('body').css({ overflow: 'auto' }); 141 | }); 142 | setTimeout(function(){ 143 | self.$wrapper.stop(true, true).css('height', 0).remove(); 144 | delete self.$dreditor; 145 | delete self.$wrapper; 146 | }, 500); 147 | } 148 | else { 149 | self.$wrapper.remove(); 150 | delete self.$dreditor; 151 | delete self.$wrapper; 152 | } 153 | }, 154 | 155 | /** 156 | * Dreditor visibility state. 157 | */ 158 | visible: false, 159 | 160 | /** 161 | * Hide Dreditor. 162 | */ 163 | hide: function () { 164 | var self = this; 165 | self.visible = false; 166 | // Backup current vertical scroll position of Dreditor content. 167 | self.scrollTop = self.$dreditor.find('#dreditor-content').scrollTop(); 168 | 169 | var button = self.$dreditor.find('#dreditor-hide').get(0); 170 | button.value = 'Show'; 171 | 172 | self.$wrapper.stop(true).animate({ height: 34 }, function () { 173 | self.$dreditor.find('> div:not(#dreditor-actions)').hide(); 174 | $('body').css({ overflow: 'auto' }); 175 | }); 176 | return false; 177 | }, 178 | 179 | /** 180 | * Show Dreditor. 181 | */ 182 | show: function () { 183 | var self = this; 184 | self.visible = true; 185 | 186 | var button = self.$dreditor.find('#dreditor-hide').get(0); 187 | self.$dreditor.find('> div:not(#dreditor-actions)').show(); 188 | 189 | $('body').css({ overflow: 'hidden' }); 190 | self.$wrapper.stop(true).animate({ height: '100%' }, function () { 191 | button.value = 'Hide'; 192 | }); 193 | 194 | // Restore previous vertical scroll position of Dreditor content. 195 | if (self.scrollTop) { 196 | self.$dreditor.find('#dreditor-content').scrollTop(self.scrollTop); 197 | } 198 | return false; 199 | }, 200 | 201 | /** 202 | * Key event handler to hide or show Dreditor. 203 | */ 204 | escapeKeyHandler: function (event) { 205 | var self = event.data.dreditor; 206 | if (event.which === 27) { 207 | if (self.visible) { 208 | self.hide(); 209 | } 210 | else { 211 | self.show(); 212 | } 213 | } 214 | }, 215 | 216 | attachBehaviors: function (args) { 217 | if (args === undefined || typeof args !== 'object') { 218 | args = []; 219 | } 220 | // Add Dreditor context as first argument. 221 | Array.prototype.unshift.call(args, Drupal.dreditor.context); 222 | // Apply application behaviors, passing any additional arguments. 223 | $.each(Drupal.dreditor[this.application].behaviors, function () { 224 | this.apply(Drupal.dreditor.context, args); 225 | }); 226 | // Apply Dreditor behaviors. 227 | $.each(Drupal.dreditor.behaviors, function () { 228 | this(Drupal.dreditor.context); 229 | }); 230 | // Apply Drupal behaviors. 231 | Drupal.attachBehaviors(Drupal.dreditor.context); 232 | }, 233 | 234 | /** 235 | * Parse CSS classes of a DOM element into parameters. 236 | * 237 | * Required, because jQuery.data() somehow seems to forget about previously 238 | * stored data in DOM elements; most probably due to context mismatches. 239 | * 240 | * Syntax for CSS classes is "-name-value". 241 | * 242 | * @param element 243 | * A DOM element containing CSS classes to parse. 244 | * @param prefix 245 | * The parameter prefix to search for. 246 | */ 247 | getParams: function(element, prefix) { 248 | var classes = element.className.split(' '); 249 | var length = prefix.length; 250 | var params = {}; 251 | for (var i in classes) { 252 | if (classes[i].substr(0, length + 1) === prefix + '-') { 253 | var parts = classes[i].split('-'); 254 | var value = parts.slice(2).join('-'); 255 | params[parts[1]] = value; 256 | // Convert numeric values. 257 | if (parseInt(value, 10) === value) { 258 | params[parts[1]] = parseInt(value, 10); 259 | } 260 | } 261 | } 262 | return params; 263 | }, 264 | 265 | /** 266 | * Jump to a fragment/hash in the document, skipping the browser's history. 267 | * 268 | * To be used for jump links within Dreditor overlay only. 269 | */ 270 | goto: function (selector) { 271 | if (!(typeof selector === 'string' && selector.length)) { 272 | return; 273 | } 274 | // @todo Does not work because of overflow: hidden. 275 | //window.scrollTo(0, $(selector).offset().top); 276 | // Gecko-only method to scroll DOM elements into view. 277 | // @see https://developer.mozilla.org/en/DOM/element.scrollIntoView 278 | var $target = $(selector); 279 | if ($target.length) { 280 | $target.get(0).scrollIntoView(); 281 | } 282 | else if (typeof window.console.warn !== 'undefined') { 283 | window.console.warn(selector + ' does not exist.'); 284 | } 285 | }, 286 | 287 | /** 288 | * Redirect to a given path or the current page. 289 | * 290 | * Avoids hard browser refresh (clearing cache). 291 | * 292 | * @param path 293 | * (optional) The path to redirect to, including leading slash. Defaults to 294 | * current path. 295 | * @param options 296 | * (optional) An object containing: 297 | * - query: A query string to append, including leading question mark 298 | * (window.location.search). Defaults to current query string. 299 | * - fragment: A fragment string to append, including leading pound 300 | * (window.location.hash). Defaults to none. 301 | */ 302 | redirect: function (path, options) { 303 | path = path || window.location.pathname; 304 | options = $.extend({ fragment: '' }, options || {}); 305 | var url = window.location.protocol + '//' + window.location.hostname + path; 306 | // If query is not null, take it; otherwise, use current. 307 | url += (typeof options.query !== 'undefined' ? options.query : window.location.search); 308 | // Not using current fragment by default. 309 | if (options.fragment.length) { 310 | url += options.fragment; 311 | } 312 | window.location.href = url; 313 | return false; 314 | } 315 | }; 316 | -------------------------------------------------------------------------------- /src/js/extensions/cache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mimic of d.o. cache_get cache_set and cache_clear 3 | * 4 | * Note we only have only a key/value store so there is no cache table. 5 | * We store each key/value pair prepending it's key with the cache id. 6 | * - cache_node/1 for node id in the default cache 7 | */ 8 | Drupal.cache = { 9 | /** 10 | * Provide a default value if needed 11 | * 12 | * @param {String} cache 13 | * Cache ID or nothing 14 | * @returns {String} 15 | * Cache ID 16 | */ 17 | getCache : function(cache) { 18 | return cache ? cache : 'cache'; 19 | }, 20 | /** 21 | * The key to use for storage. 22 | * 23 | * @param {String} cache 24 | * @param {String} id 25 | * @returns {String} 26 | */ 27 | getKey : function(cache, id) { 28 | return cache + '_' + id; 29 | }, 30 | /** 31 | * List of key for particular cache. 32 | * 33 | * @param {String} cache 34 | * @returns {Array} 35 | */ 36 | getKeys : function(cache) { 37 | cache = this.getCache(cache); 38 | var keys = Drupal.storage.load(cache); 39 | return keys ? keys : []; 40 | }, 41 | 42 | /** 43 | * Store a key/value pair in a particular cache maybe expirable. 44 | * 45 | * @param {String} id 46 | * @param {any} data 47 | * Data item to store 48 | * @param {String} cache 49 | * Named cache bin 50 | * @param {integer} expire 51 | * Value Data.now() + millisecond or CACHE_PERMANENT === 0 52 | * 53 | * @see https://api.drupal.org/api/drupal/includes!cache.inc/function/cache_set/7 54 | */ 55 | set : function(id, data, cache, expire) { 56 | cache = this.getCache(cache); 57 | expire = expire || 0; 58 | // Prepend key with it's cache 59 | var key = this.getKey(cache, id); 60 | // Grab lookup for comparing keys 61 | var keys = this.getKeys(cache); 62 | if (keys.indexOf(key) === -1) { 63 | keys.push(key); 64 | } 65 | // Save both cachekeys and cachable data @see Drupal.cache 66 | var item = {data: data, expire: expire}; 67 | Drupal.storage.save(key, item); 68 | Drupal.storage.save(cache, keys); 69 | }, 70 | /** 71 | * Get item from particular cache with given id. 72 | * 73 | * @param {String} id 74 | * @param {String} cache 75 | * @returns {any|null} 76 | */ 77 | get : function(id, cache) { 78 | cache = this.getCache(cache); 79 | var keys = this.getKeys(cache); 80 | var key = this.getKey(cache, id); 81 | if (keys.indexOf(key) > -1) { 82 | var item = Drupal.storage.load(key); 83 | if (item.expire === 0 || item.expire > Date.now()) { 84 | return item.data; 85 | } 86 | } 87 | return null; 88 | }, 89 | /** 90 | * Clears the given (or default) cache 91 | * 92 | * @param {String|null} cache 93 | */ 94 | clear : function(cache) { 95 | cache = this.getCache(cache); 96 | var keys = this.getKeys(cache); 97 | // Delete data. 98 | $.each(keys, function(i, value) { 99 | Drupal.storage.remove(value); 100 | }); 101 | // Remove the cache itself. 102 | Drupal.storage.remove(cache); 103 | } 104 | }; 105 | -------------------------------------------------------------------------------- /src/js/extensions/debug.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dreditor debugging helper. 3 | * 4 | * @usage 5 | * $.debug(var [, name]); 6 | * $variable.debug( [name] ); 7 | */ 8 | jQuery.fn.extend({ 9 | debug: function () { 10 | // Initialize window.debug storage, to make debug data accessible later 11 | // (e.g., via browser console). Although we are going to possibly store 12 | // named keys, this needs to be an Array, so we can determine its length. 13 | window.debug = window.debug || []; 14 | 15 | var name, data, args = jQuery.makeArray(arguments); 16 | // Determine data source; this is an object for $variable.debug(). 17 | // Also determine the identifier to store data with. 18 | if (typeof this === 'object') { 19 | name = (args.length ? args[0] : window.debug.length); 20 | data = this; 21 | } 22 | else { 23 | name = (args.length > 1 ? args.pop() : window.debug.length); 24 | data = args[0]; 25 | } 26 | // Store data. 27 | window.debug[name] = data; 28 | // Dump data into Firebug console. 29 | if (typeof window.console !== 'undefined') { 30 | window.console.log(name, data); 31 | } 32 | return this; 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /src/js/extensions/form.js: -------------------------------------------------------------------------------- 1 | Drupal.dreditor.form = { 2 | forms: [], 3 | 4 | create: function (form_id) { 5 | return new this.form(form_id); 6 | } 7 | }; 8 | 9 | Drupal.dreditor.form.form = function (form_id) { 10 | var self = this; 11 | 12 | // Turn this object into a jQuery object, being a form. :) 13 | $.extend(true, self, $('
')); 14 | 15 | // Override the default submit handler. 16 | self.submit(function () { 17 | // Unless proven wrong, we remove the form after submission. 18 | self.remove(); 19 | // We never really submit. 20 | return false; 21 | }); 22 | }; 23 | 24 | Drupal.dreditor.form.form.prototype = { 25 | submitHandlers: {}, 26 | 27 | addButton: function (op, onSubmit) { 28 | var self = this; 29 | self.submitHandlers[op] = onSubmit; 30 | var $button = $(''); 31 | $button.bind('click.form', function () { 32 | self.submitHandlers[op].call(self, $button); 33 | }); 34 | this.append($button); 35 | // Return the jQuery form object to allow for chaining. 36 | return this; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/js/extensions/sort.js: -------------------------------------------------------------------------------- 1 | /** 2 | * sort() callback to sort DOM elements by their actual DOM position. 3 | * 4 | * Copied from jQuery 1.3.2. 5 | * 6 | * @see Drupal.dreditor.patchReview.sort() 7 | */ 8 | var sortOrder, hasDuplicate; 9 | if ( document.documentElement && document.documentElement.compareDocumentPosition ) { 10 | sortOrder = function( a, b ) { 11 | if (a && b && a.compareDocumentPosition) { 12 | var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1; 13 | if ( ret === 0 ) { 14 | hasDuplicate = true; 15 | } 16 | return ret; 17 | } 18 | }; 19 | } else if ( "sourceIndex" in document.documentElement ) { 20 | sortOrder = function( a, b ) { 21 | var ret = a.sourceIndex - b.sourceIndex; 22 | if ( ret === 0 ) { 23 | hasDuplicate = true; 24 | } 25 | return ret; 26 | }; 27 | } else if ( document.createRange ) { 28 | sortOrder = function( a, b ) { 29 | var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange(); 30 | aRange.selectNode(a); 31 | aRange.collapse(true); 32 | bRange.selectNode(b); 33 | bRange.collapse(true); 34 | var ret = aRange.compareBoundaryPoints(window.Range.START_TO_END, bRange); 35 | if ( ret === 0 ) { 36 | hasDuplicate = true; 37 | } 38 | return ret; 39 | }; 40 | } 41 | // end sortOrder 42 | -------------------------------------------------------------------------------- /src/js/extensions/storage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Drupal HTML5 storage handler. 3 | * 4 | * @see https://www.drupal.org/node/65578 5 | */ 6 | Drupal.storage = {}; 7 | 8 | /** 9 | * Checks support for a client-side data storage bin. 10 | * 11 | * @param bin 12 | * The space to store in, one of 'session', 'local', 'global'. 13 | */ 14 | Drupal.storage.isSupported = function (bin) { 15 | try { 16 | return bin + 'Storage' in window && window[bin + 'Storage'] !== null; 17 | } 18 | catch (e) { 19 | return false; 20 | } 21 | }; 22 | 23 | Drupal.storage.support = { 24 | session: Drupal.storage.isSupported('session'), 25 | local: Drupal.storage.isSupported('local'), 26 | global: Drupal.storage.isSupported('global') 27 | }; 28 | 29 | /** 30 | * Loads data from client-side storage. 31 | * 32 | * @param key 33 | * The key name to load stored data from. Automatically prefixed with 34 | * "Dreditor.". 35 | * @param bin 36 | * (optional) A string denoting the storage space to read from. Defaults to 37 | * 'local'. See Drupal.storage.save() for details. 38 | * 39 | * @return {any} 40 | * The data stored or null. 41 | * 42 | * @see Drupal.storage.save() 43 | */ 44 | Drupal.storage.load = function (key, bin) { 45 | if (typeof bin === 'undefined') { 46 | bin = 'local'; 47 | } 48 | if (!Drupal.storage.support[bin]) { 49 | return false; 50 | } 51 | key = 'Dreditor.' + key; 52 | var item = window[bin + 'Storage'].getItem(key); 53 | if (item) { 54 | return window.JSON.parse(item); 55 | } 56 | return null; 57 | }; 58 | 59 | /** 60 | * Stores data on the client-side. 61 | * 62 | * @param key 63 | * The key name to store data under. Automatically prefixed with "Dreditor.". 64 | * Should be further namespaced by module; e.g., for 65 | * "Dreditor.moduleName.settingName" you pass "moduleName.settingName". 66 | * @param data 67 | * The data to store. 68 | * @param bin 69 | * (optional) A string denoting the storage space to store data in: 70 | * - session: Reads from window.sessionStorage. Persists for currently opened 71 | * browser window/tab only. 72 | * - local: Reads from window.localStorage. Stored values are only available 73 | * within the scope of the current host name only. 74 | * - global: Reads from window.globalStorage. 75 | * Defaults to 'local'. 76 | * 77 | * @return {Boolean} 78 | * Indicates saving succeded or not. 79 | * @see Drupal.storage.load() 80 | */ 81 | Drupal.storage.save = function (key, data, bin) { 82 | if (typeof bin === 'undefined') { 83 | bin = 'local'; 84 | } 85 | if (!Drupal.storage.support[bin]) { 86 | return false; 87 | } 88 | key = 'Dreditor.' + key; 89 | window[bin + 'Storage'].setItem(key, window.JSON.stringify(data)); 90 | return true; 91 | }; 92 | 93 | /** 94 | * Delete data from client-side storage. 95 | * 96 | * Called 'remove', since 'delete' is a reserved keyword. 97 | * 98 | * @param key 99 | * The key name to delete. Automatically prefixed with "Drupal.". 100 | * @param bin 101 | * (optional) The storage space name. Defaults to 'session'. 102 | * 103 | * @see Drupal.storage.save() 104 | */ 105 | Drupal.storage.remove = function (key, bin) { 106 | if (typeof bin === 'undefined') { 107 | bin = 'local'; 108 | } 109 | if (!Drupal.storage.support[bin]) { 110 | return false; 111 | } 112 | key = 'Dreditor.' + key; 113 | return window[bin + 'Storage'].removeItem(key); 114 | }; 115 | 116 | /** 117 | * Parses a stored value into its original data type. 118 | * 119 | * HTML5 storage always stores values as strings. This is a "best effort" to 120 | * restore data type sanity. 121 | */ 122 | Drupal.storage.parse = function (val) { 123 | // Convert numbers. 124 | if (/^[0-9.]+$/.test(val)) { 125 | val = parseFloat(val); 126 | } 127 | // Convert booleans. 128 | else if (val === 'true') { 129 | val = true; 130 | } 131 | else if (val === 'false') { 132 | val = false; 133 | } 134 | return val; 135 | }; 136 | 137 | /** 138 | * Serializes a value suitable for client-side (string) storage. 139 | */ 140 | Drupal.storage.serialize = function (val) { 141 | return $.param(val); 142 | }; 143 | 144 | /** 145 | * Unserializes a $.param() string. 146 | * 147 | * Note that this only supports simple values (numbers, booleans, strings) 148 | * and only an one-dimensional (flat) associative configuration object (due to 149 | * limitations of jQuery.param()). 150 | */ 151 | Drupal.storage.unserialize = function (str) { 152 | var obj = {}; 153 | jQuery.each(str.split('&'), function() { 154 | var splitted = this.split('='); 155 | if (splitted.length !== 2) { 156 | return; 157 | } 158 | var key = decodeURIComponent(splitted[0]); 159 | var val = decodeURIComponent(splitted[1].replace(/\+/g, ' ')); 160 | val = Drupal.storage.parse(val); 161 | 162 | // Ignore empty values. 163 | if (typeof val === 'number' || typeof val === 'boolean' || val.length > 0) { 164 | obj[key] = val; 165 | } 166 | }); 167 | return obj; 168 | }; 169 | -------------------------------------------------------------------------------- /src/js/extensions/update.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Checks for Dreditor updates every once in a while. 4 | */ 5 | Drupal.dreditor.updateCheck = function () { 6 | if (window.location.hostname === 'dreditor.org') { 7 | return; 8 | } 9 | // Do not update check for any webkit based browsers, they are extensions and 10 | // are automatically updated. 11 | if (jQuery.browser.webkit) { 12 | return; 13 | } 14 | 15 | var now = new Date(); 16 | // Time of the last update check performed. 17 | var lastUpdateCheck = Drupal.storage.load('lastUpdateCheck'); 18 | 19 | // Do not check for updates if the user just installed Dreditor. 20 | if (lastUpdateCheck === null) { 21 | Drupal.storage.save('lastUpdateCheck', now.getTime()); 22 | return; 23 | } 24 | else { 25 | lastUpdateCheck = new Date(lastUpdateCheck); 26 | } 27 | 28 | // Check whether it is time to check for updates (one a week). 29 | var interval = 1000 * 60 * 60 * 24 * 7; 30 | // Convert to time; JS confuses timezone offset in ISO dates with seconds. 31 | if (lastUpdateCheck.getTime() + interval > now.getTime()) { 32 | return; 33 | } 34 | 35 | // Save that a update check was performed. 36 | // Was previously only saved when the user confirmed or when the commit log 37 | // could not be parsed. But if the user does not confirm (cancels), the update 38 | // would run on every page load again. 39 | Drupal.storage.save('lastUpdateCheck', now.getTime()); 40 | 41 | var latestVersion, installedVersion = Drupal.dreditor.version; 42 | // Determine the latest tagged release from GitHub API. 43 | $.getJSON('https://api.github.com/repos/unicorn-fail/dreditor/tags', function (json) { 44 | for (var i = 0; i < json.length; i++) { 45 | // Find the latest stable release (no "rc", "beta" or "dev" releases). 46 | if (json[i].name.indexOf('rc') === -1 && json[i].name.indexOf('beta') === -1 && json[i].name.indexOf('dev') === -1) { 47 | latestVersion = json[i].name; 48 | break; 49 | } 50 | } 51 | if (latestVersion > installedVersion) { 52 | if (window.confirm('A new version of Dreditor is available: ' + latestVersion + '. Your current installed version of Dreditor is: ' + installedVersion + '. Would you like to visit https://dreditor.org and update?')) { 53 | window.open('https://dreditor.org', 'dreditor'); 54 | } 55 | } 56 | if (window.console) { 57 | window.console.log('Installed Dreditor version: ' + installedVersion); 58 | window.console.log('Latest Dreditor version: ' + latestVersion); 59 | } 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /src/js/init.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Initialize Dreditor. 3 | */ 4 | // Enable detection of installed chrome extension on dreditor.org. 5 | if (window.location.href.match('dreditor.org')) { 6 | var isInstalledNode = document.createElement('div'); 7 | isInstalledNode.id = 'dreditor-is-installed'; 8 | document.body.appendChild(isInstalledNode); 9 | } 10 | 11 | jQuery(document).ready(function () { 12 | Drupal.attachBehaviors(this); 13 | }); 14 | 15 | // Invoke Dreditor update check once. 16 | Drupal.dreditor.updateCheck(); 17 | -------------------------------------------------------------------------------- /src/js/plugins/comment.number.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Exposes the predicted number for a new comment. 3 | * 4 | * The (sequential) comment number is commonly used as suffix in patch names. 5 | * This plugin simply exposes the predicted new comment number in the 6 | * "Add new comment" heading of the issue comment/update form (block), so that 7 | * contributors do not have to manually make the math. 8 | */ 9 | Drupal.behaviors.dreditorCommentNumber = { 10 | attach: function (context) { 11 | $(context).find('#project-issue-ajax-form h2:first') 12 | .append(' #' + Drupal.dreditor.issue.getNewCommentNumber() + ''); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/js/plugins/form.backup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Backs up form values before submit for potential later restore. 3 | * 4 | * drupal.org's advanced infrastructure may respond with totally bogus things 5 | * like HTTP redirects to completely invalid locations. Native support for 6 | * retaining previously posted form values in modern browsers is entirely 7 | * hi-jacked in those cases; the browser doesn't even know anymore that it 8 | * posted something. 9 | */ 10 | Drupal.behaviors.dreditorFormBackup = { 11 | attach: function (context) { 12 | var self = this; 13 | // Skip HTTP GET forms and exclude all search forms (some are using POST). 14 | $(context).find('form:not([method~="GET"]):not([id*="search"])').once('dreditor-form-backup', function () { 15 | var $form = $(this); 16 | var form_id = $form.find('[name="form_id"]').val(); 17 | 18 | // Back up the current input whenever the form is submitted. 19 | $form.bind('submit.dreditor.formBackup', function () { 20 | Drupal.storage.save('form.backup.' + form_id, $form.find('input:not([type="password"]), textarea, select').serialize()); 21 | }); 22 | 23 | // Determine whether there is input that can be restored. 24 | var lastValues = Drupal.storage.load('form.backup.' + form_id); 25 | if (!lastValues) { 26 | return; 27 | } 28 | var $button = $('Restore last input'); 29 | $button.bind('click', function (e) { 30 | e.preventDefault(); 31 | if (window.confirm('Reset this form to your last submitted values?')) { 32 | self.restore($form, Drupal.storage.unserialize(lastValues)); 33 | // Remove the button. 34 | $(this).fadeOut(); 35 | } 36 | }); 37 | $button.appendTo($form.find('.form-actions:last')); 38 | }); 39 | }, 40 | restore: function ($form, values) { 41 | $form.find('[name]').not('[type=hidden]').each(function () { 42 | if (typeof values[this.name] !== 'undefined') { 43 | $(this).val(values[this.name]); 44 | } 45 | }); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/js/plugins/form.sticky.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows to make a form widget sticky. 3 | * 4 | * On an issue with many follow-ups, one needs to jump back and forth between 5 | * the comment form and individual earlier comments you want to reply to. 6 | * 7 | * To prevent that, allow to make a form widget sticky, so the user is able to 8 | * read, scroll, and comment at the same time. 9 | */ 10 | Drupal.behaviors.dreditorFormSticky = { 11 | attach: function (context) { 12 | var self = this; 13 | // Comment body textarea form item. 14 | $(context).find('#edit-nodechanges-comment .form-type-textarea').once('dreditor-form-sticky', function () { 15 | self.addButton($(this).find('.form-textarea-wrapper')); 16 | }); 17 | // Issue summary body form item. 18 | // Use the entire form item for the issue summary, so as to include the 19 | // issue summary template button. 20 | $(context).find('#project-issue-node-form .form-item-body-und-0-value').once('dreditor-form-sticky', function () { 21 | self.addButton($(this)); 22 | }); 23 | }, 24 | 25 | addButton: function ($wrapper) { 26 | if ($wrapper.attr('id')) { 27 | return; 28 | } 29 | var $toggle = $('Make sticky'); 30 | $toggle.bind('click', function (e) { 31 | e.preventDefault(); 32 | if ($wrapper.attr('id') === 'dreditor-widget') { 33 | $wrapper.removeAttr('id'); 34 | $toggle.removeClass('sticky-cancel active').text('Make sticky'); 35 | } 36 | else if (!$wrapper.attr('id') && !$('#dreditor-widget').length) { 37 | $wrapper.attr('id', 'dreditor-widget'); 38 | $toggle.addClass('sticky-cancel active').text('Unstick'); 39 | } 40 | }); 41 | $wrapper.prepend($toggle); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/js/plugins/inline.image.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Attach image attachment inline HTML injector to file attachments. 3 | */ 4 | Drupal.behaviors.dreditorInlineImage = { 5 | attach: function (context) { 6 | var $context = $(context); 7 | 8 | // Collect all the textareas we can put HTML into. 9 | var $textareas = $('textarea.text-full'); 10 | 11 | // Keep track of last textarea in focus. 12 | var $target = $textareas.last(); 13 | $textareas.bind('focus', function () { 14 | $target = $(this); 15 | }); 16 | 17 | // @todo .file clashes with patchReviewer tr.file + a.file markup. 18 | $context.find('span.file').once('dreditor-inlineimage').find('> a').each(function () { 19 | var $link = $(this); 20 | 21 | // Remove protocol + drupal.org 22 | var url = $link.attr('href').replace(/^https\:\/\/(?:www\.)?drupal\.org/, ''); 23 | 24 | // Only process image attachments. 25 | if (!url.match(/\.png$|\.jpg$|\.jpeg$|\.gif$/)) { 26 | return; 27 | } 28 | 29 | // Generate inline image button (cannot be , other scripts bind links). 30 | var $button = $('Embed'); 31 | 32 | // Append inline image button to attachment. 33 | $link.parent().prepend($button); 34 | 35 | // Override click event. 36 | $button 37 | .bind('click', function (e) { 38 | if (!$target.length) { 39 | // Well we tried, guess the page doesn't have the textareas we want. 40 | return; 41 | } 42 | 43 | // Focus comment textarea. 44 | $('html, body').animate({ 45 | scrollTop: $target.offset().top 46 | }, 300); 47 | // Insert image tag to URL in comment textarea. 48 | $target.focus().val($target.val() + "\n\"\"\n"); 49 | e.preventDefault(); 50 | }); 51 | }); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/js/plugins/issue.clone.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Attach clone issue button to issues. 3 | */ 4 | Drupal.behaviors.dreditorIssueClone = { 5 | attach: function (context) { 6 | var _window = window; 7 | var $context = $(context); 8 | $context.find('body.node-type-project-issue:not(.page-node-edit)').once('dreditor-clone-button', function () { 9 | $('
  • ') 10 | .appendTo($context.find('#tabs ul')) 11 | .find('button') 12 | .bind('click.dreditor-clone', function () { 13 | // Retrieve the current issue's project shortname. 14 | var project = /[^/]*$/.exec($('.breadcrumb').find('a').attr('href'))[0]; 15 | 16 | // Open a new window. 17 | var w = _window.open('/node/add/project-issue/' + project + '#project-issue-node-form', '_blank'); 18 | // @todo Revisit this once Dreditor no longer depends on d.o's jQuery. 19 | // $(w).bind('load') does not actually bind to the new window "load" 20 | // event. This may be on purpose or a bug with the currently used 21 | // jQuery version on d.o (1.4.4). 22 | w.addEventListener('load', function () { 23 | // Retrieve the DOM of the newly created window. 24 | var $document = $(w.document); 25 | $document.ready(function () { 26 | var parentNid = Drupal.dreditor.issue.getNid(); 27 | var $parentForm = $context.find('#project-issue-node-form'); 28 | var $newForm = $document.contents().find('#project-issue-node-form'); 29 | var selector, selectors = [ 30 | '#edit-title', 31 | '#edit-body-und-0-value', 32 | '#edit-field-issue-category-und', 33 | '#edit-field-issue-priority-und', 34 | '#edit-field-issue-status-und', 35 | '#edit-field-issue-version-und', 36 | '#edit-field-issue-component-und', 37 | '#edit-field-issue-assigned-und', 38 | '#edit-taxonomy-vocabulary-9-und' 39 | ]; 40 | for (selector in selectors) { 41 | $newForm.find(selectors[selector]).val($parentForm.find(selectors[selector]).val()); 42 | } 43 | 44 | // Prepend body with "Follow-up to ..." line. 45 | var $body = $newForm.find('#edit-body-und-0-value'); 46 | $body.val('Follow-up to [#' + parentNid + ']\n\n' + $body.val()); 47 | 48 | // Add originating issue was parent issue relationship. 49 | $newForm.find('#edit-field-issue-parent-und-0-target-id') 50 | .val($parentForm.find('#edit-title').val() + ' (' + parentNid + ')'); 51 | 52 | 53 | // Ensure all fieldsets are expanded. 54 | $newForm.find('.collapsed').removeClass('collapsed'); 55 | 56 | // Focus on the new issue title so users can enter it. 57 | $newForm.find('#edit-title').focus(); 58 | }); 59 | }, false); 60 | }); 61 | }); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/js/plugins/issue.count.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Attach issue count to project issue tables and hide fixed/needs more info issues without update marker. 3 | */ 4 | Drupal.behaviors.dreditorIssueCount = { 5 | attach: function (context) { 6 | $('table.project-issue', context).once('dreditor-issuecount', function () { 7 | var $table = $(this); 8 | var countTotal = $table.find('tbody tr').length; 9 | var countSuffix = ($table.parent().parent().find('.pager').length ? '+' : ''); 10 | var countHidden = 0; 11 | 12 | var $container = $('
    '); 13 | $table.before($container); 14 | 15 | // Add link to toggle this feature. 16 | var enabled = Drupal.storage.load('issuecount.status'); 17 | $('
    ') 18 | .text(enabled ? 'Show all issues' : 'Hide irrelevant issues') 19 | .click(function () { 20 | Drupal.storage.save('issuecount.status', !enabled); 21 | // Reload the current page without refresh from server. 22 | window.location.href = window.location.href; 23 | return false; 24 | }) 25 | .prependTo($container); 26 | 27 | if (enabled) { 28 | countHidden = $table.find('tr.state-2, tr.state-16').not(':has(.marker)').addClass('dreditor-issue-hidden').hide().length; 29 | } 30 | 31 | // Output optimized count (minus hidden). 32 | // Separate calculation required, or otherwise some browsers output NaN. 33 | var count = countTotal - countHidden; 34 | $container.append('Displaying ' + count + '' + countSuffix + ' issues.'); 35 | if (!countHidden) { 36 | return; 37 | } 38 | var $counter = $container.find('span.dreditor-issuecount-total span.count'); 39 | 40 | // Output 'fixed' count. 41 | var $issuesFixed = $table.find('tr.state-2.dreditor-issue-hidden'); 42 | if ($issuesFixed.length) { 43 | $('' + $issuesFixed.length + ' fixed issues.' + '') 44 | .click(function () { 45 | $issuesFixed.removeClass('dreditor-issue-hidden').show(); 46 | $counter.text(parseInt($counter.text(), 10) + $issuesFixed.length); 47 | $(this).remove(); 48 | return false; 49 | }) 50 | .appendTo($container); 51 | } 52 | 53 | // Output 'needs more info' count. 54 | var $issuesInfo = $table.find('tr.state-16.dreditor-issue-hidden'); 55 | if ($issuesInfo.length) { 56 | $('' + $issuesInfo.length + ' issues need more info.' + '') 57 | .click(function () { 58 | $issuesInfo.removeClass('dreditor-issue-hidden').show(); 59 | $counter.text(parseInt($counter.text(), 10) + $issuesInfo.length); 60 | $(this).remove(); 61 | return false; 62 | }) 63 | .appendTo($container); 64 | } 65 | }); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /src/js/plugins/issue.js: -------------------------------------------------------------------------------- 1 | Drupal.dreditor.issue = {}; 2 | /** 3 | * Gets the issue node id. 4 | */ 5 | Drupal.dreditor.issue.getNid = function() { 6 | var href = $('#tabs a:first').attr('href'); 7 | if (href.length) { 8 | return href.match(/(?:node|comment\/reply)\/(\d+)/)[1]; 9 | } 10 | return false; 11 | }; 12 | 13 | /** 14 | * Returns the next comment number for the current issue. 15 | */ 16 | Drupal.dreditor.issue.getNewCommentNumber = function() { 17 | // Get comment count. 18 | var lastCommentNumber = $('.comments div.comment:last .permalink').text().match(/\d+$/); 19 | return (lastCommentNumber ? parseInt(lastCommentNumber[0], 10) : 0) + 1; 20 | }; 21 | 22 | /** 23 | * Gets the issue title. 24 | */ 25 | Drupal.dreditor.issue.getIssueTitle = function() { 26 | var title = $('#page-subtitle').text() || ''; 27 | return title; 28 | }; 29 | 30 | /** 31 | * Gets the project shortname. 32 | * 33 | * @return 34 | * Return false when using the preview mode since the breadcrumb is not 35 | * included in the preview mode. 36 | */ 37 | Drupal.dreditor.issue.getProjectShortName = function() { 38 | 39 | // Retreive project from breadcrumb. 40 | var project = $('.breadcrumb a:eq(0)').attr('href'); 41 | 42 | // @todo The comment preview page does not contain a breadcrumb and also 43 | // does not expose the project name anywhere else. 44 | if (project) { 45 | // The Drupal (core) project breadcrumb does not contain a project page link. 46 | if (project === '/project/issues/drupal') { 47 | project = 'drupal'; 48 | } 49 | else { 50 | project = project.substr(9); 51 | } 52 | } 53 | else { 54 | project = false; 55 | } 56 | 57 | return project; 58 | }; 59 | 60 | Drupal.dreditor.issue.getSelectedComponent = function() { 61 | // Retrieve component from the comment form selected option label. 62 | var version = $(':input[name*="issue_component"] :selected').text(); 63 | return version; 64 | }; 65 | 66 | /** 67 | * Gets the selected version. 68 | * 69 | * Variations: 70 | * 7.x 71 | * 7.x-dev 72 | * 7.x-alpha1 73 | * 7.20 74 | * 7.x-1.x 75 | * 7.x-1.12 76 | * 7.x-1.x 77 | * - 8.x issues - 78 | * - Any - 79 | * All-versions-4.x-dev 80 | */ 81 | Drupal.dreditor.issue.getSelectedVersion = function() { 82 | // Retrieve version from the comment form selected option label. 83 | var version = $(':input[name*="issue_version"] :selected').text(); 84 | return version; 85 | }; 86 | 87 | /** 88 | * Gets the selected core version. 89 | * 90 | * Variations: 91 | * 7.x 92 | * 7.20 93 | */ 94 | Drupal.dreditor.issue.getSelectedVersionCore = function() { 95 | var version = Drupal.dreditor.issue.getSelectedVersion(); 96 | var matches = version.match(/^(\d+\.[x\d]+)/); 97 | if (matches) { 98 | return matches[0]; 99 | } 100 | else { 101 | return false; 102 | } 103 | }; 104 | 105 | /** 106 | * Gets the selected contrib version. 107 | * 108 | * Variations: 109 | * 1.x 110 | * 1.2 111 | */ 112 | Drupal.dreditor.issue.getSelectedVersionContrib = function() { 113 | var version = Drupal.dreditor.issue.getSelectedVersion(); 114 | var matches = version.match(/^\d+\.x-(\d+\.[x\d]+)/); 115 | if (matches) { 116 | return matches[1]; 117 | } 118 | else { 119 | return false; 120 | } 121 | }; 122 | 123 | /** 124 | * Gets the selected core + contrib version. 125 | * 126 | * Variations: 127 | * 7.x-1.x 128 | * 7.x-1.2 129 | */ 130 | Drupal.dreditor.issue.getSelectedVersionCoreContrib = function() { 131 | var version = Drupal.dreditor.issue.getSelectedVersion(); 132 | var matches = version.match(/^(\d+\.x-\d+\.[x\d]+)/); 133 | if (matches) { 134 | return matches[0]; 135 | } 136 | else { 137 | return false; 138 | } 139 | }; 140 | -------------------------------------------------------------------------------- /src/js/plugins/issue.markasread.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Attach mark as read to project issue tables. 3 | */ 4 | Drupal.behaviors.dreditorIssueMarkAsRead = { 5 | attach: function (context) { 6 | $('table.project-issue', context).once('dreditor-issuemarkasread', function () { 7 | var throbber = '
     
    '; 8 | $(throbber).appendTo(this).hide(); 9 | 10 | // 'a + .marker' accounts for a d.o bug; the HTML markup contains two 11 | // span.marker elements, the second being nested inside the first. 12 | var $markers = $(this).find('a + .marker').addClass('clickable'); 13 | 14 | var $markAll = $('Mark all as read') 15 | .click(function (e) { 16 | $(this).append(throbber); 17 | $markers.trigger('click.dreditor-markasread'); 18 | e.preventDefault(); 19 | e.stopPropagation(); 20 | }); 21 | if ($markers.length) { 22 | $markAll.prependTo($(this).parent()); 23 | } 24 | 25 | $markers.bind('click.dreditor-markasread', function () { 26 | var $marker = $(this); 27 | $marker.append(throbber); 28 | var $link = $marker.prev('a'); 29 | $.ajax({ 30 | // The actual HTML page output is irrelevant, so denote that by using 31 | // the appropriate HTTP method. 32 | type: 'HEAD', 33 | url: $link.attr('href'), 34 | complete: function () { 35 | $markers = $markers.not($marker); 36 | if (!$markers.length) { 37 | $markAll.remove(); 38 | } 39 | $marker.remove(); 40 | } 41 | }); 42 | }); 43 | }); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/js/plugins/issue.summary.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Issue summary AJAX editor. 3 | */ 4 | Drupal.behaviors.dreditorIssueSummary = { 5 | attach: function () { 6 | // Limit to project_issue node view page. 7 | $('#project-summary-container').once('dreditor-issue-summary', function () { 8 | // Clone "Edit" link after "Issue summary" title. 9 | var $edit_wrapper = $(' [ ] '); 10 | var $edit_link = $('#tabs a:contains("' + 'Edit' + '")').clone(); 11 | $edit_wrapper.find('span').append($edit_link); 12 | $edit_wrapper.appendTo($(this).parent().find('h2:first')); 13 | 14 | var $widget = $('
    ').insertAfter(this).hide(); 15 | 16 | $edit_link.click(function () { 17 | // First of all, remove this link. 18 | $edit_wrapper.remove(); 19 | // Retrieve the node edit form. 20 | $.get(this.href, function (data) { 21 | var $data = $(data); 22 | // Do power users really need this advise? Investigate this. 23 | // $widget.append($data.find('div.help')); 24 | $widget.append($data.find('#node-form')); 25 | 26 | // For users with just one input format, wrap filter tips in a fieldset. 27 | // @todo Abstract this into a behavior. Also applies to comment form. 28 | $widget.find('fieldset > ul.tips') 29 | .wrap('') 30 | .before('Input format'); 31 | // Clean up. 32 | // Remove messages; contains needless info. 33 | $widget.find('div.messages.status').remove(); 34 | // That info about issue fields in .standard .standard thingy, too. 35 | $widget.find('div.node-form > div.standard > div.standard').remove(); 36 | // Hide node admin fieldsets; removing these would result in nodes being 37 | // unpublished and author being changed to Anonymous on submit. 38 | $widget.find('div.admin').hide(); 39 | 40 | // Flatten issue summary, input format, and revision info fielsets. 41 | // Blatantly remove all other fieldsets. :) 42 | $widget.find('fieldset') 43 | .not(':has(#edit-body, .tips, #edit-log)') 44 | .removeClass('collapsible').hide(); 45 | // Visually remove top-level fieldsets, except text format. 46 | $widget.find('fieldset:has(#edit-body, #edit-log)') 47 | .removeClass('collapsible').addClass('fieldset-flat'); 48 | // Remove needless spacing between summary and revision elements. 49 | $widget.find('.fieldset-flat:eq(0)').css('marginBottom', 0); 50 | 51 | // Hide revision checkbox (only visible for admins, can't be disabled) 52 | // and revision log message description. 53 | $widget.find('#edit-revision-wrapper, #edit-log-wrapper .description').hide(); 54 | // Convert revision log message textarea into textfield and prepopulate it. 55 | var $textarea = $widget.find('#edit-log'); 56 | var $textfield = $(''); 57 | $.each($textarea[0].attributes, function (index, attr) { 58 | $textfield.attr(attr.name, attr.value); 59 | }); 60 | // Enforced log message doesn't really make sense for power users. 61 | // We're not crafting an encyclopedia with issues. 62 | $textfield.val('Updated issue summary.'); 63 | $textarea.replaceWith($textfield); 64 | 65 | // Remove "Preview changes" and "Delete" buttons. 66 | $widget.find('#edit-preview-changes').remove(); 67 | $widget.find('#edit-delete').remove(); 68 | // Sorry, no support for "Preview" yet. 69 | $widget.find('#edit-preview').remove(); 70 | 71 | // Add a Cancel button. Move it far away from the submit button. ;) 72 | $widget.find('#edit-submit').before( 73 | $('Cancel').click(function () { 74 | $widget.slideUp('fast', function () { 75 | $widget.remove(); 76 | }); 77 | return false; 78 | }) 79 | ); 80 | 81 | // Lastly, attach behaviors and slide in. 82 | Drupal.attachBehaviors($widget.get(0)); 83 | $widget.slideDown(); 84 | }, 'html'); 85 | return false; 86 | }); 87 | }); 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /src/js/plugins/issue.summary.template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adds a button to insert the issue summary template. 3 | */ 4 | Drupal.behaviors.dreditorIssueSummaryTemplate = { 5 | attach: function () { 6 | var self = this; 7 | $('body.logged-in.page-node form.node-project_issue-form textarea[name="body[und][0][value]"]').once('dreditorIssueTemplate', function () { 8 | var $textarea = $(this); 9 | var $label = $('label[for*="edit-body-und-0-value"]'); 10 | 11 | // Add a link to issue summary instructions. 12 | $('instructions') 13 | .appendTo($label); 14 | 15 | // Add a button to insert issue summary template. 16 | $('Insert template') 17 | .appendTo($label) 18 | .bind('click', function (e) { 19 | e.preventDefault(); 20 | self.insertSummaryTemplate($textarea); 21 | }); 22 | 23 | // Add a button to insert tasks. 24 | $('Insert tasks') 25 | .appendTo($label) 26 | .bind('click', function (e) { 27 | e.preventDefault(); 28 | self.insertTasks($textarea); 29 | }); 30 | }); 31 | }, 32 | insertSummaryTemplate: function ($textarea) { 33 | $.get('/node/1326662', function (data) { 34 | // Retrieve the template. 35 | var $template = $('
    ').html($(data).find('#node-1326662 code').text()); 36 | 37 | // On node/add, remove the "Original report by" section. 38 | if (location.href.search('node/add') !== -1) { 39 | $template.find('#summary-original-report').remove(); 40 | } 41 | // On node view, simply replace @username with the existing link to the 42 | // original author. 43 | else if (!location.href.match(/^.*node\/[^\/]*\/edit/)) { 44 | var $profileLink = $('.node .submitted a.username').clone(); 45 | if ($profileLink.length) { 46 | $profileLink.text('@' + $profileLink.text()); 47 | } 48 | else { 49 | $profileLink = $('').text('Anonymous').attr('href', '#'); 50 | } 51 | $template.find('#summary-original-report a').replaceWith($('
    ').html($profileLink).html()); 52 | } 53 | // On node edit, the node author is only visible for privileged users. 54 | // Retrieve the author from the issue's JSON data. 55 | // @todo Update when JSON data is available, or find a better solution. 56 | // else { 57 | // var nodePath = location.href.match(/^.*node\/[0-9]*/); 58 | // if (nodePath) { 59 | // $.getJSON(nodePath[0] + '/project-issue/json', function (json) { 60 | // var $profileLink; 61 | // var $bodyVal = $('
    ').html($textarea.val()); 62 | // if (!json.authorId || !json.authorName || !json.authorUrl) { 63 | // $profileLink = $('').text('Anonymous').attr('href', '#'); 64 | // } 65 | // else { 66 | // $profileLink = $('').text('@' + json.authorName).attr('href', json.authorUrl); 67 | // } 68 | // $bodyVal.find('#summary-original-report a').replaceWith($('
    ').html($profileLink).html()); 69 | // $textarea.val($bodyVal.html()); 70 | // }); 71 | // } 72 | // } 73 | 74 | var template = $template.html() 75 | .replace(/<\/em>/g, "\n\n") 76 | .replace(/<\/h3>/g, "\n\n"); 77 | 78 | // Prepend template to current body. 79 | $textarea.val(template + $textarea.val()); 80 | }); 81 | }, 82 | insertTasks: function ($textarea) { 83 | $.get('/node/2272209', function (data) { 84 | // Retrieve the template. 85 | var $template = $('
    ').html($(data).find('#node-2272209 code').text()); 86 | 87 | // Add missing newlines. 88 | var template = $template.html() 89 | .replace(/-->/g, "-->\n\n"); 90 | 91 | // Insert the template at the cursor if possible. 92 | var pos = $textarea[0].selectionStart; 93 | var bodyValue = $textarea.val(); 94 | $textarea.val(bodyValue.substring(0, pos) + template + bodyValue.substring(pos)); 95 | }); 96 | } 97 | }; 98 | -------------------------------------------------------------------------------- /src/js/plugins/issues.filter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cleans up views exposed filter form values before the filter form is submitted. 3 | * 4 | * The purpose is that only non-default views filters are contained in the 5 | * resulting GET query parameters. Better and cleaner for sharing links to a 6 | * certain filtered issue queue result. 7 | * 8 | * Input elements (except multiple selects) always serialize into an empty 9 | * string, so the entire element needs to be disabled. 10 | */ 11 | Drupal.behaviors.dreditorIssuesFilterFormValuesClean = { 12 | attach: function (context) { 13 | $('.view-filters form', context).once('dreditor-issues-form-values-clean', function () { 14 | $(this).submit(function (event) { 15 | $.each(event.target.elements, function (index, element) { 16 | var $element = $(element); 17 | var value = $element.val(); 18 | switch (element.name) { 19 | case 'text': 20 | case 'assigned': 21 | case 'submitted': 22 | case 'participant': 23 | case 'issue_tags': 24 | if (value === '') { 25 | element.disabled = true; 26 | } 27 | break; 28 | 29 | case 'status': 30 | if (value === 'Open') { 31 | element.disabled = true; 32 | } 33 | break; 34 | 35 | case 'priorities': 36 | case 'categories': 37 | case 'version': 38 | case 'component': 39 | if (value === 'All') { 40 | element.disabled = true; 41 | } 42 | break; 43 | 44 | case 'issue_tags_op': 45 | if (value === 'or') { 46 | element.disabled = true; 47 | } 48 | break; 49 | } 50 | }); 51 | }); 52 | }); 53 | } 54 | }; 55 | 56 | /** 57 | * Add a 'Reset' button to project issue exposed views filter form. 58 | */ 59 | Drupal.behaviors.dreditorIssuesFilterFormReset = { 60 | attach: function (context) { 61 | if (!window.location.search) { 62 | return; 63 | } 64 | $('.view-filters form', context).once('dreditor-issues-form-reset', function () { 65 | var $form = $(this); 66 | var $container = $form.find('input.form-submit').parent(); 67 | var $button = $container.clone().find('input').val('Reset').click(function () { 68 | // Reload the current page without query string and without refresh. 69 | Drupal.dreditor.redirect(null, { query: '' }); 70 | return false; 71 | }).end(); 72 | $container.after($button); 73 | }); 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /src/js/plugins/patch.name.suggestion.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Suggest a filename for patches to upload in an issue. 3 | */ 4 | Drupal.behaviors.dreditorPatchNameSuggestion = { 5 | attach: function (context) { 6 | // Attach this behavior only to project_issue nodes. Use a fast selector for 7 | // the common case, but also support comment/reply/% pages. 8 | if (!($('body.node-type-project-issue', context).length || $('div.project-issue', context).length)) { 9 | return; 10 | } 11 | 12 | $('#project-issue-ajax-form .field-name-field-issue-files .form-type-managed-file', context).once('dreditor-patchsuggestion', function () { 13 | var $container = $('> label', this); 14 | var $link = $('Patchname suggestion'); 15 | $link.prependTo($container); 16 | $link.click(function() { 17 | var patchName = ''; 18 | 19 | function truncateString (str, n,useWordBoundary){ 20 | var toLong = str.length>n, 21 | s_ = toLong ? str.substr(0,n-1) : str; 22 | return useWordBoundary && toLong ? s_.substr(0,s_.lastIndexOf(' ')) : s_; 23 | } 24 | 25 | var title = truncateString(Drupal.dreditor.issue.getIssueTitle() || '', 25, true); 26 | 27 | // Truncate and remove a heading/trailing underscore. 28 | patchName += title.replace(/[^a-zA-Z0-9]+/g, '_').replace(/(^_|_$)/, '').toLowerCase(); 29 | 30 | var nid = Drupal.dreditor.issue.getNid() || 0; 31 | if (nid !== 0) { 32 | patchName += (patchName.length ? '-' : '') + nid; 33 | } 34 | patchName += '-' + Drupal.dreditor.issue.getNewCommentNumber(); 35 | patchName += '.patch'; 36 | 37 | window.prompt("Please use this value", patchName); 38 | return false; 39 | }); 40 | }); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/js/plugins/patch.review.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Attach patch review editor to issue attachments. 3 | */ 4 | Drupal.behaviors.dreditorPatchReview = { 5 | attach: function (context) { 6 | var $context = $(context); 7 | // Prevent users from starting to review patches when not logged in. 8 | if (!$context.find('#project-issue-ajax-form').length) { 9 | return; 10 | } 11 | var $elements = $context.find('.file').once('dreditor-patchreview').find('> a'); 12 | $elements.each(function () { 13 | if (this.href.match(/\.(patch|diff|txt)$/)) { 14 | // Generate review link. 15 | var $file = $(this).closest('tr').find('.file'); 16 | var $link = $('Review').click(function (e) { 17 | if (Drupal.dreditor.link !== this && Drupal.dreditor.$wrapper) { 18 | Drupal.dreditor.tearDown(false); 19 | } 20 | if (Drupal.dreditor.link === this && Drupal.dreditor.$wrapper) { 21 | Drupal.dreditor.show(); 22 | } 23 | else { 24 | Drupal.dreditor.link = this; 25 | // Load file. 26 | $.get(this.href, function (content, status) { 27 | if (status === 'success') { 28 | // Invoke Dreditor. 29 | Drupal.dreditor.setup(context, 'patchReview', content); 30 | } 31 | }); 32 | } 33 | e.preventDefault(); 34 | }); 35 | // Append review link to parent table cell. 36 | $link.prependTo($file); 37 | 38 | // Generate simplytest.me links only for patches and diffs. 39 | if (this.href.substr(-6) === '.patch' || this.href.substr(-5) === '.diff') { 40 | // Retrieve project shortname. 41 | var project = Drupal.dreditor.issue.getProjectShortName(); 42 | if (project) { 43 | var version = Drupal.dreditor.issue.getSelectedVersion().replace('-dev', ''); 44 | if (version) { 45 | $('').text('simplytest.me').attr({ 46 | class: 'dreditor-button dreditor-patchtest', 47 | href: 'http://simplytest.me/project/' + project + '/' + version + '?patch[]=' + this.href, 48 | target: '_blank' 49 | }).prependTo($file); 50 | } 51 | } 52 | } 53 | } 54 | }); 55 | } 56 | }; 57 | /** 58 | * Dreditor patchReview application. 59 | * 60 | * This is two-fold: 61 | * - Drupal.dreditor.patchReview: Handles selections and storage/retrieval of 62 | * temporary comment data. 63 | * - Drupal.dreditor.patchReview.comment: An API to load/save/delete permanent 64 | * comments being attached to code lines. 65 | */ 66 | Drupal.dreditor.patchReview = { 67 | /** 68 | * patchReview behaviors stack. 69 | */ 70 | behaviors: {}, 71 | 72 | /** 73 | * Current selection jQuery DOM element stack. 74 | */ 75 | data: { 76 | elements: [] 77 | }, 78 | 79 | reset: function () { 80 | // Reset currently stored selection data. 81 | $(this.data.elements).removeClass('selected'); 82 | this.data = { elements: [] }; 83 | // Remove and delete pastie form. 84 | if (this.$form) { 85 | this.$form.remove(); 86 | delete this.$form; 87 | } 88 | }, 89 | 90 | /** 91 | * Load data into selection storage. 92 | */ 93 | load: function (data) { 94 | // Do not overwrite other comment data; also works for the undefined case. 95 | if (this.data.id !== data.id) { 96 | this.reset(); 97 | } 98 | this.data = data; 99 | }, 100 | 101 | /** 102 | * Add elements to current selection storage. 103 | * 104 | * $.unique() invoked via $.add() fails to apply and identify an existing 105 | * DOM element id (which is internally done via $.data()). Additionally, === 106 | * in $.inArray() fails to identify DOM elements coming from .getSelection(), 107 | * which are already in our stack. Hence, we need custom code to merge DOM 108 | * elements of a new selection into our stack. 109 | * 110 | * After merging, all elements in the stack are re-ordered by their actual 111 | * DOM position. 112 | */ 113 | add: function (elements) { 114 | if (!elements.length) { 115 | return elements; 116 | } 117 | // Merge new elements. 118 | var self = this; 119 | $.each(elements, function () { 120 | var newelement = this, merge = true; 121 | // Check whether this element is already in the stack. 122 | $.each(self.data.elements, function () { 123 | if (this === newelement) { 124 | merge = false; 125 | return; 126 | } 127 | }); 128 | if (merge) { 129 | self.data.elements.push(newelement); 130 | } 131 | }); 132 | // Re-order elements by their actual DOM position. 133 | self.data.elements.sort(sortOrder); 134 | return elements; 135 | }, 136 | 137 | remove: function (elements) { 138 | if (!elements.length) { 139 | return elements; 140 | } 141 | var self = this; 142 | $(elements).removeClass('selected'); 143 | $.each(elements, function () { 144 | var element = this; 145 | var newlist = []; 146 | $.each(self.data.elements, function () { 147 | if (this !== element) { 148 | newlist.push(this); 149 | } 150 | }); 151 | self.data.elements = newlist; 152 | }); 153 | }, 154 | 155 | edit: function () { 156 | var self = this; 157 | // Mark current selection/commented code as selected. 158 | $(self.data.elements).addClass('selected'); 159 | 160 | // Add Pastie. 161 | if (!self.$form) { 162 | self.$form = Drupal.dreditor.form.create('pastie'); 163 | // Add comment textarea. 164 | self.$form.append('

    Comment selected code:

    '); 165 | self.$form.append(''); 166 | // Add comment save button. 167 | self.$form.addButton((self.data.id !== undefined ? 'Update' : 'Save'), function () { 168 | // @todo For any reason, FF 3.5 breaks when trying to access 169 | // form.comment.value. Works in FF 3.0.x. WTF? 170 | var value = this.find('textarea').val(); 171 | // Store new comment, if non-empty. 172 | if ($.trim(value).length) { 173 | self.comment.save({ 174 | id: self.data.id, 175 | elements: self.data.elements, 176 | comment: value 177 | }); 178 | } 179 | $.each(self.data.elements, function () { 180 | $(this).attr('title', value); 181 | }); 182 | // Reset pastie. 183 | self.reset(); 184 | }); 185 | // Add comment cancel button. 186 | self.$form.addButton('Cancel', function () { 187 | // Reset pastie. 188 | self.reset(); 189 | }); 190 | // Add comment delete button for existing comments. 191 | if (self.data.id !== undefined) { 192 | self.$form.addButton('Delete', function () { 193 | self.comment.remove(self.data.id); 194 | // Reset pastie. 195 | self.reset(); 196 | }); 197 | } 198 | // Append pastie to sidebar, insert current comment and focus it. 199 | self.$form.appendTo('#bar').find('textarea').val(self.data.comment || ''); 200 | Drupal.dreditor.attachBehaviors(); 201 | // Focus pastie; only for initial comment selection to still allow for 202 | // copying of file contents. 203 | self.$form.find('textarea').focus(); 204 | } 205 | }, 206 | 207 | /** 208 | * Wrapper around jQuery's sortOrder() to sort review comments. 209 | */ 210 | sort: function (a, b) { 211 | if (!a || !b) { 212 | return 0; 213 | } 214 | return sortOrder(a.elements[0], b.elements[0]); 215 | }, 216 | 217 | paste: function () { 218 | var html = ''; 219 | var comments = []; 220 | this.comment.comments.sort(this.sort); 221 | $.each(this.comment.comments, function (index, comment) { 222 | // Skip deleted (undefined) comments; this would return window here. 223 | if (!comment) { 224 | return true; 225 | } 226 | var $elements = $(this.elements); 227 | // Skip comments with no corresponding lines. 228 | var firstLine = $elements.get(0); 229 | if (!firstLine) { 230 | return true; 231 | } 232 | var markup = '\n'; 233 | // Add file information. 234 | var lastfile = $elements.eq(0).prevAll('tr.file:has(a.file)').get(0); 235 | if (lastfile) { 236 | markup += lastfile.textContent + '\n'; 237 | } 238 | // Add hunk information. 239 | var lasthunk = $elements.eq(0).prevAll('tr.file').get(0); 240 | if (lasthunk) { 241 | markup += lasthunk.textContent + '\n'; 242 | } 243 | 244 | var lastline = firstLine.previousSibling; 245 | var lastfileNewlineAdded; 246 | 247 | $elements.each(function () { 248 | var $element = $(this); 249 | lastfileNewlineAdded = false; 250 | // Add new last file, in case a comment spans over multiple files. 251 | if (lastfile && lastfile !== $element.prevAll('tr.file:has(a.file)').get(0)) { 252 | lastfile = $element.prevAll('tr.file:has(a.file)').get(0); 253 | if (lastfile) { 254 | markup += '\n' + lastfile.textContent + '\n'; 255 | lastfileNewlineAdded = true; 256 | } 257 | } 258 | // Add new last hunk, in case a comment spans over multiple hunks. 259 | if (lasthunk && lasthunk !== $element.prevAll('tr.file').get(0)) { 260 | lasthunk = $element.prevAll('tr.file').get(0); 261 | if (lasthunk) { 262 | // Only add a newline if there was no new file already. 263 | if (!lastfileNewlineAdded) { 264 | markup += '\n'; 265 | lastfileNewlineAdded = true; 266 | } 267 | markup += lasthunk.textContent + '\n'; 268 | } 269 | } 270 | // Add a delimiter, in case a comment spans over multiple selections. 271 | else if (lastline && lastline !== $element.get(0).previousSibling) { 272 | markup += '...\n'; 273 | } 274 | markup += $element.find('.pre').text() + '\n'; 275 | 276 | // Use this line as previous line for next line. 277 | lastline = $element.get(0); 278 | }); 279 | 280 | markup += '\n'; 281 | markup += '\n' + this.comment; 282 | comments.push(markup); 283 | }); 284 | if (comments.length === 1) { 285 | html += comments.join(''); 286 | } 287 | // If there's more than one comment, wrap them in ordered list markup. 288 | else if (comments.length > 1) { 289 | html += '
      \n\n'; 290 | for (var i = 0; i < comments.length; i++) { 291 | html += '
    1. \n' + comments[i] + '\n
    2. \n\n'; 292 | } 293 | html += '
    '; 294 | } 295 | 296 | // Paste comment into issue comment textarea. 297 | var $commentField = $('#project-issue-ajax-form :input[name*="comment_body"]'); 298 | $commentField.val($commentField.val() + html); 299 | // Flush posted comments. 300 | this.comment.comments = []; 301 | // Change the status to 'needs work'. 302 | // @todo Prevent unintended/inappropriate status changes. 303 | //$('#edit-sid').val(13); 304 | // Jump to the issue comment textarea after pasting. 305 | Drupal.dreditor.goto('#project-issue-ajax-form'); 306 | // Close Dreditor. 307 | Drupal.dreditor.tearDown(); 308 | } 309 | }; 310 | Drupal.dreditor.patchReview.comment = { 311 | /** 312 | * Review comments storage. 313 | */ 314 | comments: [], 315 | 316 | /** 317 | * Create or update a comment. 318 | * 319 | * If data already contains an id, the existing comment is updated. 320 | * 321 | * @return 322 | * The stored data, including new id for new comments. 323 | */ 324 | save: function (data) { 325 | if (data.id !== undefined) { 326 | this.comments[data.id] = data; 327 | } 328 | else { 329 | this.comments.push(data); 330 | // Return value of .push() is not suitable for real ids. 331 | var newid = this.comments.length - 1; 332 | this.comments[newid].id = data.id = newid; 333 | } 334 | // Mark new comments, if there are any. 335 | $(this.comments[data.id].elements).addClass('new-comment'); 336 | $(this.comments[data.id].elements).addClass('comment-id-' + data.id).addClass('has-comment'); 337 | 338 | Drupal.dreditor.attachBehaviors(); 339 | return data; 340 | }, 341 | 342 | load: function (id) { 343 | var data; 344 | if (typeof id !== undefined && typeof this.comments[id] === 'object') { 345 | data = this.comments[id]; 346 | } 347 | return data || {}; 348 | }, 349 | 350 | /** 351 | * Deletes a comment by ID. 352 | * 353 | * Called 'remove', since 'delete' is a reserved keyword. 354 | */ 355 | remove: function (id) { 356 | var data = this.load(id); 357 | if (data && data.id !== undefined) { 358 | $(data.elements) 359 | .removeClass('has-comment') 360 | .removeClass('comment-id-' + id) 361 | .removeAttr('title') 362 | // @todo For whatever reason, the click event is not unbound here. 363 | .unbind('click.patchReview.editComment'); 364 | delete this.comments[id]; 365 | } 366 | return data || {}; 367 | } 368 | }; 369 | Drupal.dreditor.patchReview.overlay = { 370 | element: null, 371 | data: {}, 372 | 373 | setup: function () { 374 | this.element = $('
    ').hide().appendTo('#dreditor #bar'); 375 | return this; 376 | }, 377 | 378 | load: function (data) { 379 | // Setup overlay if required. 380 | if (!this.element) { 381 | this.setup(); 382 | } 383 | if (data !== undefined && typeof data.comment === 'string') { 384 | this.data = data; 385 | this.element.empty(); 386 | // Do some basic text2html processing. 387 | var content = data.comment.replace(/\n$[^<]/gm, '
    \n'); 388 | // @todo jQuery seems to suck up newlines in child nodes (such as ). 389 | this.element.append('

    ' + content + '

    '); 390 | } 391 | }, 392 | 393 | show: function () { 394 | this.element.show(); 395 | return this; 396 | }, 397 | 398 | hide: function () { 399 | this.element.hide(); 400 | return this; 401 | } 402 | }; 403 | /** 404 | * Create diff outline and highlighting from plaintext code. 405 | * 406 | * We parse all lines of the file into separate DOM elements to be able to 407 | * attach data (e.g. comments) to selected lines and generate a "jump menu" 408 | * for files and hunks. 409 | * 410 | * @param context 411 | * The context to work on. 412 | * @param code 413 | * Plain-text code to parse. 414 | * 415 | * @todo Move setup and storage of pastie elsewhere? 416 | */ 417 | Drupal.dreditor.patchReview.behaviors.setup = function (context, code) { 418 | // Ensure this is only executed once. 419 | if ($('#code', context).length || !code) { 420 | return; 421 | } 422 | 423 | // Reset pastie; may have been active when user clicked global 'Cancel' button. 424 | // @todo This cries for a proper hook system. 425 | Drupal.dreditor.patchReview.reset(); 426 | 427 | // Convert CRLF, CR into LF. 428 | code = code.replace(/\r\n|\r/g, "\n"); 429 | // Escape HTML tags and entities; order of replacements is important. 430 | code = code.replace(/&/g, '&'); 431 | code = code.replace(//g, '>'); 433 | // Remove cruft: IDE comments and unversioned files. 434 | code = code.replace(/^\# .+\n|^\? .+\n/mg, ''); 435 | 436 | // Setup code container. 437 | var $code = $('
    '); 438 | $code.append(''); 439 | var $menu = $('#menu', context); 440 | var $lastFile = $('
  • Parse error
  • '); 441 | 442 | $('

    Diff statistics

    ').appendTo('#dreditor #bar'); 443 | var $diffstat = $('
    ').appendTo('#dreditor #bar'); 444 | var diffstat = { files: 0, insertions: 0, deletions: 0 }; 445 | 446 | code = code.split('\n'); 447 | var ln1 = ''; 448 | var ln2 = ''; 449 | var ln1content = ''; 450 | var ln2content = ''; 451 | var maxln1 = 0; 452 | var maxln2 = 0; 453 | for (var n in code) { 454 | var ln1o = true; 455 | var ln2o = true; 456 | var line = code[n]; 457 | 458 | // Build file menu links. 459 | line = line.replace(/^(\+\+\+ )([^\s]+)(\s.*)?/, function (full, match1, match2, match3) { 460 | var id = match2.replace(/[^A-Za-z_-]/g, ''); 461 | $lastFile = $('
  • ' + match2 + '
  • '); 462 | $menu.append($lastFile); 463 | diffstat.files++; 464 | return match1 + '' + match2 + '' + (match3 ? match3 : ''); 465 | }); // jshint ignore:line 466 | // Build hunk menu links for file. 467 | line = line.replace(/^(@@ .+ @@\s+)([^\s]+\s[^\s\(]*)/, function (full, match1, match2) { 468 | var id = match2.replace(/[^A-Za-z_-]/g, ''); 469 | $lastFile.append('
  • ' + match2 + '
  • '); 470 | return match1 + '' + match2 + ''; 471 | }); // jshint ignore:line 472 | 473 | // parse hunk line numbers 474 | var line_numbers = line.match(/^@@ -([0-9]+),[0-9]+ \+([0-9]+),[0-9]+ @@/); 475 | if (line_numbers) { 476 | ln1 = line_numbers[1]; 477 | ln2 = line_numbers[2]; 478 | } 479 | 480 | var classes = [], syntax = false; 481 | // Colorize file diff lines. 482 | if (line.match(/^((index|===|RCS|new file mode|deleted file mode|similarity|rename|copy|retrieving|diff|\-\-\-\s|\-\-\s|\+\+\+\s|@@\s).*)$/i)) { 483 | classes.push('file'); 484 | ln1o = false; 485 | ln2o = false; 486 | // Renames and copies are easy to miss; colorize them. 487 | if (line.match(/^rename from|^copy from|^deleted file/)) { 488 | classes.push('old'); 489 | } 490 | else if (line.match(/^rename to|^copy to/)) { 491 | classes.push('new'); 492 | } 493 | } 494 | // Colorize old code, but skip file diff lines. 495 | else if (line.match(/^((?!\-\-\-$|\-\-$)\-.*)$/)) { 496 | classes.push('old'); 497 | diffstat.deletions++; 498 | syntax = true; 499 | if (ln1) { 500 | ln2o = false; 501 | ln1++; 502 | } 503 | } 504 | // Colorize new code, but skip file diff lines. 505 | else if (line.match(/^((?!\+\+\+)\+.*)$/)) { 506 | // Expose tabs. 507 | line = line.replace(/(\t+)/, '$1'); 508 | // Wrap trailing white-space with a SPAN to expose them during patch 509 | // review. Also add a hidden end-of-line character that will only appear 510 | // in the pasted code. 511 | line = line.replace(/^(.*\S)(\s+)$/, '$1$2'); 512 | 513 | classes.push('new'); 514 | diffstat.insertions++; 515 | syntax = true; 516 | if (ln2) { 517 | ln1o = false; 518 | ln2++; 519 | } 520 | } 521 | // Replace line with a space (so ruler shows up). 522 | else if (!line.length) { 523 | line = ' '; 524 | } 525 | // Match git format-patch EOF lines and reset line count. 526 | else if (line.match(/^\-\-$/)) { 527 | ln1o = false; 528 | ln2o = false; 529 | ln1 = ''; 530 | ln2 = ''; 531 | } 532 | // Detect missing newline at end of file. 533 | else if (line.match(/.*No newline at end of file.*/i)) { 534 | line = '' + line + ''; 535 | } 536 | else { 537 | if (ln1 && ln1o) { 538 | ln1++; 539 | } 540 | if (ln2 && ln2o) { 541 | ln2++; 542 | } 543 | } 544 | // Colorize comments. 545 | if (syntax && line.match(/^.\s*\/\/|^.\s*\/\*[\* ]|^.\s+\*|^.\s*#/)) { 546 | classes.push('comment'); 547 | } 548 | 549 | // Wrap all lines in PREs for copy/pasting and add the 80 character ruler. 550 | ln1content = (ln1o ? ln1 : ''); 551 | ln2content = (ln2o ? ln2 : ''); 552 | classes = (classes.length ? ' class="' + classes.join(' ') + '"' : ''); 553 | line = '' + line + ''; 554 | 555 | // Calculate the largest line numbers in the gutter, used 556 | // for determining the position of the 80 character ruler. 557 | if (ln1content > maxln1) { 558 | maxln1 = ln1content; 559 | } 560 | if (ln2content > maxln2) { 561 | maxln2 = ln2content; 562 | } 563 | 564 | // Append line to parsed code. 565 | $code.append(line); 566 | } 567 | 568 | // The line ruler must be displayed consistently across all browsers and OS 569 | // that may or may not have the same fonts (kerning). Calculate the width of 570 | // 81 "0" characters (80 character line plus the +/- prefix from the diff) 571 | // by using an array (82 items joined by "0"). 572 | // 573 | // We also calculate the width of the gutter (line numbers) by using the 574 | // largest combination of line numbers calculated above. 575 | var $lineRuler = $('
    ' + new Array(82).join('0') + '
    ') 576 | .appendTo('#dreditor'); 577 | var ln1gutter = $lineRuler.find('.ln-1').outerWidth(); 578 | var ln2gutter = $lineRuler.find('.ln-2').outerWidth(); 579 | var lineWidth = $lineRuler.find('.pre').width(); 580 | // Add 10px for padding (the td that contains span.pre). 581 | var lineRulerOffset = ln1gutter + ln2gutter + lineWidth + 10; 582 | var lineRulerStyle = {}; 583 | // Check for a reasonable value for the ruler offset. 584 | if (lineRulerOffset > 100) { 585 | lineRulerStyle = { 586 | 'visibility': 'visible', 587 | 'left': lineRulerOffset + 'px' 588 | }; 589 | } 590 | $lineRuler.remove(); 591 | 592 | // Append to body... 593 | $('#dreditor-content', context) 594 | // the parsed code. 595 | .append($code); 596 | 597 | // Set the position of the 80-character ruler. 598 | $('thead .line-ruler').css(lineRulerStyle); 599 | 600 | // Append diffstat to sidebar. 601 | $diffstat.html(diffstat.files + ' files changed, ' + diffstat.insertions + ' insertions, ' + diffstat.deletions + ' deletions.'); 602 | 603 | var start_row; 604 | $('tr', $code).mousedown(function(){ 605 | start_row = $(this)[0]; 606 | }); 607 | 608 | // Colorize rows during selection. 609 | $('tr', $code).mouseover(function(){ 610 | if (start_row) { 611 | var end_row = $(this)[0]; 612 | var start = false; 613 | var end = false; 614 | var selection = []; 615 | selection.push(start_row); 616 | $('tr', $code).each(function(){ 617 | if ($(this)[0] === start_row) { 618 | start = true; 619 | } 620 | if (start && !end) { 621 | selection.push($(this)[0]); 622 | } 623 | if ($(this)[0] === end_row) { 624 | end = true; 625 | } 626 | }); 627 | // Refresh selection. 628 | $('.pre-selected').removeClass('pre-selected'); 629 | $.each(selection, function () { 630 | $(this).addClass('pre-selected'); 631 | }); 632 | } 633 | }); 634 | 635 | // Finalize selection. 636 | $('tr', $code).mouseup(function(){ 637 | if (start_row) { 638 | var end_row = $(this)[0]; 639 | var start = false; 640 | var end = false; 641 | var selection = []; 642 | selection.push(start_row); 643 | $('tr', $code).each(function(){ 644 | if ($(this)[0] === start_row) { 645 | start = true; 646 | } 647 | if (start && !end) { 648 | selection.push($(this)[0]); 649 | } 650 | if ($(this)[0] === end_row) { 651 | end = true; 652 | } 653 | }); 654 | 655 | // If at least one element in selection is not yet selected, we need to select all. Otherwise, deselect all. 656 | var deselect = true; 657 | $.each(selection, function () { 658 | if (!$(this).is('.selected')) { 659 | deselect = false; 660 | } 661 | }); 662 | $('.pre-selected').removeClass('pre-selected'); 663 | if (deselect) { 664 | Drupal.dreditor.patchReview.remove(selection); 665 | } 666 | else { 667 | Drupal.dreditor.patchReview.add(selection); 668 | // Display pastie. 669 | Drupal.dreditor.patchReview.edit(); 670 | } 671 | } 672 | start_row = false; 673 | }); 674 | }; 675 | /** 676 | * Attach click handler to jump menu. 677 | */ 678 | Drupal.dreditor.patchReview.behaviors.jumpMenu = function (context) { 679 | $('#menu a', context).once('dreditor-jumpmenu', function () { 680 | $(this).click(function () { 681 | Drupal.dreditor.goto(this.hash); 682 | return false; 683 | }); 684 | }); 685 | }; 686 | Drupal.dreditor.patchReview.behaviors.attachPastie = function (context) { 687 | // @todo Seems we need detaching behaviors, but only for certain DOM elements, 688 | // wrapped in a jQuery object to eliminate the naive 'new-comment' handling. 689 | $('#code .has-comment.new-comment', context).removeClass('new-comment') 690 | .unbind('click.patchReview.editComment').bind('click.patchReview.editComment', function () { 691 | // Load data from from element attributes. 692 | var params = Drupal.dreditor.getParams(this, 'comment'); 693 | if (params.id !== undefined) { 694 | // Load comment and put data into selection storage. 695 | var data = Drupal.dreditor.patchReview.comment.load(params.id); 696 | Drupal.dreditor.patchReview.load(data); 697 | // Display pastie. 698 | Drupal.dreditor.patchReview.edit(); 699 | } 700 | return false; 701 | }) 702 | // Display existing comment on hover. 703 | .hover( 704 | function () { 705 | // Load data from from element attributes. 706 | var params = Drupal.dreditor.getParams(this, 'comment'); 707 | // Load comment and put data into selection storage. 708 | if (params.id !== undefined) { 709 | var data = Drupal.dreditor.patchReview.comment.load(params.id); 710 | Drupal.dreditor.patchReview.overlay.load(data); 711 | // Display overlay. 712 | Drupal.dreditor.patchReview.overlay.show(); 713 | } 714 | }, 715 | function () { 716 | Drupal.dreditor.patchReview.overlay.hide(); 717 | } 718 | ); 719 | }; 720 | Drupal.dreditor.patchReview.behaviors.saveButton = function (context) { 721 | if (!$('#dreditor-actions #dreditor-save', context).length) { 722 | // @todo Convert global Dreditor buttons into a Dreditor form. 723 | var $save = $(''); 724 | $save.click(function () { 725 | Drupal.dreditor.patchReview.paste(); 726 | return false; 727 | }); 728 | $save.prependTo('#dreditor-actions'); 729 | } 730 | }; 731 | /** 732 | * Add link to toggle display of deleted patch lines. 733 | */ 734 | Drupal.dreditor.patchReview.behaviors.toggleDeletions = function (context) { 735 | $('#dreditor #bar').once('toggle-deletions', function () { 736 | var $link = $('Hide deletions'); 737 | $link.toggle( 738 | function () { 739 | $('#code tr.old', context).addClass('element-invisible'); 740 | $link.text('Show deletions'); 741 | this.blur(); 742 | return false; 743 | }, 744 | function () { 745 | $('#code tr.old', context).removeClass('element-invisible'); 746 | $link.text('Hide deletions'); 747 | this.blur(); 748 | return false; 749 | } 750 | ); 751 | $(this).append($link); 752 | }); 753 | }; 754 | -------------------------------------------------------------------------------- /src/js/plugins/pift.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PIFT enhancements. 3 | */ 4 | Drupal.behaviors.dreditorPIFT = { 5 | attach: function (context) { 6 | var $context = $(context); 7 | $context.find('.field-name-field-issue-files').attr('id', 'recent-files'); 8 | $context.find('.field-name-field-issue-files table').once('dreditor-pift', function () { 9 | var $table = $(this); 10 | $table.find('th[name*="size"], th[name*="uid"]').remove(); 11 | var comments = 0; 12 | $table.find('tbody tr').each(function() { 13 | var $row = $(this); 14 | // File row. 15 | if ($row.is('.extended-file-field-table-row:not(.pift-test-info)')) { 16 | var $cid = $row.find('.extended-file-field-table-cid'); 17 | var $file = $row.find('.extended-file-field-table-filename .file'); 18 | var $size = $row.find('.extended-file-field-table-filesize'); 19 | var $name = $row.find('.extended-file-field-table-uid'); 20 | var comment = parseInt($cid.text().replace('#', ''), 10) || 0; 21 | $file.find('a:not(.dreditor-button)').before('' + $size.text() + ''); 22 | $size.remove(); 23 | $cid.append($name.html()); 24 | $name.remove(); 25 | var $parentComment = $table.find('tr[data-comment="' + comment +'"]'); 26 | var zebra = $parentComment.data('zebra'); 27 | if (zebra) { 28 | $row.removeClass('odd even').addClass(zebra); 29 | } 30 | var $prevCid = $parentComment.find('.extended-file-field-table-cid'); 31 | if ($prevCid.length) { 32 | var rowspan = $cid.attr('rowspan'); 33 | $prevCid.attr('rowspan', ($prevCid.attr('rowspan') + rowspan)); 34 | $cid.remove(); 35 | } 36 | else { 37 | comments++; 38 | zebra = comments % 2 ? 'odd' : 'even'; 39 | $row 40 | .attr({ 41 | 'data-comment': comment, 42 | 'data-zebra': zebra 43 | }) 44 | .removeClass('odd even') 45 | .addClass(zebra); 46 | } 47 | } 48 | // PIFT row. 49 | else if ($row.is('.pift-test-info')) { 50 | var $cell = $row.find('td'); 51 | $row.prev().find('td:not(.extended-file-field-table-cid)').addClass($cell.attr('class')); 52 | $cell.find('.pift-operations').prependTo($cell); 53 | } 54 | }); 55 | }); 56 | 57 | $context.find('.field-name-field-issue-changes table.nodechanges-file-changes').once('dreditor-pift', function() { 58 | var $table = $(this); 59 | $table.find('th:last').remove(); 60 | $table.find('tbody tr').each(function() { 61 | var $row = $(this); 62 | // PIFT row. 63 | if ($row.is('.pift-test-info')) { 64 | var $cell = $row.find('td'); 65 | $row.prev().find('td').addClass($cell.attr('class')); 66 | $cell.find('.pift-operations').prependTo($cell); 67 | } 68 | // File row. 69 | else { 70 | var $file = $row.find('.nodechanges-file-link .file'); 71 | var $size = $row.find('.nodechanges-file-size'); 72 | $file.find('a:not(.dreditor-button)').before('' + $size.text() + ''); 73 | $size.remove(); 74 | } 75 | }); 76 | }); 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /src/js/plugins/projects.collapse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Attach collapsing behavior to user project tables. 3 | */ 4 | Drupal.behaviors.dreditorProjectsCollapse = { 5 | attach: function (context) { 6 | var $tables = $(context).find('.view-project-issue-user-projects table'); 7 | if (!$tables.length) { 8 | return; 9 | } 10 | var enabled = Drupal.storage.load('projectscollapse.status'); 11 | 12 | // Add link to toggle this feature. 13 | $('') 14 | .text(enabled ? 'Always show projects' : 'Collapse projects') 15 | .click(function () { 16 | Drupal.storage.save('projectscollapse.status', !enabled); 17 | // Reload the current page without refresh from server. 18 | window.location.href = window.location.href; 19 | return false; 20 | }) 21 | .insertBefore($tables.eq(0)); 22 | 23 | if (!enabled) { 24 | return; 25 | } 26 | $tables.once('dreditor-projectscollapse', function () { 27 | var $elements = $(this).children(':not(caption)'); 28 | $(this).css('width', '100%') 29 | .find('> caption') 30 | .css({ cursor: 'pointer' }) 31 | .bind('click.projectscollapse', function () { 32 | // .slideToggle() forgets about table width in d.o's outdated jQuery 33 | // version. 34 | $elements.toggle(); 35 | }) 36 | .triggerHandler('click'); 37 | }); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/js/plugins/syntax.autocomplete.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Attaches syntax/markup autocompletion to all textareas. 3 | */ 4 | Drupal.behaviors.dreditorSyntaxAutocomplete = { 5 | attach: function (context) { 6 | $('textarea', context).once('dreditor-syntaxautocomplete', function () { 7 | new Drupal.dreditor.syntaxAutocomplete(this); 8 | }); 9 | } 10 | }; 11 | 12 | /** 13 | * Initializes a new syntax autocompletion object. 14 | * 15 | * @param element 16 | * A form input element (e.g., textarea) to bind to. 17 | */ 18 | Drupal.dreditor.syntaxAutocomplete = function (element) { 19 | this.keyCode = 9; 20 | this.$element = $(element); 21 | 22 | this.$suggestion = $(''); 23 | this.$tooltip = $('
    TAB:
    ') 24 | .hide() 25 | .insertAfter(this.$element) 26 | .append(this.$suggestion); 27 | 28 | // Intercept the autocompletion key upon pressing the key. Webkit does not 29 | // support the keypress event for special keys (such as arrows and TAB) that 30 | // are reserved for internal browser behavior. Only the keydown event is 31 | // triggered for all keys. 32 | // @see http://bugs.jquery.com/ticket/7300 33 | this.$element.bind('keydown.syntaxAutocomplete', { syntax: this }, this.keypressHandler); 34 | // After user input has been entered, check for suggestions. 35 | this.$element.bind('keyup.syntaxAutocomplete', { syntax: this }, this.keyupHandler); 36 | }; 37 | 38 | /** 39 | * Responds to keypress events in the bound element to prevent default key event handlers. 40 | */ 41 | Drupal.dreditor.syntaxAutocomplete.prototype.keypressHandler = function (event) { 42 | var self = event.data.syntax, pos = this.selectionEnd; 43 | 44 | // If the autocompletion key was pressed and there is a suggestion, perform 45 | // the text replacement. 46 | // event.which is 0 in the keypress event, so directly compare with keyCode. 47 | if (event.keyCode === self.keyCode && self.suggestion) { 48 | // Backup the current scroll position within the textarea. Any manipulation 49 | // of this.value automatically resets this.scrollTop to zero. 50 | var scrollTop = this.scrollTop; 51 | 52 | var prefix = this.value.substring(0, pos - self.needle.length); 53 | var suffix = this.value.substring(pos); 54 | this.value = prefix + self.suggestion.replace('^', '') + suffix; 55 | 56 | // Move the cursor to the autocomplete position marker. 57 | var newpos = pos - self.needle.length + self.suggestion.indexOf('^'); 58 | this.setSelectionRange(newpos, newpos); 59 | 60 | // Restore original scroll position. 61 | this.scrollTop = scrollTop; 62 | 63 | // Remove the tooltip and suggestion directly after executing the 64 | // autocompletion. 65 | self.delSuggestion(); 66 | 67 | // Do not trigger the browser's default keyboard shortcut. 68 | event.preventDefault(); 69 | event.stopPropagation(); 70 | return false; 71 | } 72 | }; 73 | 74 | /** 75 | * Responds to keyup events in the bound element. 76 | */ 77 | Drupal.dreditor.syntaxAutocomplete.prototype.keyupHandler = function (event) { 78 | // Don't interfere with text selections. 79 | if (this.selectionStart !== this.selectionEnd) { 80 | return; 81 | } 82 | // Skip special keystrokes. 83 | if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) { 84 | return; 85 | } 86 | var self = event.data.syntax, pos = this.selectionEnd; 87 | // Retrieve the needle: The word before the cursor. 88 | var needle = this.value.substring(0, pos).match(/[^\s>(]+$/); 89 | // If there is a needle, check whether to show a suggestion. 90 | // @todo Revamp the entire following conditional code to call 91 | // delSuggestion() only once. 92 | if (needle) { 93 | self.needle = needle[0]; 94 | // If the needle is found in the haystack of suggestions, show a suggestion. 95 | var suggestion; 96 | if (suggestion = self.checkSuggestion(self.needle)) { 97 | self.setSuggestion(suggestion); 98 | } 99 | // Otherwise, ensure a possibly existing last suggestion is removed. 100 | else { 101 | self.delSuggestion(); 102 | } 103 | } 104 | // Otherwise, ensure there is no suggestion. 105 | else { 106 | self.delSuggestion(); 107 | } 108 | }; 109 | 110 | /** 111 | * Determines whether there is a suggestion for a given needle. 112 | */ 113 | Drupal.dreditor.syntaxAutocomplete.prototype.checkSuggestion = function (needle) { 114 | var self = this, suggestion = false; 115 | $.each(self.suggestions, function () { 116 | if ($.isFunction(this)) { 117 | // Use .call() to provide self in this. 118 | if (suggestion = this.call(self, needle)) { 119 | return false; 120 | } 121 | } 122 | else if (this[needle]) { 123 | if (suggestion = this[needle]) { 124 | return false; 125 | } 126 | } 127 | }); 128 | return suggestion; 129 | }; 130 | 131 | /** 132 | * Sets the suggestion and shows the autocompletion tooltip. 133 | */ 134 | Drupal.dreditor.syntaxAutocomplete.prototype.setSuggestion = function (suggestion) { 135 | var self = this; 136 | if (suggestion !== self.suggestion) { 137 | self.suggestion = suggestion; 138 | self.$suggestion.text(self.suggestion.replace('^', '')); 139 | self.$tooltip.show(); 140 | } 141 | }; 142 | 143 | /** 144 | * Deletes the suggestion and hides the autocompletion tooltip. 145 | */ 146 | Drupal.dreditor.syntaxAutocomplete.prototype.delSuggestion = function () { 147 | var self = this; 148 | delete self.suggestion; 149 | self.$tooltip.hide(); 150 | }; 151 | 152 | Drupal.dreditor.syntaxAutocomplete.prototype.suggestions = {}; 153 | 154 | /** 155 | * Look-up map for simple HTML/markup suggestions. 156 | */ 157 | Drupal.dreditor.syntaxAutocomplete.prototype.suggestions.html = { 158 | '\n", 159 | '', 160 | '^\n\n", 161 | '\n^", 162 | '^', 163 | '^
    ', 164 | '^', 165 | '\n
    ^
    \n
    \n\n", 166 | '^\n
    ", 167 | '^', 168 | '^', 169 | '^\n", 170 | '^\n", 171 | '^\n", 172 | '^\n", 173 | '^\n", 174 | '^\n", 175 | '\n\n^", 176 | '', 177 | '^", 178 | '\n^\n\n", 179 | '^

    \n", 180 | '\n^\n\n", 181 | '^', 182 | '^', 183 | '\n\n^\n\n\n\n\n\n", 184 | '\n^\n", 185 | '^", 186 | '^", 187 | '^', 188 | '\n^\n\n" 189 | }; 190 | 191 | /** 192 | * Suggest a [#issue] conversion for Project Issue input filter. 193 | */ 194 | Drupal.dreditor.syntaxAutocomplete.prototype.suggestions.issue = function (needle) { 195 | var matches; 196 | if (matches = needle.match('^https?://(?:www.)?drupal.org/node/([0-9]+)')) { 197 | return '[#' + matches[1] + ']^'; 198 | } 199 | return false; 200 | }; 201 | 202 | /** 203 | * Suggest a username. 204 | */ 205 | Drupal.dreditor.syntaxAutocomplete.prototype.suggestions.user = function (needle) { 206 | var matches, self = this; 207 | if (matches = needle.match('^@([a-zA-Z0-9]+)$')) { 208 | // Performance: Upon first match, setup a username list once. 209 | if (typeof self.suggestionUserList === 'undefined') { 210 | self.suggestionUserList = {}; 211 | var seen = {}; 212 | // Add issue author to comment authors and build the suggestion list. 213 | $('.comment a.username').add('.node .submitted a.username').each(function () { 214 | if (!seen[this.text]) { 215 | seen[this.text] = 1; 216 | // Use the shortest possible needle. 217 | var i, n, name = this.text.toLowerCase(); 218 | for (i = 1; i < name.length; i++) { 219 | n = name.substring(0, i); 220 | if (!self.suggestionUserList[n]) { 221 | self.suggestionUserList[n] = '@' + this.text + '^'; 222 | break; 223 | } 224 | } 225 | } 226 | }); 227 | } 228 | if (self.suggestionUserList[matches[1]]) { 229 | return self.suggestionUserList[matches[1]]; 230 | } 231 | } 232 | return false; 233 | }; 234 | 235 | /** 236 | * Suggest a comment on issue. 237 | */ 238 | Drupal.dreditor.syntaxAutocomplete.prototype.suggestions.comment = function (needle) { 239 | var matches, self = this; 240 | if (matches = needle.match('^#([0-9]+)$')) { 241 | // Performance: Upon first match, setup a username list once. 242 | if (typeof self.suggestionCommentList === 'undefined') { 243 | self.suggestionCommentList = { 244 | 0: 'content' 245 | }; 246 | // Add issue author to comment authors and build the suggestion list. 247 | var n, id; 248 | $('.comment a.permalink').each(function () { 249 | n = this.text.substring(9); 250 | id = this.hash.substring(1); 251 | self.suggestionCommentList[n] = id; 252 | }); 253 | } 254 | if (self.suggestionCommentList[matches[1]]) { 255 | return '#' + matches[1] + '^'; 256 | } 257 | } 258 | return false; 259 | }; 260 | -------------------------------------------------------------------------------- /src/less/dreditor.less: -------------------------------------------------------------------------------- 1 | #dreditor-wrapper { 2 | position: fixed; 3 | z-index: 1000; 4 | width: 100%; 5 | top: 0; 6 | } 7 | #dreditor { 8 | position: relative; 9 | width: 100%; 10 | height: 100%; 11 | background-color: #fff; 12 | border: 1px solid #ccc; 13 | &.resizing { 14 | cursor: ew-resize; 15 | -moz-user-select: none; 16 | -webkit-user-select: none; 17 | user-select: none; 18 | } 19 | } 20 | #dreditor #bar, #dreditor-actions { 21 | padding: 0 10px; 22 | font: 10px/18px sans-serif, verdana, tahoma, arial; 23 | min-width: 230px; 24 | } 25 | #dreditor #bar { 26 | float: left; 27 | height: 100%; 28 | position: relative; 29 | .resizer { 30 | bottom: 0; 31 | cursor: ew-resize; 32 | display: block; 33 | position: absolute; 34 | right: -1px; 35 | top: 0; 36 | width: 6px; 37 | z-index: 9999; 38 | &:hover, &.resizing { 39 | background: rgba(0,0,0,.1); 40 | } 41 | } 42 | } 43 | #dreditor-actions { 44 | bottom: 0; 45 | left: -5px; 46 | padding-top: 5px; 47 | padding-bottom: 5px; 48 | position: absolute; 49 | } 50 | .dreditor-button, .dreditor-button:link, .dreditor-button:visited, #page a.dreditor-button { 51 | background: rgb(122,188,255); 52 | background: -moz-linear-gradient(top, rgba(122,188,255,1) 0%, rgba(96,171,248,1) 44%, rgba(64,150,238,1) 100%); 53 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(122,188,255,1)), color-stop(44%,rgba(96,171,248,1)), color-stop(100%,rgba(64,150,238,1))); 54 | background: -webkit-linear-gradient(top, rgba(122,188,255,1) 0%,rgba(96,171,248,1) 44%,rgba(64,150,238,1) 100%); 55 | background: -o-linear-gradient(top, rgba(122,188,255,1) 0%,rgba(96,171,248,1) 44%,rgba(64,150,238,1) 100%); 56 | background: -ms-linear-gradient(top, rgba(122,188,255,1) 0%,rgba(96,171,248,1) 44%,rgba(64,150,238,1) 100%); 57 | background: linear-gradient(to bottom, rgba(122,188,255,1) 0%,rgba(96,171,248,1) 44%,rgba(64,150,238,1) 100%); 58 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#7abcff', endColorstr='#4096ee',GradientType=0 ); 59 | border: 1px solid #3598E8; 60 | color: #fff; 61 | cursor: pointer; 62 | font-size: 11px; 63 | font-family: sans-serif, verdana, tahoma, arial; 64 | font-weight: bold; 65 | padding: 0.1em 0.8em; 66 | text-transform: uppercase; 67 | text-decoration: none; 68 | moz-border-radius: 3px; 69 | webkit-border-radius: 3px; 70 | border-radius: 3px; 71 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); 72 | } 73 | .dreditor-button:hover, #page a.dreditor-button:hover { 74 | background: rgb(145,200,255); 75 | background: -moz-linear-gradient(top, rgba(145,200,255,1) 0%, rgba(96,171,248,1) 44%, rgba(94,166,237,1) 100%); 76 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(145,200,255,1)), color-stop(44%,rgba(96,171,248,1)), color-stop(100%,rgba(94,166,237,1))); 77 | background: -webkit-linear-gradient(top, rgba(145,200,255,1) 0%,rgba(96,171,248,1) 44%,rgba(94,166,237,1) 100%); 78 | background: -o-linear-gradient(top, rgba(145,200,255,1) 0%,rgba(96,171,248,1) 44%,rgba(94,166,237,1) 100%); 79 | background: -ms-linear-gradient(top, rgba(145,200,255,1) 0%,rgba(96,171,248,1) 44%,rgba(94,166,237,1) 100%); 80 | background: linear-gradient(to bottom, rgba(145,200,255,1) 0%,rgba(96,171,248,1) 44%,rgba(94,166,237,1) 100%); 81 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#91c8ff', endColorstr='#5ea6ed',GradientType=0 ); 82 | } 83 | .dreditor-button:active, #page a.dreditor-button:active { 84 | background: rgb(64,150,238); 85 | background: -moz-linear-gradient(top, rgba(64,150,238,1) 0%, rgba(96,171,248,1) 56%, rgba(122,188,255,1) 100%); 86 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(64,150,238,1)), color-stop(56%,rgba(96,171,248,1)), color-stop(100%,rgba(122,188,255,1))); 87 | background: -webkit-linear-gradient(top, rgba(64,150,238,1) 0%,rgba(96,171,248,1) 56%,rgba(122,188,255,1) 100%); 88 | background: -o-linear-gradient(top, rgba(64,150,238,1) 0%,rgba(96,171,248,1) 56%,rgba(122,188,255,1) 100%); 89 | background: -ms-linear-gradient(top, rgba(64,150,238,1) 0%,rgba(96,171,248,1) 56%,rgba(122,188,255,1) 100%); 90 | background: linear-gradient(to bottom, rgba(64,150,238,1) 0%,rgba(96,171,248,1) 56%,rgba(122,188,255,1) 100%); 91 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#4096ee', endColorstr='#7abcff',GradientType=0 ); 92 | } 93 | .dreditor-button { 94 | margin: 0 0.5em 0.5em; 95 | } 96 | .dreditor-patchreview, .dreditor-patchtest, .dreditor-inlineimage { 97 | float: right; 98 | line-height: 1.25em; 99 | margin: 0 0 0 1em; 100 | } 101 | #dreditor h3 { 102 | margin: 18px 0 0; 103 | } 104 | #dreditor #menu { 105 | margin: 0; 106 | max-height: 30%; 107 | overflow-y: scroll; 108 | padding: 0; 109 | } 110 | #dreditor #menu li { 111 | list-style: none; 112 | margin: 0; 113 | white-space: nowrap; 114 | } 115 | #dreditor #menu li li { 116 | padding: 0 0 0 1em; 117 | } 118 | #dreditor #menu > li > a { 119 | display: block; 120 | padding: 0 0 0 0.2em; 121 | background-color: #f0f0f0; 122 | } 123 | #dreditor a { 124 | text-decoration: none; 125 | background: transparent; 126 | } 127 | #dreditor .form-textarea { 128 | width: 100%; 129 | height: 12em; 130 | font: 13px Consolas, 'Liberation Mono', Courier, monospace; 131 | color: #000; 132 | } 133 | #dreditor .resizable-textarea { 134 | margin: 0 0 9px; 135 | } 136 | #dreditor-content { 137 | border-left: 1px solid #ccc; 138 | overflow: scroll; 139 | height: 100%; 140 | } 141 | #dreditor-content, #code tr, #code td { 142 | font: 13px/18px Consolas, 'Liberation Mono', Courier, monospace; 143 | } 144 | #dreditor #code { 145 | position: relative; 146 | width: 100%; 147 | } 148 | #dreditor #code td { 149 | overflow: hidden; 150 | padding: 0 10px; 151 | } 152 | #dreditor #code .ln { 153 | width: 1px; 154 | border-right: 1px solid #e5e5e5; 155 | text-align: right; 156 | } 157 | #dreditor #code .ln:before { 158 | content: attr(data-line-number); 159 | } 160 | #dreditor #code tr { 161 | background-color: transparent; 162 | border: 0; 163 | color: #888; 164 | margin: 0; 165 | padding: 0; 166 | } 167 | #dreditor #code .pre { 168 | white-space: pre; 169 | } 170 | #dreditor #code thead .line-ruler { 171 | border-left: 1px solid rgba(0,0,0,0.15); 172 | position: absolute; 173 | height: 100%; 174 | width: 1px; 175 | top: 0; 176 | padding: 0; 177 | visibility: hidden; 178 | } 179 | #dreditor #code .pre span.space { 180 | display: inline-block; 181 | margin-left: 1px; 182 | width: 2px; 183 | height: 7px; 184 | background-color: #ddd; 185 | } 186 | #dreditor #code .pre span.error { 187 | background-color: #f99; 188 | line-height: 100%; 189 | width: auto; 190 | height: auto; 191 | border: 0; 192 | } 193 | #dreditor #code .pre span.error.eof { 194 | color: #fff; 195 | background-color: #f66; 196 | } 197 | #dreditor #code .pre span.error.tab { 198 | background-color: #fdd; 199 | } 200 | #dreditor #code .pre span.hidden { 201 | display: none; 202 | } 203 | #dreditor #code tr.file { 204 | background-color: #E8F1F6; 205 | color: #064A6F; 206 | } 207 | #dreditor #code tr.file a { 208 | color: #064A6F; 209 | } 210 | #dreditor #code tr.file .ln { 211 | background-color: #DAEAF3; 212 | border-color: #BFD4EE; 213 | } 214 | #dreditor #code tr.old { 215 | background-color: #fdd; 216 | color: #c00; 217 | } 218 | #dreditor #code tr.old a { 219 | color: #c00; 220 | } 221 | #dreditor #code tr.old .ln { 222 | background-color: #f7c8c8; 223 | border-color: #e9aeae; 224 | } 225 | #dreditor #code tr.new { 226 | background-color: #dfd; 227 | color: #008503; 228 | float: none; 229 | font-size: 100%; 230 | font-weight: normal; 231 | } 232 | #dreditor #code tr.new a { 233 | color: #008503; 234 | } 235 | #dreditor #code tr.new .ln { 236 | background-color: #ceffce; 237 | border-color: #b4e2b4; 238 | } 239 | #dreditor #code .comment { 240 | color: #070; 241 | } 242 | tr.selected td { 243 | background: transparent; 244 | } 245 | #dreditor #code tr:hover, #dreditor #code tr:hover td, #dreditor #code tr:hover td a { 246 | background: #FFF4CE !important; 247 | border-color: #F3D670 !important; 248 | color: #BB7306 !important; 249 | cursor: pointer; 250 | } 251 | #dreditor #code tr.selected, 252 | #dreditor #code tr.pre-selected, 253 | #dreditor #code tr.has-comment { 254 | background: #FFF4CE; 255 | cursor: pointer; 256 | .ln { 257 | background: #FFECAB; 258 | border-color: #EBD17B; 259 | } 260 | &:hover { 261 | &, td, td a { 262 | background: #fff !important; 263 | } 264 | } 265 | } 266 | #dreditor #code tr:hover td { 267 | box-shadow: 0px -1px 0 0px #FCD773 inset, 0px 1px 0 0px #FCD773 inset; 268 | } 269 | .element-invisible { 270 | clip: rect(1px, 1px, 1px, 1px); 271 | position: absolute !important; 272 | } 273 | 274 | /** 275 | * Quick inline admin links. 276 | * 277 | * @see system.admin.css 278 | */ 279 | .admin-link { 280 | font-size: 11px; 281 | font-weight: normal; 282 | text-transform: lowercase; 283 | } 284 | small .admin-link:before { 285 | content: '['; 286 | } 287 | small .admin-link:after { 288 | content: ']'; 289 | } 290 | 291 | #dreditor-overlay { 292 | margin-top: 18px; 293 | font-size: 13px; 294 | } 295 | #column-left { 296 | z-index: 2; 297 | } 298 | #dreditor-widget { 299 | position: fixed; 300 | bottom: 0; 301 | left: 2%; 302 | width: 94%; 303 | z-index: 10; 304 | overflow: auto; 305 | padding: 0 1em 1em; 306 | background-color: #fff; 307 | moz-box-shadow: 0 0 20px #bbb; 308 | box-shadow: 0 0 20px #bbb; 309 | moz-border-radius: 8px 8px 0 0; 310 | border-radius: 8px 8px 0 0; 311 | } 312 | /* Prevent the sticky cancel button from being located outside of the visible viewport. */ 313 | #dreditor-widget .sticky-cancel { 314 | bottom: 0; 315 | position: absolute; 316 | right: 1em; 317 | } 318 | .dreditor-actions { 319 | overflow: hidden; 320 | position: relative; 321 | } 322 | a.dreditor-application-toggle { 323 | display: inline-block; 324 | padding: 0.05em 0.3em; 325 | line-height: 150%; 326 | border: 1px solid #ccc; 327 | background-color: #fafcfe; 328 | font-weight: normal; 329 | text-decoration: none; 330 | .ajax-progress { 331 | float: right; 332 | margin: -1px -5px 0 2px; 333 | } 334 | } 335 | a.dreditor-application-toggle.active { 336 | border-color: #48e; 337 | background-color: #4af; 338 | color: #fff; 339 | } 340 | #page a.dreditor-application-toggle { 341 | float: right; 342 | margin: 0 0 0 0.5em; 343 | } 344 | .dreditor-input { 345 | border: 1px solid #ccc; 346 | padding: 0.2em 0.3em; 347 | font-size: 100%; 348 | line-height: 150%; 349 | moz-box-sizing: border-box; 350 | box-sizing: border-box; 351 | width: 100%; 352 | } 353 | .choice { 354 | display: inline-block; 355 | margin: 0 0.33em 0.4em 0; 356 | padding: 0.2em 0.7em; 357 | border: 1px solid #ccc; 358 | background-color: #fafcfe; 359 | moz-border-radius: 5px; 360 | border-radius: 5px; 361 | } 362 | .choice.selected { 363 | background-color: #2e96d5; 364 | border: 1px solid #28d; 365 | color: #fff; 366 | } 367 | div.dreditor-issuecount { 368 | line-height: 200%; 369 | } 370 | .dreditor-issuecount a { 371 | padding: 0 0.3em; 372 | } 373 | .marker.clickable { 374 | cursor: pointer; 375 | } 376 | #page .fieldset-flat { 377 | display: block; 378 | border: 0; 379 | width: auto; 380 | padding: 0; 381 | } 382 | .fieldset-flat > legend { 383 | display: none; 384 | } 385 | #dreditor-issue-data #edit-title-wrapper { 386 | margin-top: 0; 387 | } 388 | #dreditor-issue-data .inline-options .form-item { 389 | margin-bottom: 0.3em; 390 | } 391 | .dreditor-tooltip { 392 | display: none; 393 | position: fixed; 394 | bottom: 0; 395 | background-color: #ffffbf; 396 | border: 1px solid #000; 397 | padding: 0 3px; 398 | font-family: sans-serif; 399 | font-size: 11px; 400 | line-height: 150%; 401 | z-index: 100; 402 | } 403 | // D.o temporary "fixes". 404 | .field-name-field-issue-files table, .field-name-field-issue-changes table.nodechanges-file-changes { 405 | width: 100%; 406 | } 407 | .extended-file-field-table-cid, th[name="extended-file-field-table-header-cid"] { 408 | width: 100px; 409 | word-wrap: break-word; 410 | } 411 | .field-name-field-issue-changes table td .file { 412 | display: block; 413 | } 414 | td.extended-file-field-table-cid { 415 | text-align: right; 416 | } 417 | td.extended-file-field-table-cid .username { 418 | color: #777; 419 | display: block; 420 | font-size: 10px; 421 | } 422 | td.extended-file-field-table-filename .file, tr.pift-file-info .file { 423 | font-weight: 600; 424 | } 425 | td.extended-file-field-table-filename .file a, tr.pift-file-info .file a { 426 | display: block; 427 | overflow: hidden; 428 | } 429 | td.extended-file-field-table-filename .file .file-icon, tr.pift-file-info .file .file-icon { 430 | float: left; 431 | margin-right: .5em; 432 | } 433 | td.extended-file-field-table-filename .file .size, tr.nodechanges-file-changes .file .size { 434 | color: #999; 435 | float: right; 436 | font-size: 10px; 437 | margin-left: .5em; 438 | } 439 | tr.extended-file-field-table-row td, .field-name-field-issue-changes table.nodechanges-file-changes td { 440 | padding: .75em; 441 | } 442 | tr.extended-file-field-table-row:not(.pift-test-info) td.pift-pass, tr.extended-file-field-table-row:not(.pift-test-info) td.pift-fail, table.nodechanges-file-changes .pift-file-info td.pift-pass, table.nodechanges-file-changes .pift-file-info td.pift-fail { 443 | padding-bottom: 0; 444 | } 445 | tr.pift-test-info td { 446 | font-size: 11px; 447 | font-style: italic; 448 | padding: 0.5em .75em .75em 2.9em; 449 | } 450 | div.pift-operations { 451 | float: right; 452 | font-size: 10px; 453 | font-style: normal; 454 | font-weight: 600; 455 | margin-left: 1em; 456 | text-transform: uppercase; 457 | } 458 | -------------------------------------------------------------------------------- /templates/chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "%PKG.TITLE%", 4 | "homepage_url": "%PKG.HOMEPAGE%", 5 | "version": "%PKG.VERSION%", 6 | "description": "%PKG.DESCRIPTION%", 7 | "author": "%PKG.AUTHOR%", 8 | "icons": { "128": "%PKG.ICON%" }, 9 | "content_scripts": [ 10 | { 11 | "matches": [ 12 | "*://*.drupal.org/*", 13 | "*://*.dreditor.org/*", 14 | "*://*.devdrupal.org/*" 15 | ], 16 | "js": ["%PKG.NAME%.js"] 17 | } 18 | ], 19 | "permissions": [ 20 | "*://*.drupal.org/*", 21 | "*://*.dreditor.org/*", 22 | "*://*.devdrupal.org/*" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /templates/firefox/lib/main.js: -------------------------------------------------------------------------------- 1 | var data = require("sdk/self").data; 2 | var pageMod = require("sdk/page-mod"); 3 | pageMod.PageMod({ 4 | include: [ 5 | "*.drupal.org", 6 | "*.dreditor.org", 7 | "*.devdrupal.org" 8 | ], 9 | contentScriptFile: data.url("%PKG.NAME%.js") 10 | }); 11 | -------------------------------------------------------------------------------- /templates/firefox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "%PKG.NAME%", 3 | "title": "%PKG.TITLE%", 4 | "id": "%PKG.NAME%@%PKG.NAME%.org", 5 | "homepage": "%PKG.HOMEPAGE%", 6 | "description": "%PKG.DESCRIPTION%", 7 | "author": "%PKG.AUTHOR%", 8 | "icon": "%PKG.ICON%", 9 | "license": "%PKG.LICENSE%", 10 | "version": "%PKG.VERSION%" 11 | } 12 | -------------------------------------------------------------------------------- /templates/safari/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Author 6 | %PKG.AUTHOR% 7 | Builder Version 8 | 9537.71 9 | CFBundleDisplayName 10 | %PKG.TITLE% 11 | CFBundleIdentifier 12 | org.%PKG.NAME%.%PKG.NAME% 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleShortVersionString 16 | %PKG.VERSION% 17 | CFBundleVersion 18 | %PKG.VERSION% 19 | Chrome 20 | 21 | Database Quota 22 | 5242880 23 | 24 | Content 25 | 26 | Scripts 27 | 28 | End 29 | 30 | %PKG.NAME%.js 31 | 32 | 33 | 34 | Description 35 | %PKG.DESCRIPTION% 36 | ExtensionInfoDictionaryVersion 37 | 1.0 38 | Permissions 39 | 40 | Website Access 41 | 42 | Allowed Domains 43 | 44 | *.dreditor.org 45 | *.drupal.org 46 | *.devdrupal.org 47 | 48 | Include Secure Pages 49 | 50 | Level 51 | Some 52 | 53 | 54 | Update Manifest URL 55 | %PKG.HOMEPAGE%/update.plist 56 | Website 57 | %PKG.HOMEPAGE% 58 | 59 | 60 | -------------------------------------------------------------------------------- /templates/safari/Settings.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /templates/safari/update.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Extension Updates 6 | 7 | 8 | CFBundleIdentifier 9 | org.%PKG.NAME%.%PKG.NAME% 10 | Developer Identifier 11 | 69BZ7HZU59 12 | CFBundleVersion 13 | %PKG.VERSION% 14 | CFBundleShortVersionString 15 | %PKG.VERSION% 16 | URL 17 | %PKG.HOMEPAGE%/%PKG.NAME%.safariextz 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Writing QUnit tests 2 | 3 | As we write tests for the js code in the src tree we mimic it's structure same as Drupal 8 does. 4 | 5 | ## Directories 6 | 7 | - lib/ contains needed libraries like qunit 8 | - artifacts/ contains replicas from drupal.org like nodes and users 9 | - src/js where the js tests are following the same tree structure 10 | 11 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Basic infra test. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 25 | 26 | 27 |
    28 |
    29 | 30 | 31 | 32 |
    33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/lib/qunit-1.12.0.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.12.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | overflow: hidden; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | #qunit-modulefilter-container { 81 | float: right; 82 | } 83 | 84 | /** Tests: Pass/Fail */ 85 | 86 | #qunit-tests { 87 | list-style-position: inside; 88 | } 89 | 90 | #qunit-tests li { 91 | padding: 0.4em 0.5em 0.4em 2.5em; 92 | border-bottom: 1px solid #fff; 93 | list-style-position: inside; 94 | } 95 | 96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 97 | display: none; 98 | } 99 | 100 | #qunit-tests li strong { 101 | cursor: pointer; 102 | } 103 | 104 | #qunit-tests li a { 105 | padding: 0.5em; 106 | color: #c2ccd1; 107 | text-decoration: none; 108 | } 109 | #qunit-tests li a:hover, 110 | #qunit-tests li a:focus { 111 | color: #000; 112 | } 113 | 114 | #qunit-tests li .runtime { 115 | float: right; 116 | font-size: smaller; 117 | } 118 | 119 | .qunit-assert-list { 120 | margin-top: 0.5em; 121 | padding: 0.5em; 122 | 123 | background-color: #fff; 124 | 125 | border-radius: 5px; 126 | -moz-border-radius: 5px; 127 | -webkit-border-radius: 5px; 128 | } 129 | 130 | .qunit-collapsed { 131 | display: none; 132 | } 133 | 134 | #qunit-tests table { 135 | border-collapse: collapse; 136 | margin-top: .2em; 137 | } 138 | 139 | #qunit-tests th { 140 | text-align: right; 141 | vertical-align: top; 142 | padding: 0 .5em 0 0; 143 | } 144 | 145 | #qunit-tests td { 146 | vertical-align: top; 147 | } 148 | 149 | #qunit-tests pre { 150 | margin: 0; 151 | white-space: pre-wrap; 152 | word-wrap: break-word; 153 | } 154 | 155 | #qunit-tests del { 156 | background-color: #e0f2be; 157 | color: #374e0c; 158 | text-decoration: none; 159 | } 160 | 161 | #qunit-tests ins { 162 | background-color: #ffcaca; 163 | color: #500; 164 | text-decoration: none; 165 | } 166 | 167 | /*** Test Counts */ 168 | 169 | #qunit-tests b.counts { color: black; } 170 | #qunit-tests b.passed { color: #5E740B; } 171 | #qunit-tests b.failed { color: #710909; } 172 | 173 | #qunit-tests li li { 174 | padding: 5px; 175 | background-color: #fff; 176 | border-bottom: none; 177 | list-style-position: inside; 178 | } 179 | 180 | /*** Passing Styles */ 181 | 182 | #qunit-tests li li.pass { 183 | color: #3c510c; 184 | background-color: #fff; 185 | border-left: 10px solid #C6E746; 186 | } 187 | 188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 189 | #qunit-tests .pass .test-name { color: #366097; } 190 | 191 | #qunit-tests .pass .test-actual, 192 | #qunit-tests .pass .test-expected { color: #999999; } 193 | 194 | #qunit-banner.qunit-pass { background-color: #C6E746; } 195 | 196 | /*** Failing Styles */ 197 | 198 | #qunit-tests li li.fail { 199 | color: #710909; 200 | background-color: #fff; 201 | border-left: 10px solid #EE5757; 202 | white-space: pre; 203 | } 204 | 205 | #qunit-tests > li:last-child { 206 | border-radius: 0 0 5px 5px; 207 | -moz-border-radius: 0 0 5px 5px; 208 | -webkit-border-bottom-right-radius: 5px; 209 | -webkit-border-bottom-left-radius: 5px; 210 | } 211 | 212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 213 | #qunit-tests .fail .test-name, 214 | #qunit-tests .fail .module-name { color: #000000; } 215 | 216 | #qunit-tests .fail .test-actual { color: #EE5757; } 217 | #qunit-tests .fail .test-expected { color: green; } 218 | 219 | #qunit-banner.qunit-fail { background-color: #EE5757; } 220 | 221 | 222 | /** Result */ 223 | 224 | #qunit-testresult { 225 | padding: 0.5em 0.5em 0.5em 2.5em; 226 | 227 | color: #2b81af; 228 | background-color: #D2E0E6; 229 | 230 | border-bottom: 1px solid white; 231 | } 232 | #qunit-testresult .module-name { 233 | font-weight: bold; 234 | } 235 | 236 | /** Fixture */ 237 | 238 | #qunit-fixture { 239 | position: absolute; 240 | top: -10000px; 241 | left: -10000px; 242 | width: 1000px; 243 | height: 1000px; 244 | } 245 | -------------------------------------------------------------------------------- /tests/src/js/extensions/cache.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | storage tests 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 79 | 80 | 81 |
    82 |
    83 | 84 | 85 | 86 |
    87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /tests/src/js/extensions/storage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | storage tests 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 95 | 96 | 97 |
    98 |
    99 | 100 | 101 | 102 |
    103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /tests/src/js/plugins/form.backup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Form backup tests 8 | 9 | 10 | 11 | 12 | 13 | 14 | 28 | 29 | 32 | 33 | 34 | 35 | 93 | 94 | 95 |
    96 |
    97 | 98 |
    99 | 100 | 101 | 102 | 104 | 108 |
    109 |
    110 | 111 |
    112 | 113 | 114 | 115 | --------------------------------------------------------------------------------