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