From 6a9b4bee369af96d15dbe78b619e1514366ef6ff Mon Sep 17 00:00:00 2001 From: Mark Carver Date: Thu, 6 Oct 2016 21:42:07 +0100 Subject: [PATCH] Issue #1673278 by markcarver: Implement User Enhancements on Drupal.org and port Dreditor features --- drupalorg_project/drupalorg_project.module | 87 +++++ .../drupalorg_project.user_enhancements.inc | 200 +++++++++++ drupalorg_project/user_enhancements/.eslintrc | 90 +++++ .../user_enhancements/drupalorg.file.js | 394 +++++++++++++++++++++ .../user_enhancements/drupalorg.issue.clone.js | 72 ++++ .../drupalorg.issue.image.embed.js | 65 ++++ .../user_enhancements/drupalorg.issue.image.js | 145 ++++++++ .../user_enhancements/drupalorg.issue.js | 111 ++++++ .../user_enhancements/drupalorg.issue.patch.js | 166 +++++++++ .../drupalorg.issue.patch.review.css | 184 ++++++++++ .../drupalorg.issue.patch.review.js | 118 ++++++ .../drupalorg.issue.patch.simplytestme.js | 34 ++ .../drupalorg.issue.patchFilenameSuggestion.js | 30 ++ drupalorg_project/user_enhancements/drupalorg.js | 69 ++++ .../user_enhancements/drupalorg.markAsRead.js | 130 +++++++ .../user_enhancements/drupalorg.projectCollapse.js | 42 +++ 16 files changed, 1937 insertions(+) create mode 100644 drupalorg_project/drupalorg_project.user_enhancements.inc create mode 100644 drupalorg_project/user_enhancements/.eslintrc create mode 100644 drupalorg_project/user_enhancements/drupalorg.file.js create mode 100644 drupalorg_project/user_enhancements/drupalorg.issue.clone.js create mode 100644 drupalorg_project/user_enhancements/drupalorg.issue.image.embed.js create mode 100644 drupalorg_project/user_enhancements/drupalorg.issue.image.js create mode 100644 drupalorg_project/user_enhancements/drupalorg.issue.js create mode 100644 drupalorg_project/user_enhancements/drupalorg.issue.patch.js create mode 100644 drupalorg_project/user_enhancements/drupalorg.issue.patch.review.css create mode 100644 drupalorg_project/user_enhancements/drupalorg.issue.patch.review.js create mode 100644 drupalorg_project/user_enhancements/drupalorg.issue.patch.simplytestme.js create mode 100644 drupalorg_project/user_enhancements/drupalorg.issue.patchFilenameSuggestion.js create mode 100644 drupalorg_project/user_enhancements/drupalorg.js create mode 100644 drupalorg_project/user_enhancements/drupalorg.markAsRead.js create mode 100644 drupalorg_project/user_enhancements/drupalorg.projectCollapse.js diff --git a/drupalorg_project/drupalorg_project.module b/drupalorg_project/drupalorg_project.module index 6cc2a48..9d125a6 100644 --- a/drupalorg_project/drupalorg_project.module +++ b/drupalorg_project/drupalorg_project.module @@ -95,10 +95,55 @@ function drupalorg_project_menu() { ); } + // Provide a menu item for the drupalorg.issue.patch.review user_enhancement. + // @see drupalorg_project_review() + $items['node/%node/review/%'] = array( + 'page callback' => 'drupalorg_project_review', + 'page arguments' => array(1, 3), + 'access callback' => 'node_access', + 'access arguments' => array('view', 2), + 'type' => MENU_CALLBACK, + ); + return $items; } /** + * Callback for "node/%node/review/%". + * + * This is a helper callback. It has two primary purposes: + * - Redirect back to the node "view" callback. Yes, there normally would not + * be a callback for this path and node would just default to the "view" mode. + * However, if a user actually loaded this URL, then none of the blocks that + * have the "node/* /*" visibility restrictions would actually show up. Thus + * a redirect is needed. + * - Provide a fragment that indicates what the original "/review/%" value was. + * This is needed to allow the user enhancement to internally load the + * appropriate front-end based route. Unfortunately, due to a bug in + * vertical-tabs.js:53 (and possibly elsewhere in core), a fragment cannot + * be prefixed with a forward slash (/) as this would cause jQuery's sizzle + * engine to fail fantastically. Fortunately the router will recognize this + * path without it. + * + * @param stdClass $node + * The node object. + * @param string $digest + * A unique SHA1 digest to pass along. + */ +function drupalorg_project_review($node, $digest = NULL) { + $options = array(); + // Ensure that it's actually a valid SHA1 digest. + if (!empty($digest) && preg_match('/[0-9a-f]{5,40}/', $digest)) { + $options['fragment'] = 'review/' . $digest; + // Pass any provided query parameters onto the fragment. + if ($query = drupal_get_query_parameters()) { + $options['fragment'] .= '?' . drupal_http_build_query($query); + } + } + drupal_goto('node/' . $node->nid, $options); +} + +/** * Menu callback to map legacy project solr urls to the D7 equivilent. */ function _drupalorg_project_legacy_solr_redirect($type, $text = '') { @@ -1817,6 +1862,48 @@ function drupalorg_project_node_type_label($node_type, $label_type) { } /** + * Iterates over all child elements to look for tables that match. + * + * @param array $element + * The element render array, passed by reference. + */ +function _drupalorg_project_issue_extended_file_field_table_alter(array &$element) { + if (!isset($element['#theme']) || $element['#theme'] !== 'table') { + foreach (element_children($element) as $child) { + _drupalorg_project_issue_extended_file_field_table_alter($element[$child]); + } + return; + } + + // Move "author" into the "comment" column. + foreach ($element['#rows'] as &$row) { + foreach ($row['data'] as $key => $cell) { + if ($key === 'uid' && isset($row['data']['cid']['data'])) { + $row['data']['cid']['data'] .= '' . $cell['data'] . ''; + unset($row['data']['uid']); + } + } + } + // Remove the "author" header. + unset($element['#header']['uid']); +} + +/** + * Implements hook_extended_file_field_output_alter(). + */ +function drupalorg_project_extended_file_field_output_alter(&$elements, &$context) { + $entity = $context['entity']; + + // Only affect issues. + if (!isset($entity->type) || $entity->type !== 'project_issue') { + return; + } + + // Alter the tables. + _drupalorg_project_issue_extended_file_field_table_alter($elements); +} + +/** * Implements hook_field_extra_fields_display_alter. */ function drupalorg_project_field_extra_fields_display_alter(&$displays, $context) { diff --git a/drupalorg_project/drupalorg_project.user_enhancements.inc b/drupalorg_project/drupalorg_project.user_enhancements.inc new file mode 100644 index 0000000..d694462 --- /dev/null +++ b/drupalorg_project/drupalorg_project.user_enhancements.inc @@ -0,0 +1,200 @@ +Some of these user enhancements are a port from a browser extension previously known as "Dreditor". As such, these user enhancements are JavaScript based in nature and require that it is enabled in your browser for you to use them. Over time, these user enhancements may become official features of the site or, quite possibly, replaced entirely with better solutions.

', array( + '!1673278' => 'https://www.drupal.org/node/1673278', + '!2786361' => 'https://www.drupal.org/node/2786361', + )); + return array( + 'projects' => array('title' => t('Projects'), 'description' => $description), + 'issues' => array('title' => t('Issues'), 'description' => $description), + ); +} + +/** + * Implements hook_user_enhancements_info(). + */ +function drupalorg_project_user_enhancements_info() { + // Dreditor (separate CSS and JS). + $enhancements['dreditor.css'] = array( + 'ui' => FALSE, + 'css' => array( + 'sites/all/libraries/dreditor/dist/css/dreditor.min.css' => array(), + ), + ); + $enhancements['dreditor.js'] = array( + 'ui' => FALSE, + 'js' => array( + 'sites/all/libraries/dreditor/dist/js/dreditor.min.js' => array(), + ), + ); + + // Dreditor UI. + $enhancements['dreditor.ui'] = array( + 'ui' => FALSE, + 'dependencies' => array('dreditor.css', 'dreditor.js', array('system', 'jquery')), + 'js' => array( + 'sites/all/libraries/dreditor-ui/dist/js/dreditor-ui.min.js' => array('requires_jquery' => TRUE), + ), + 'css' => array( + 'sites/all/libraries/dreditor-ui/dist/css/dreditor-ui.min.css' => array(), +`` ), + ); + + // Non-UI based enhancements (base/shared functionality). + $enhancements['drupalorg'] = array( + 'dependencies' => array('dreditor.css', 'user.enhancements'), + 'ui' => FALSE, + ); + $enhancements['drupalorg.file'] = array( + 'dependencies' => array('drupalorg'), + 'ui' => FALSE, + ); + $enhancements['drupalorg.issue'] = array( + 'dependencies' => array('drupalorg'), + 'ui' => FALSE, + ); + $enhancements['drupalorg.issue.image'] = array( + 'dependencies' => array('drupalorg.file', 'drupalorg.issue'), + 'ui' => FALSE, + ); + $enhancements['drupalorg.issue.patch'] = array( + 'dependencies' => array('drupalorg.file', 'drupalorg.issue'), + 'ui' => FALSE, + ); + + // General enhancements. + $enhancements['drupalorg.markAsRead'] = array( + 'dependencies' => array('drupalorg'), + 'title' => t('Mark as read'), + 'description' => t('Makes the "new" marker next to links in tables and item lists clickable so it can be "Marked it as read" without actually having to view the link. It also adds "Mark all as read" button(s) relative to the table or item list that contains the markers.'), + 'settings' => array( + 'hideArtifacts' => 1, + ), + ); + + // Project enhancements. + $enhancements['drupalorg.projectCollapse'] = array( + 'dependencies' => array('drupalorg'), + 'group' => 'projects', + 'title' => t('Collapsible "Your projects"'), + 'description' => t('Adds a "Collapse projects" toggle button at the top of the "Your Projects" tab of your profile. When toggled, it will show/hide your list of projects. This is especially helpful if you have a lot of projects.'), + 'conditions' => array( + array('path' => 'project/user'), + ), + ); + + // Issue enhancements. + $enhancements['drupalorg.issue.clone'] = array( + 'dependencies' => array('drupalorg.issue'), + 'group' => 'issues', + 'title' => t('Clone this issue'), + 'description' => t('Adds a "Clone this issue" button at the bottom of the issue meta data block in the sidebar. When clicked it will open a new tab/window to create a new issue in that project and populate the current issue values into that new tab/window.'), + 'conditions' => array( + array('entity' => 'node:project_issue'), + ), + ); + $enhancements['drupalorg.issue.image.embed'] = array( + 'dependencies' => array('drupalorg.issue.image'), + 'group' => 'issues', + 'title' => t('Image embed'), + 'description' => t('Adds an "Embed" button next to each image file. When clicked, it will insert the image into the issue comment.'), + 'conditions' => array( + array('entity' => 'node:project_issue'), + ), + 'settings' => array( + 'trackLastFocused' => 1 + ), + ); + $enhancements['drupalorg.issue.patchFilenameSuggestion'] = array( + 'dependencies' => array('drupalorg.issue'), + 'group' => 'issues', + 'title' => t('Patch filename suggestion'), + 'description' => t('Adds a "Patch filename suggestion" button at the top of the "Files" fields on an issue. When clicked, it will suggest a patch filename based on the current state of the issue.'), + 'conditions' => array( + array('entity' => 'node:project_issue'), + ), + ); + $enhancements['drupalorg.issue.patch.review'] = array( + 'dependencies' => array(array('codefilter_prism', 'prism'), 'dreditor.ui', 'drupalorg.issue.patch'), + 'group' => 'issues', + 'title' => t('Patch review'), + 'experimental' => TRUE, + 'description' => t('Adds a "Review" button next to each patch and inner-diff file. When clicked, it will open the patch review overlay.'), + 'conditions' => array( + array('entity' => 'node:project_issue'), + ), + ); + $enhancements['drupalorg.issue.patch.simplytestme'] = array( + 'dependencies' => array('drupalorg.issue.patch'), + 'group' => 'issues', + 'title' => t('Patch simplytest.me'), + 'description' => t('Adds a "simplytest.me" button next to each patch file. When clicked, it will open a new tab with the patch URL already pre-filled on the simplytest.me site.'), + 'conditions' => array( + array('entity' => 'node:project_issue'), + ), + ); + + return $enhancements; +} + +/** + * Implements hook_user_enhancements_settings_form(). + */ +function drupalorg_project_user_enhancements_settings_form(array $enhancement) { + $element = array(); + + switch ($enhancement['name']) { + case 'drupalorg.markAsRead': + $element['hideArtifacts'] = array( + '#type' => 'checkbox', + '#title' => t('Hide artifacts'), + '#description' => t('Sometimes, nodes on Drupal.org get deleted (for various reasons). However, some lists still show a artifact "new" marker for a non-existing node. This setting conveniently hides these artifacts from the page.'), + '#default_value' => !empty($enhancement['settings']['hideArtifacts']) ? 1 : 0, + ); + break; + + case 'drupalorg.issue.image.embed': + $element['trackLastFocused'] = array( + '#type' => 'checkbox', + '#title' => t('Track last focused textarea'), + '#description' => t('Enabling this will embed the image in the last focused textarea, defaulting to the issue comment or, as a last resort, the issue summary.'), + '#default_value' => !empty($enhancement['settings']['trackLastFocused']) ? 1 : 0, + ); + break; + } + + return $element; +} + +/** + * Implements hook_user_enhancements_js_settings_alter(). + */ +function drupalorg_project_user_enhancements_js_settings_alter(array &$settings) { + $node = menu_get_object(); + if (!$node || !$settings) { + return; + } + + foreach ($settings as $name => &$setting) { + switch ($name) { + // Fill in the current issue values. + case 'drupalorg.issue': + $setting['commentCount'] = isset($node->comment_count) ? (int) $node->comment_count : 0; + $setting['nid'] = (int) $node->nid; + if (isset($node->field_project[LANGUAGE_NONE][0]['entity'])) { + $machine_name = field_get_items('node', $node->field_project[LANGUAGE_NONE][0]['entity'], 'field_project_machine_name'); + $setting['project'] = $machine_name[0]['value']; + } + $setting['title'] = check_plain($node->title); + break; + } + } +} diff --git a/drupalorg_project/user_enhancements/.eslintrc b/drupalorg_project/user_enhancements/.eslintrc new file mode 100644 index 0000000..8827833 --- /dev/null +++ b/drupalorg_project/user_enhancements/.eslintrc @@ -0,0 +1,90 @@ +{ + "extends": "eslint:recommended", + "env": { + "browser": true + }, + "globals": { + "Drupal": true, + "jQuery": true, + "Prism": true, + "Promise": true, + "DreditorUI": true, + "UserEnhancements": true + }, + "rules": { + // Errors. + "array-bracket-spacing": [2, "never"], + "block-scoped-var": 2, + "brace-style": [2, "stroustrup", {"allowSingleLine": true}], + "comma-dangle": [2, "never"], + "comma-spacing": 2, + "comma-style": [2, "last"], + "computed-property-spacing": [2, "never"], + "curly": [2, "all"], + "eol-last": 2, + "eqeqeq": [2, "smart"], + "guard-for-in": 2, + "indent": [2, 2, {"SwitchCase": 1}], + "key-spacing": [2, {"beforeColon": false, "afterColon": true}], + "linebreak-style": [2, "unix"], + "lines-around-comment": [2, {"beforeBlockComment": true, "afterBlockComment": false}], + "new-parens": 2, + "no-array-constructor": 2, + "no-caller": 2, + "no-catch-shadow": 2, + "no-eval": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-extra-parens": [2, "functions"], + "no-implied-eval": 2, + "no-iterator": 2, + "no-label-var": 2, + "no-labels": 2, + "no-lone-blocks": 2, + "no-loop-func": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-native-reassign": 2, + "no-nested-ternary": 2, + "no-new-func": 2, + "no-new-object": 2, + "no-new-wrappers": 2, + "no-octal-escape": 2, + "no-process-exit": 2, + "no-proto": 2, + "no-return-assign": 2, + "no-script-url": 2, + "no-sequences": 2, + "no-shadow-restricted-names": 2, + "no-spaced-func": 2, + "no-trailing-spaces": 2, + "no-undef-init": 2, + "no-undefined": 2, + "no-unused-expressions": 2, + "no-unused-vars": [2, {"vars": "all", "args": "none"}], + "no-with": 2, + "object-curly-spacing": [2, "never"], + "one-var": [2, "never"], + "quote-props": [2, "consistent-as-needed"], + "quotes": [2, "single", "avoid-escape"], + "semi": [2, "always"], + "semi-spacing": [2, {"before": false, "after": true}], + "space-before-blocks": [2, "always"], + "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "space-unary-ops": [2, { "words": true, "nonwords": false }], + "spaced-comment": [2, "always", { "exceptions": ["-", "+", "!"] }], + "strict": 2, + "yoda": [2, "never"], + // Warnings. + "max-nested-callbacks": [1, 3], + "valid-jsdoc": [1, { + "prefer": { + "returns": "return", + "property": "prop" + }, + "requireReturn": false + }] + } +} diff --git a/drupalorg_project/user_enhancements/drupalorg.file.js b/drupalorg_project/user_enhancements/drupalorg.file.js new file mode 100644 index 0000000..eb1c2a5 --- /dev/null +++ b/drupalorg_project/user_enhancements/drupalorg.file.js @@ -0,0 +1,394 @@ +(function ($, Drupal, UserEnhancements) { + 'use strict'; + + /** + * @namespace FileObject + */ + var FileObject = { + + /** + * The absolute URL. + * + * @type {String} + */ + absolute: null, + + /** + * The base filename, without the extension. + * + * @type {String} + */ + basename: null, + + /** + * An anchor element that the values were extract from. + * + * @type {HTMLAnchorElement} + */ + element: null, + + /** + * The file extensions. + * + * @type {String} + */ + extension: null, + + /** + * The filename. + * + * @type {String} + */ + filename: null, + + /** + * The file relative URL (minus the origin). + * + * @type {String} + */ + relative: null, + + /** + * The file size. + * + * @type {Number} + */ + size: null, + + /** + * The file mime type. + * + * @type {String} + */ + type: null, + + /** + * The identifier of the user who is associated with this file, if any. + */ + uid: null, + + /** + * The absolute URL. + * + * An alias for FileObject.absolute. + * + * @type {String} + * + * @see FileObject.absolute + */ + url: null + + }; + + window.FileObject = FileObject; + + /** + * @namespace UserEnhancement.drupalorg.file + * + * @extends UserEnhancement.drupalorg + */ + UserEnhancements + .create('drupalorg.file', { + + /** + * @type {Array} + */ + fileProcessCallbacks: [], + + /** + * @type {Array} + */ + fileAttachmentCallbacks: [], + + /** + * @type {Array} + */ + files: [], + + /** + * The callback to invoke when attaching files. + * + * @callback UserEnhancement.drupalorg.file~attachToFilesCallback + * + * @this {UserEnhancement.drupalorg.file} + */ + + /** + * Attaches a callback for files found on the page. + * + * @param {UserEnhancement.drupalorg.file~attachToFilesCallback} callback + * The callback to invoke. + * + * @chainable + * + * @return {UserEnhancement.drupalorg.file} + * The user enhancement object. + */ + attachToFiles: function (callback) { + this.fileAttachmentCallbacks.push(callback); + return this; + }, + + /** + * Extracts the basename of a path; similar to the PHP basename function. + * + * @param {String} filename + * The filename to extract the basename from. + * @param {String} [suffix] + * Optional. The suffix to strip off. + * + * @return {String} + * The basename of a path. + * + * @see http://locutus.io/php/filesystem/basename/ + */ + basename: function (filename, suffix) { + var b = filename; + var lastChar = b.charAt(b.length - 1); + if (lastChar === '/' || lastChar === '\\') { + b = b.slice(0, -1); + } + b = b.replace(/^.*[\/\\]/g, ''); + if (typeof suffix === 'string' && b.substr(b.length - suffix.length) === suffix) { + b = b.substr(0, b.length - suffix.length); + } + return b; + }, + + /** + * Constructs a new file object from an anchor node element + * + * @param {Object} values + * The values to assign to the new FileObject. + * + * @return {FileObject} + * The new FileObject. + */ + createFileObject: function (values) { + var file = Object.create(FileObject); + values = values || {}; + for (var i in values) { + if (!values.hasOwnProperty(i)) { + continue; + } + file[i] = values[i]; + } + file.url = file.absolute; + return file; + }, + + /** + * Creates a new FileObject object from found links. + * + * @param {HTMLAnchorElement} anchor + * An anchor node element. + * + * @return {FileObject} + * The newly created FileObject object. + */ + createFromAnchor: function (anchor) { + if (!(anchor instanceof HTMLAnchorElement)) { + this.fatal('The argument passed to construct a new FileObject object from an anchor must be an instance of HTMLAnchorElement.'); + } + + var values = { + absolute: anchor.href, + element: anchor, + relative: anchor.href.replace(new RegExp('^' + anchor.origin || window.location.origin), '') + }; + + values.extension = this.extension(anchor.href); + values.basename = this.basename(anchor.href, '.' + values.extension); + values.filename = [values.basename, values.extension].join('.'); + + // Extract the content type, if provided. + var contentType = anchor.type && anchor.type.split(/;\s*/) || []; + + // Extract the file mime type, if provided. + values.type = contentType && contentType.shift() || null; + + // Extract key/value pairs from the content type. + contentType.forEach(function (item) { + var parts = item.split('='); + // Extract the file size from the provided content type, if provided. + if (parts[0] === 'length') { + values.size = parseInt(parts[1] || 0); + } + }); + + return this.createFileObject(values); + }, + + /** + * Retrieves a file's extension. + * + * @param {String} filename + * The filename to extract the extension from. + * + * @return {String} + * The extension. + * + * @see http://stackoverflow.com/a/12900504 + */ + extension: function (filename) { + return /tar\.gz$/.test(filename) ? 'tar.gz' : filename.slice((filename.lastIndexOf('.') - 1 >>> 0) + 2); + }, + + /** + * Filters found files based on certain conditions. + * + * @param {Array} conditions + * An array of test conditions. Only one array item test condition must + * return true for a "match" to be found. Each array item must be an + * object where all the properties must match against the property found + * on the FileObject object. The value of a test object property + * can be one of: {String}, {String[]}, {RegExp} or {RegExp[]}. + * + * @return {FileObject[]} + * An array of filtered FileObject objects. + * + * @see UserEnhancement.drupalorg.file.match + */ + filter: function (conditions) { + var files = []; + this.files.forEach(function (file) { + if (this.match(file, conditions)) { + files.push(file); + } + }, this); + return files; + }, + + /** + * Determines if a file matches certain conditions. + * + * @param {FileObject} file + * The file object. + * @param {Array} conditions + * An array of test conditions. Only one array item test condition must + * return true for a "match" to be found. Each array item must be an + * object where all the properties must match against the property found + * on the FileObject object. The value of a test object property + * can be one of: {String}, {String[]}, {RegExp} or {RegExp[]}. + * + * @return {boolean} + * Returns `true` or `false`. + * + * @see UserEnhancement.drupalorg.file.filter + */ + match: function (file, conditions) { + var fileMatched; + + conditions = conditions || []; + conditions.forEach(function (condition) { + // If there is already a match, skip. + if (fileMatched) { + return; + } + + /** + * The callback to invoke when attaching files. + * + * @callback UserEnhancement.drupalorg.file~matchConditionCallback + * + * @param {FileObject} file + * The file being matched against. + * + * @return {Boolean} + * If the condition matches. + * + * @this UserEnhancement.drupalorg.file + */ + if (typeof condition === 'function') { + fileMatched = !!condition.apply(this, [file]); + } + // Otherwise, iterate over and test the file properties. + else if (typeof condition === 'object') { + // Iterate over the properties, ensuring that they all match. + var filePropertiesMatched = true; + for (var property in condition) { + // Immediately skip if the condition doesn't own the property or + // if even one of the properties has failed to match. + if (!condition.hasOwnProperty(property) || !filePropertiesMatched) { + continue; + } + + // If the file doesn't have the property, it hasn't matched. + if (!file.hasOwnProperty(property)) { + filePropertiesMatched = false; + return; + } + + // Typecast the value to an array of values. + var values = [].concat(condition[property]); + + // If any of the values match, the entire file property matched. + var filePropertyMatched = false; + for (var i = 0, l = values.length; i < l; i++) { + // If there is already a match, skip. + if (filePropertyMatched) { + return; + } + + /** + * The callback to invoke when attaching files. + * + * @callback UserEnhancement.drupalorg.file~matchConditionFilePropertyCallback + * + * @param {FileObject} file + * The file being matched against. + * + * @return {Boolean} + * If the condition matches. + * + * @this UserEnhancement.drupalorg.file + */ + var value = values[i]; + if (typeof value === 'function') { + value = value.apply(this, [file]); + } + filePropertyMatched = value instanceof RegExp ? value.test(file[property]) : value === file[property]; + } + + // Bubble up whether or not the individual property matched. + filePropertiesMatched = !!filePropertyMatched; + } + // Bubble up whether or not all the file's properties match. + fileMatched = filePropertiesMatched; + } + }, this); + + return !!fileMatched; + } + + }) + .attach('.file > a', function ($selectors) { + var self = this; + $selectors.each(function () { + // Create the FileObject. + var file = self.createFromAnchor(this); + + // Attempt to find the user associated with this file and add the uid. + var $file = $(this); + var $parent = $file.parents('.comment').first(); + if (!$parent[0]) { + $parent = $file.parents('tr').first(); + } + if ($parent[0]) { + var uid = $parent.find('.username').first().data('uid'); + if (uid) { + file.uid = uid; + } + } + + self.files.push(file); + }); + + // Invoke each file attachment callback. + this.fileAttachmentCallbacks.forEach(function (callback) { + callback.call(this); + }, this); + }) + ; + +})(jQuery, Drupal, UserEnhancements); diff --git a/drupalorg_project/user_enhancements/drupalorg.issue.clone.js b/drupalorg_project/user_enhancements/drupalorg.issue.clone.js new file mode 100644 index 0000000..bf26d54 --- /dev/null +++ b/drupalorg_project/user_enhancements/drupalorg.issue.clone.js @@ -0,0 +1,72 @@ +(function ($, Drupal, UserEnhancements, _window) { + 'use strict'; + + var selector = '.region-sidebar-first .issue-update'; + + /** + * @namespace UserEnhancement.drupalorg.issue.clone + * + * @extends UserEnhancement.drupalorg.issue + */ + UserEnhancements.create('drupalorg.issue.clone', { + $wrapper: $(''), + $element: $('' + Drupal.t('Clone this issue') + '') + }) + .detachElements(selector) + .attachElements('append', selector, function () { + this.$originalForm = $('#project-issue-node-form'); + }) + .on('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + + // Get the project. + this.project = this.getProject(); + this.parentNid = this.getNid(); + if (this.project) { + // Open a new window. + this.newWindow = _window.open('/node/add/project-issue/' + this.project + '#project-issue-node-form', '_blank'); + + // $(w).bind('load') does not actually bind to the new window "load" + // event. This may be on purpose or a bug with the currently used + // jQuery version on d.o (1.4.4). + this.newWindow.addEventListener('load', function () { + // Retrieve the DOM of the newly created window. + var $newDocument = $(this.newWindow.document); + $newDocument.ready(function () { + var $newForm = $newDocument.contents().find('#project-issue-node-form'); + var selector, selectors = [ + '#edit-title', + '#edit-body-und-0-value', + '#edit-field-issue-category-und', + '#edit-field-issue-priority-und', + '#edit-field-issue-status-und', + '#edit-field-issue-version-und', + '#edit-field-issue-component-und', + '#edit-field-issue-assigned-und', + '#edit-taxonomy-vocabulary-9-und' + ]; + for (selector in selectors) { + $newForm.find(selectors[selector]).val(this.$originalForm.find(selectors[selector]).val()); + } + + // Prepend body with "Follow-up to ..." line. + var $body = $newForm.find('#edit-body-und-0-value'); + $body.val('Follow-up to [#' + this.parentNid + ']\n\n' + $body.val()); + + // Add originating issue was parent issue relationship. + $newForm + .find('#edit-field-issue-parent-und-0-target-id') + .val(this.$originalForm.find('#edit-title').val() + ' (' + this.parentNid + ')'); + + // Ensure all fieldsets are expanded. + $newForm.find('.collapsed').removeClass('collapsed'); + + // Focus on the new issue title so users can enter it. + $newForm.find('#edit-title').focus(); + }.bind(this)); + }.bind(this), false); + } + }); + +})(jQuery, Drupal, UserEnhancements, window); diff --git a/drupalorg_project/user_enhancements/drupalorg.issue.image.embed.js b/drupalorg_project/user_enhancements/drupalorg.issue.image.embed.js new file mode 100644 index 0000000..653b39d --- /dev/null +++ b/drupalorg_project/user_enhancements/drupalorg.issue.image.embed.js @@ -0,0 +1,65 @@ +(function ($, Drupal, UserEnhancements) { + 'use strict'; + + /** + * @namespace UserEnhancement.drupalorg.issue.image.embed + * + * @extends UserEnhancement.drupalorg.issue.image + */ + UserEnhancements + .create('drupalorg.issue.image.embed', { + $element: $(''), + $body: $(), + $comment: $(), + $last: $(), + settings: { + trackLastFocused: true + } + }) + .ready(function () { + this.$body = $('textarea[name="body[und][0][value]"]'); + this.$comment = $('textarea[name="nodechanges_comment[comment_body][und][0][value]"]'); + + // Keep track of the last focused textarea element. + if (this.getSetting('trackLastFocused')) { + $(document).on('focus', 'textarea', function (e) { + this.$last = $(e.currentTarget); + }.bind(this)); + } + }) + .attachElementToImage() + .on('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + + // Retrieve the file object. + var file = this.getFileFromEvent(e); + if (!file) { + return; + } + + // Attempt to retrieve the last focused textarea. + var $target = this.getSetting('trackLastFocused') ? this.$last : $(); + + // If no target, default to the comment or, as a last resort, the body. + if (!$target[0]) { + $target = this.$comment[0] ? this.$comment : this.$body; + } + + // Scroll to the target. + this.scrollTo($target, { offset: -60 }); + + // Retrieve the target's current value. + var value = $target.val(); + + // Append a new line if there is already content. + if (value.length) { + value += "\n"; + } + + // Focus the textarea and then embed the image. + $target.focus().val(value + "\"""); + }) + ; + +})(jQuery, Drupal, UserEnhancements); diff --git a/drupalorg_project/user_enhancements/drupalorg.issue.image.js b/drupalorg_project/user_enhancements/drupalorg.issue.image.js new file mode 100644 index 0000000..03e1472 --- /dev/null +++ b/drupalorg_project/user_enhancements/drupalorg.issue.image.js @@ -0,0 +1,145 @@ +(function ($, Drupal, UserEnhancements) { + 'use strict'; + + /** + * @typedef {FileObject} ImageFileObject + * + * @property {Boolean} image + * Flag indicating whether or not a file is an image. + */ + + /** + * @namespace UserEnhancement.drupalorg.issue.image + * + * @extends UserEnhancement.drupalorg.issue + */ + UserEnhancements.create('drupalorg.issue.image', { + + /** + * The attached elements. + * + * @type {jQuery} + */ + $elements: $(), + + /** + * Patch attachment callbacks. + * + * @type {Function[]} + */ + imageAttachments: [], + + /** + * Patch files. + * + * @type {FileObject[]} + */ + images: [], + + /** + * A callback that is invoked when an image's element is being attached. + * + * @typedef {Function} UserEnhancement.drupalorg.issue.image.attachElementToImage~callback + * + * @param {jQuery} $element + * The element that was cloned from the original element. + * @param {ImageFileObject} file + * The image file. + * + * @return {Boolean} + */ + + /** + * Attaches the defined element to a image file. + * + * @param {UserEnhancement.drupalorg.issue.image.attachElementToImage~callback} [callback] + * Optional. A callback to invoke when the element is being attached. + * + * @return {UserEnhancement.drupalorg.issue.image} + */ + attachElementToImage: function (callback) { + this.imageAttachments.push(function (file) { + var $element = this.$element.clone(true, true); + if (typeof callback === 'function') { + var ret = callback.apply(this, [$element, file]); + if (ret === false) { + $element.remove(); + return; + } + } + $element.data(this.name, file); + this.$elements = this.$elements.add($element); + $(file.element).parent().before($element); + }.bind(this)); + return this; + }, + + /** + * Retrieves the FileObject object. + * + * If the associated current target has a with an element + * from an event, if any. + * + * @param {Event} e + * The jQuery event object. + * + * @return {FileObject|void} + */ + getFileFromEvent: function (e) { + return $(e.currentTarget).data(this.name); + } + + }); + + /** + * The "drupalorg.file" user enhancement object. + * + * @type {UserEnhancement.drupalorg.file} + */ + var files = UserEnhancements.get('drupalorg.file'); + + /** + * The "drupalorg.issue.image" user enhancement object. + * + * @type {UserEnhancement.drupalorg.issue.image} + */ + var image = UserEnhancements.get('drupalorg.issue.image'); + + /** + * @type {UserEnhancement.drupalorg.file~attachToFilesCallback} + * + * @this UserEnhancement.drupalorg.file + */ + files.attachToFiles(function () { + image.images = this.filter([ + /** + * @type {UserEnhancement.drupalorg.file~matchConditionCallback} + * + * @param {ImageFileObject} file + * The file being matched against. + * + * @this UserEnhancement.drupalorg.file + */ + function (file) { + // Two tests, either will match. + if (this.match(file, [{ type: ['image/jpg', 'image/jpeg', 'image/gif', 'image/png'] }, { extension: ['jpg', 'jpeg', 'gif', 'png'] }])) { + file.image = true; + return true; + } + } + ]); + + /** + * Iterate over each image and attach it. + * + * @param {FileObject} file + * The image file. + */ + image.images.forEach(function (file) { + image.imageAttachments.forEach(function (callback) { + callback.apply(image, [file]); + }); + }); + }); + +})(jQuery, Drupal, UserEnhancements); diff --git a/drupalorg_project/user_enhancements/drupalorg.issue.js b/drupalorg_project/user_enhancements/drupalorg.issue.js new file mode 100644 index 0000000..2b016ee --- /dev/null +++ b/drupalorg_project/user_enhancements/drupalorg.issue.js @@ -0,0 +1,111 @@ +(function ($, Drupal, UserEnhancements) { + 'use strict'; + + /** + * @namespace UserEnhancement.drupalorg.issue + * + * @extends UserEnhancement.drupalorg + */ + UserEnhancements.create('drupalorg.issue', { + + settings: { + commentCount: null, + nid: null, + project: null, + title: null + }, + + /** + * Retrieve the project short name. + * + * Attempt to first use any passed JS settings value, falling back to + * extrapolating the value from somewhere in the DOM. + */ + getProject: function () { + return this.getSetting('project', function () { + // Retrieve project from breadcrumb. + var project = $('.breadcrumb a:eq(0)').attr('href'); + // @todo The comment preview page does not contain a breadcrumb and + // also does not expose the project name anywhere else. + // The Drupal (core) project breadcrumb does not contain a project page link. + return (project && project === '/project/issues/drupal' ? 'drupal' : project.substr(9)) || false; + }); + }, + + /** + * Gets the selected version. + * + * Variations: + * 7.x + * 7.x-dev + * 7.x-alpha1 + * 7.20 + * 7.x-1.x + * 7.x-1.12 + * 7.x-1.x + * - 8.x issues - + * - Any - + * All-versions-4.x-dev + */ + getSelectedVersion: function () { + // Retrieve version from the comment form selected option label. + return $(':input[name*="issue_version"] :selected').text(); + }, + + /** + * Gets the selected core version. + * + * Variations: + * 7.x + * 7.20 + */ + getSelectedVersionCore: function () { + var version = this.getSelectedVersion(); + var matches = version.match(/^(\d+\.[x\d]+)/); + if (matches) { + return matches[0]; + } + else { + return false; + } + }, + + /** + * Gets the selected contrib version. + * + * Variations: + * 1.x + * 1.2 + */ + getSelectedVersionContrib: function () { + var version = this.getSelectedVersion(); + var matches = version.match(/^\d+\.x-(\d+\.[x\d]+)/); + if (matches) { + return matches[1]; + } + else { + return false; + } + }, + + /** + * Gets the selected core + contrib version. + * + * Variations: + * 7.x-1.x + * 7.x-1.2 + */ + getSelectedVersionCoreContrib: function () { + var version = this.getSelectedVersion(); + var matches = version.match(/^(\d+\.x-\d+\.[x\d]+)/); + if (matches) { + return matches[0]; + } + else { + return false; + } + } + + }); + +})(jQuery, Drupal, UserEnhancements); diff --git a/drupalorg_project/user_enhancements/drupalorg.issue.patch.js b/drupalorg_project/user_enhancements/drupalorg.issue.patch.js new file mode 100644 index 0000000..3b24034 --- /dev/null +++ b/drupalorg_project/user_enhancements/drupalorg.issue.patch.js @@ -0,0 +1,166 @@ +(function ($, Drupal, UserEnhancements) { + 'use strict'; + + /** + * @typedef {FileObject} PatchFileObject + * + * @property {Boolean} interdiff + * Flag indicating whether or not a file is an interdiff file. + * @property {Boolean} patch + * Flag indicating whether or not a file is a patch file. + */ + + /** + * @namespace UserEnhancement.drupalorg.issue.patch + * + * @extends UserEnhancement.drupalorg.issue + */ + UserEnhancements.create('drupalorg.issue.patch', { + + /** + * The attached elements. + * + * @type {jQuery} + */ + $elements: $(), + + /** + * Patch attachment callbacks. + * + * @type {Function[]} + */ + patchAttachments: [], + + /** + * Patch files. + * + * @type {PatchFileObject[]} + */ + patches: [], + + /** + * A callback that is invoked when an patch's element is being attached. + * + * @typedef {Function} UserEnhancement.drupalorg.issue.patch.attachElementToPatch~callback + * + * @param {jQuery} $element + * The element that was cloned from the original element. + * @param {PatchFileObject} file + * The patch file. + * + * @return {Boolean} + */ + + /** + * Attaches the defined element to a patch file. + * + * @param {UserEnhancement.drupalorg.issue.patch.attachElementToPatch~callback} [callback] + * Optional. A callback to invoke when the element is being attached. + * + * @return {UserEnhancement.drupalorg.issue.patch} + * The user enhancement object. + */ + attachElementToPatch: function (callback) { + this.patchAttachments.push(function (file) { + var $element = this.$element.clone(true, true); + if (typeof callback === 'function') { + var ret = callback.apply(this, [$element, file]); + if (ret === false) { + $element.remove(); + return; + } + } + $element.data(this.name, file); + this.$elements = this.$elements.add($element); + $(file.element).parent().before($element); + }.bind(this)); + return this; + }, + + /** + * Retrieves the PatchFileObject object. + * + * If the associated current target has a with an element + * from an event, if any. + * + * @param {Event} e + * The jQuery event object. + * + * @return {PatchFileObject|null} + * The PatchFileObject or null. + */ + getFileFromEvent: function (e) { + return $(e.currentTarget).data(this.name) || null; + } + + }); + + /** + * The "drupalorg.file" user enhancement object. + * + * @type {UserEnhancement.drupalorg.file} + */ + var files = UserEnhancements.get('drupalorg.file'); + + /** + * The "drupalorg.issue.patch" user enhancement object. + * + * @type {UserEnhancement.drupalorg.issue.patch} + */ + var patch = UserEnhancements.get('drupalorg.issue.patch'); + + /** + * @type {UserEnhancement.drupalorg.file~attachToFilesCallback} + * + * @this UserEnhancement.drupalorg.file + */ + files.attachToFiles(function () { + + /** + * @type {UserEnhancement.drupalorg.file~matchConditionCallback} + * + * @param {PatchFileObject} file + * The file being matched against. + * + * @return {Boolean} + * If the condition matches. + * + * @this UserEnhancement.drupalorg.file + */ + patch.patches = this.filter([ + function (file) { + // Single test, all properties must match. + var interdiffConditions = [{ + basename: [/interdiff/, /inter-diff/, /innerdiff/, /inner-diff/, /increment/], + extension: ['diff', 'patch', 'txt'] + }]; + // Two tests, either will match. + var patchConditions = [ + {type: ['text/x-diff']}, + {extension: ['diff', 'patch']} + ]; + if (this.match(file, interdiffConditions)) { + file.interdiff = true; + return true; + } + else if (this.match(file, patchConditions)) { + file.patch = true; + return true; + } + } + ]); + + /** + * Iterate over each patch and attach it. + * + * @param {PatchFileObject} file + * The patch file. + */ + patch.patches.forEach(function (file) { + patch.patchAttachments.forEach(function (callback) { + callback.apply(patch, [file]); + }); + }); + }); + +})(jQuery, Drupal, UserEnhancements); diff --git a/drupalorg_project/user_enhancements/drupalorg.issue.patch.review.css b/drupalorg_project/user_enhancements/drupalorg.issue.patch.review.css new file mode 100644 index 0000000..7ad9eb1 --- /dev/null +++ b/drupalorg_project/user_enhancements/drupalorg.issue.patch.review.css @@ -0,0 +1,184 @@ +/** + * Cube "throbber" from http://tobiasahlin.com/spinkit/. + */ +@-webkit-keyframes sk-cubeGridScaleDelay { + 0%, 70%, 100% { + -webkit-transform: scale3D(1, 1, 1); + transform: scale3D(1, 1, 1); + } + 35% { + -webkit-transform: scale3D(0, 0, 1); + transform: scale3D(0, 0, 1); + } +} + +@keyframes sk-cubeGridScaleDelay { + 0%, 70%, 100% { + -webkit-transform: scale3D(1, 1, 1); + transform: scale3D(1, 1, 1); + } + 35% { + -webkit-transform: scale3D(0, 0, 1); + transform: scale3D(0, 0, 1); + } +} + +.sk-cube-grid { + box-sizing: content-box; + height: 100px; + margin: 100px; + padding: 0 calc(50% - 200px); + position: relative; + width: 100px; +} + +.sk-cube-grid:after { + content: attr(data-state); + text-align: center; + color: rgba(255, 255, 255, 0.7); + margin: 10px 0; + position: absolute; + left: 0; + right: 0; + top: 100%; + white-space: pre; +} + +.sk-cube-grid[data-state=loading]:after { + content: attr(data-state) '...'; + text-transform: capitalize; +} + +.sk-cube-grid.sm { + height: 40px; + width: 40px; +} + +.sk-cube-grid .sk-cube { + height: 33%; + width: 33%; + background-color: #0678BE; + float: left; +} + +.sk-cube-grid .sk-cube2, +.sk-cube-grid .sk-cube4, +.sk-cube-grid .sk-cube6, +.sk-cube-grid .sk-cube8 { + visibility: hidden; +} + +.sk-cube-grid[data-state=loading] .sk-cube { + -webkit-animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out; + animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out; +} +.sk-cube-grid[data-state=loading] .sk-cube1 { -webkit-animation-delay: 0.2s; animation-delay: 0.2s; } +.sk-cube-grid[data-state=loading] .sk-cube2 { -webkit-animation-delay: 0.3s; animation-delay: 0.3s; visibility: visible; } +.sk-cube-grid[data-state=loading] .sk-cube3 { -webkit-animation-delay: 0.4s; animation-delay: 0.4s; } +.sk-cube-grid[data-state=loading] .sk-cube4 { -webkit-animation-delay: 0.1s; animation-delay: 0.1s; visibility: visible; } +.sk-cube-grid[data-state=loading] .sk-cube5 { -webkit-animation-delay: 0.2s; animation-delay: 0.2s; } +.sk-cube-grid[data-state=loading] .sk-cube6 { -webkit-animation-delay: 0.3s; animation-delay: 0.3s; visibility: visible; } +.sk-cube-grid[data-state=loading] .sk-cube7 { -webkit-animation-delay: 0s; animation-delay: 0s; } +.sk-cube-grid[data-state=loading] .sk-cube8 { -webkit-animation-delay: 0.1s; animation-delay: 0.1s; visibility: visible; } +.sk-cube-grid[data-state=loading] .sk-cube9 { -webkit-animation-delay: 0.2s; animation-delay: 0.2s; } + + + +.patch-review { + align-items: center; + background: rgba(6, 71, 113, 0.87); + bottom: 0; + display: flex; + flex-direction: column; + justify-content: center; + left: 0; + overflow: hidden; + position: fixed; + right: 0; + top: 0; + opacity: 0; + transition: opacity 300ms ease-in-out, z-index 300ms ease-in-out 300ms; + visibility: hidden; + z-index: -10000; +} + +.patch-review-active .patch-review { + opacity: 1; + transition: opacity 300ms ease-in-out; + visibility: visible; + z-index: 9999; +} + +.patch-review-overlay .sk-cube-grid .sk-cube { + background-color: rgba(85, 176, 235, 0.68); +} + +/* Progress */ + +.patch-review-progress { + background: rgba(0, 42, 68, 0.6); + border-radius: 5px; + height: 10px; + margin: 40px 0; + max-width: 800px; + position: relative; + width: 80%; +} + +.patch-review-progress-inner { + background: #0678BE; + border-radius: 5px; + height: 100%; + min-width: 10px; + transition: width 150ms ease-in-out; + width: 1%; +} + +.patch-review-progress-inner:before { + bottom: 100%; + color: rgba(255, 255, 255, 0.75); + content: attr(data-filename); + left: 30px; + line-height: 1; + margin-bottom: 10px; + overflow: hidden; + position: absolute; + right: 30px; + text-align: center; + text-overflow: ellipsis; + white-space: pre; +} + +.patch-review-progress-inner:after { + color: rgba(255, 255, 255, 0.75); + content: attr(data-percentage); + left: 0; + line-height: 1; + margin-top: 10px; + position: absolute; + top: 100%; + right: 0; + text-align: center; +} + + +/* Progress */ + +.patch-review-content { + background: #fff; + bottom: 50px; + left: 50px; + opacity: 0; + overflow: auto; + position: absolute; + transition: opacity 300ms ease-in-out; + right: 50px; + top: 50px; + visibility: hidden; + z-index: 1; +} + +.patch-review-content.loaded { + opacity: 1; + visibility: visible; +} diff --git a/drupalorg_project/user_enhancements/drupalorg.issue.patch.review.js b/drupalorg_project/user_enhancements/drupalorg.issue.patch.review.js new file mode 100644 index 0000000..84293ee --- /dev/null +++ b/drupalorg_project/user_enhancements/drupalorg.issue.patch.review.js @@ -0,0 +1,118 @@ +(function ($, Drupal, UserEnhancements) { + 'use strict'; + + /** + * @namespace UserEnhancement.drupalorg.issue.patch.review + * + * @extends UserEnhancement.drupalorg.issue.patch + */ + UserEnhancements + .create('drupalorg.issue.patch.review', { + + /** + * List of users currently only the page. + * + * @type {Object} + */ + users: {}, + + /** + * The element that's attached to the patch. + * + * @type {jQuery} + */ + $element: $(''), + + /** + * @type {DreditorUI} + */ + dreditor: new DreditorUI({ + + comments: true, + + // Map Drupal extensions to their appropriate Prism language. + prismExtensionLanguageMap: { + coffee: ['coffeescript', 'javascript'], + engine: 'php', + htaccess: 'apacheconf', + inc: 'php', + info: 'ini', + install: 'php', + make: 'ini', + md: 'markdown', + module: 'php', + profile: 'php', + test: 'php', + theme: 'php', + yml: 'yaml' + }, + + // Change the route prefix from "/dreditor" to "/review". + routePrefix: 'review' + }) + + // @todo Add this back in once meta information is displayed again. + // dreditorParseEnd: function (e, parser) { + // var util = window.Dreditor.Utility; + // var title = this.getTitle(); + // var url = parser.url && this.dreditor.getUrl(parser.url); + // if (url && url.uid && this.users[url.uid]) { + // var user = this.users[url.uid]; + // for (var i = 0, l = parser.patches.length; i < l; i++) { + // var link = util.createElement('').setAttribute('href', user.url).setAttribute('target', '_blank'); + // var picture = link.clone().html(util.createElement('').setAttribute('src', user.picture).setAttribute('alt', '')); + // parser.patches[i].meta.from = picture + link.text(user.name); + // if (!parser.patches[i].meta.subject) { + // parser.patches[i].meta.subject = title; + // } + // } + // } + // } + + }) + .ready(function () { + var self = this; + + // Find all users on the page and keep track of them. + $('.comments > .comment:not(.system-message)').each(function () { + var $comment = $(this); + var $username = $comment.find('.submitted .username'); + var uid = $username.data('uid'); + if (uid && !self.users[uid]) { + self.users[uid] = { + uid: uid, + name: $username.text(), + picture: $comment.find('> .picture img').attr('src'), + url: $username.attr('href') + }; + } + }); + + // Further configure the Dreditor instance now that the plugin is ready. + this.dreditor + // Pass the patch objects along as "urls". + .setUrls(this.patches) + + // @todo Add this back in once meta information is displayed again. + // Bind necessary listeners. + // .on('parse.end', this.dreditorParseEnd.bind(this)) + ; + }) + .attachElementToPatch() + .on('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + + // Retrieve the file from the event. + // @todo Show error if the file couldn't be retrieved. + var file = this.getFileFromEvent(e); + if (!file) { + return; + } + + // Load the file. + this.dreditor.navigate(file.absolute); + }) + ; + +})(jQuery, Drupal, window.UserEnhancements); diff --git a/drupalorg_project/user_enhancements/drupalorg.issue.patch.simplytestme.js b/drupalorg_project/user_enhancements/drupalorg.issue.patch.simplytestme.js new file mode 100644 index 0000000..b2e3b02 --- /dev/null +++ b/drupalorg_project/user_enhancements/drupalorg.issue.patch.simplytestme.js @@ -0,0 +1,34 @@ +(function ($, Drupal, UserEnhancements) { + 'use strict'; + + /** + * @namespace UserEnhancement.drupalorg.issue.patch.simplytestme + * + * @extends UserEnhancement.drupalorg.issue.patch + */ + UserEnhancements + .create('drupalorg.issue.patch.simplytestme', { + $element: $('simplytest.me') + }) + + /** + * @type {UserEnhancement.drupalorg.issue.patch.attachElementToPatch~callback} + */ + .attachElementToPatch(function ($element, file) { + // Don't show this button on interdiff files since they cannot be applied. + if (file.interdiff) { + return false; + } + + $element.attr({ + href: 'http://simplytest.me/project/' + this.getProject() + '/' + this.getSelectedVersion().replace('-dev', '') + '?patch[]=' + encodeURIComponent(file.url), + target: '_blank', + // When opening a new window/tab to an external site, it should + // protect itself by specifying the following "rel" attributes: + // @see https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/ + rel: 'noopener noreferrer' + }); + }) + ; + +})(jQuery, Drupal, UserEnhancements); diff --git a/drupalorg_project/user_enhancements/drupalorg.issue.patchFilenameSuggestion.js b/drupalorg_project/user_enhancements/drupalorg.issue.patchFilenameSuggestion.js new file mode 100644 index 0000000..af168bc --- /dev/null +++ b/drupalorg_project/user_enhancements/drupalorg.issue.patchFilenameSuggestion.js @@ -0,0 +1,30 @@ +(function ($, Drupal, UserEnhancements) { + 'use strict'; + + var selector = '.field-name-field-issue-files .form-type-managed-file'; + + /** + * @namespace UserEnhancement.drupalorg.issue.patchFilenameSuggestion + * + * @extends UserEnhancement.drupalorg.issue + */ + UserEnhancements.create('drupalorg.issue.patchFilenameSuggestion', { + $element: $('') + }) + .detachElements(selector) + .attachElements('prepend', selector) + .on('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + + // Truncate and remove a heading/trailing underscore. + var filename = [ + this.getProject(), + this.machineName(this.truncateString(this.getTitle(), 25, true)), + this.getNid(), + this.getCommentCount() + 1 + ]; + window.prompt("Please use this value", (filename.filter(Boolean).join('-') + '.patch')); + }); + +})(jQuery, Drupal, UserEnhancements); diff --git a/drupalorg_project/user_enhancements/drupalorg.js b/drupalorg_project/user_enhancements/drupalorg.js new file mode 100644 index 0000000..de0ef64 --- /dev/null +++ b/drupalorg_project/user_enhancements/drupalorg.js @@ -0,0 +1,69 @@ +(function ($, Drupal, UserEnhancements) { + 'use strict'; + + /** + * @namespace UserEnhancement.drupalorg + * + * @extends UserEnhancement + */ + UserEnhancements.create('drupalorg', { + + settings: { + commentCount: 0, + nid: 0, + title: '' + }, + + /** + * Retrieve the current comment count. + * + * Attempt to first use any passed JS settings value, falling back to + * extrapolating the value from somewhere in the DOM. + * + * @return {Number} + * The comment count. + */ + getCommentCount: function () { + return this.getSetting('commentCount', function () { + var lastCommentNumber = $('.comments .comment:last .permalink').text().match(/\d+$/); + return (lastCommentNumber ? parseInt(lastCommentNumber[0], 10) : 0); + }); + }, + + /** + * Retrieve the node identifier. + * + * Attempt to first use any passed JS settings value, falling back to + * extrapolating the value from somewhere in the DOM. + * + * @return {Number} + * The nid. + */ + getNid: function () { + return this.getSetting('nid', function () { + var href = $('#tabs a:first').attr('href'); + if (href.length) { + return href.match(/(?:node|comment\/reply)\/(\d+)/)[1]; + } + return false; + }); + }, + + /** + * Retrieve the node title. + * + * Attempt to first use any passed JS settings value, falling back to + * extrapolating the value from somewhere in the DOM. + * + * @return {String} + * The title. + */ + getTitle: function () { + return this.getSetting('title', function () { + return $('#page-subtitle').text() || ''; + }); + } + + }); + +})(jQuery, Drupal, window.UserEnhancements); diff --git a/drupalorg_project/user_enhancements/drupalorg.markAsRead.js b/drupalorg_project/user_enhancements/drupalorg.markAsRead.js new file mode 100644 index 0000000..fddb331 --- /dev/null +++ b/drupalorg_project/user_enhancements/drupalorg.markAsRead.js @@ -0,0 +1,130 @@ +(function ($, Drupal, UserEnhancements) { + 'use strict'; + + // 'a + .marker' accounts for a d.o bug; the HTML markup contains two + // span.marker elements, the second being nested inside the first. + var selector = 'table:not(.sticky-header), .item-list > ul'; + + /** + * @name UserEnhancement.drupalorg.markAsRead + * + * @extends UserEnhancement.drupalorg + */ + UserEnhancements.create('drupalorg.markAsRead', { + settings: { + hideArtifacts: true + } + }) + .detach(selector, function ($selectors) { + $selectors.each(function () { + var $container = $(this).parents('div').first(); + $container.find('input.mark-all').remove(); + $container.find('.marker.mark-as-read').off('click.mark-as-read').css('cursor', 'auto'); + }); + }) + .attach(selector, function ($selectors) { + var getParent = function ($marker) { + return $marker.parents('li, tr'); + }; + var getLink = function ($marker) { + // Usually a marker appears right after the link it is marking. + var $link = $marker.prev('a'); + + // In some cases, the marker is in a separate column of the view table. + if (!$link[0]) { + $link = getParent($marker).find('.views-field-title a'); + } + return $link; + }; + + var hideArtifacts = this.getSetting('hideArtifacts'); + + $selectors.each(function () { + var $selector = $(this); + + // Filter markers. + var $markers = $selector.find('.marker').filter(function () { + // Usually a marker appears right after the link it is marking. + var $marker = $(this); + var $link = getLink($marker); + + // If no valid "node" link could be found, filter marker. + if (!$link[0]) { + return false; + } + + // Valid node link, but has no content (artifact node). + if ($link.is(':empty')) { + // Hide artifact, if setting is enabled. + hideArtifacts && getParent($marker).hide(); + return false; + } + + return true; + }); + + // Immediately return if there are no markers. + if (!$markers[0]) { + return; + } + + var throbber = '
 
'; + + var $markAll = $(''); + $markAll.on('click.mark-as-read', function (e) { + e.preventDefault(); + e.stopPropagation(); + var $throbber = $(throbber); + $markAll.before($throbber); + $markAll.remove(); + $markAll = $throbber; + $markers.trigger('click.mark-as-read'); + }.bind(this)); + $selector.before($markAll); + + // Bind the click event to individual markers. + $markers.addClass('mark-as-read').css('cursor', 'pointer').on('click.mark-as-read', function () { + var $marker = $(this); + + var done = function () { + // Filter out the marker from the current list. + $markers = $markers.not($marker); + + // Remove any corresponding "X new" links. + var $new = getParent($marker).find('a[href$="#new"],a[href*="#comment-"]'); + if (/new/.test($new.text())) { + $new.remove(); + } + + // Remove the marker. + $marker.remove(); + + // Remove the "Mark All" button if there are no more. + if (!$markers.length) { + $markAll.remove(); + } + + }; + + var $link = getLink($marker); + if (!$link[0]) { + done(); + return; + } + + // Replace the marker with the throbber. + $marker.html(throbber); + + // The actual HTML page output is irrelevant, so denote that by + // using the appropriate HTTP method. + $.ajax({ + type: 'HEAD', + url: $link.attr('href'), + complete: done + }); + }); + }); + + }); + +})(jQuery, Drupal, UserEnhancements); diff --git a/drupalorg_project/user_enhancements/drupalorg.projectCollapse.js b/drupalorg_project/user_enhancements/drupalorg.projectCollapse.js new file mode 100644 index 0000000..12c0f87 --- /dev/null +++ b/drupalorg_project/user_enhancements/drupalorg.projectCollapse.js @@ -0,0 +1,42 @@ +(function ($, Drupal, UserEnhancements) { + 'use strict'; + + var selector = '.view-project-issue-user-projects .view-header'; + + /** + * @namespace UserEnhancement.drupalorg.projectCollapse + * + * @extends UserEnhancement.drupalorg + */ + UserEnhancements.create('drupalorg.projectCollapse', { + $element: $(''), + state: true + }) + .detachElements(selector) + .attachElements('before', selector, function () { + this.trigger(this.storage.load('state') ? 'expand' : 'collapse', [false]); + }) + .on('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + this.trigger('toggle'); + }) + .on('collapse', function (e, slide) { + slide = slide !== void 0 ? slide : true; + this.$element.text(Drupal.t('Expand projects')); + this.$selectors[slide ? 'slideUp' : 'hide'](); + this.state = false; + this.storage.save('state', this.state); + }) + .on('expand', function (e, slide) { + slide = slide !== void 0 ? slide : true; + this.$element.text(Drupal.t('Collapse projects')); + this.$selectors[slide ? 'slideDown' : 'show'](); + this.state = true; + this.storage.save('state', this.state); + }) + .on('toggle', function (e, slide) { + this.trigger(this.state ? 'collapse' : 'expand', [slide]); + }); + +})(jQuery, Drupal, UserEnhancements); -- 2.10.0