Skip to content
Snippets Groups Projects
Commit c91f915a authored by Thomas Randolph's avatar Thomas Randolph
Browse files

Extract and refactor dropdown item rendering

Basic Intent: Allow all branch names without accidentally creating
layout or backstage DOM. e.g. a branch named `separator` should never
create a separator `li` element.  

Ideally, there should never be a string that could cause this kind of 
conflict.  

Implementation: All of `GitLabDropdown.renderItem` is extracted to a 
standalone module.  

To render a divider or separator, consumers must now pass in an object 
like `{ "type": "divider" }` or `{ "type": "separator" }`   

Notable choices:  
- All of the functions have a cyclomatic complexity of 3 or less
    - See: https://en.wikipedia.org/wiki/Cyclomatic_complexity
    - Note the "Correlation to number of defects" section
    - While software complexity may not have a directly causal
      relationship with defects, less complex software is generally
      easier to reason about, and **may** reduce defects.
      I personally try to maintain complexity of no higher than 3.
parent a15a69cf
No related branches found
No related tags found
Loading
......@@ -7,6 +7,7 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
import axios from './lib/utils/axios_utils';
import { visitUrl } from './lib/utils/url_utility';
import { isObject } from './lib/utils/type_utility';
import item from './gl_dropdown/render';
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput;
......@@ -521,8 +522,8 @@ GitLabDropdown = (function() {
html.push(
this.renderItem(
{
header: name,
// Add header for each group
content: name,
type: 'header',
},
name,
),
......@@ -542,16 +543,7 @@ GitLabDropdown = (function() {
};
GitLabDropdown.prototype.renderData = function(data, group) {
if (group == null) {
group = false;
}
return data.map(
(function(_this) {
return function(obj, index) {
return _this.renderItem(obj, group, index);
};
})(this),
);
return data.map((obj, index) => this.renderItem(obj, group || false, index));
};
GitLabDropdown.prototype.shouldPropagate = function(e) {
......@@ -688,104 +680,24 @@ GitLabDropdown = (function() {
};
GitLabDropdown.prototype.renderItem = function(data, group, index) {
var field, html, selected, text, url, value, rowHidden;
if (!this.options.renderRow) {
value = this.options.id ? this.options.id(data) : data.id;
if (value) {
value = value.toString().replace(/'/g, "\\'");
}
}
// Hide element
if (this.options.hideRow && this.options.hideRow(value)) {
rowHidden = true;
}
if (group == null) {
group = false;
}
if (index == null) {
// Render the row
index = false;
}
html = document.createElement('li');
if (rowHidden) {
html.style.display = 'none';
}
if (data === 'divider' || data === 'separator') {
html.className = data;
return html;
}
// Header
if (data.header != null) {
html.className = 'dropdown-header';
html.innerHTML = data.header;
return html;
}
if (this.options.renderRow) {
// Call the render function
html = this.options.renderRow.call(this.options, data, this);
} else {
if (!selected) {
const { fieldName } = this.options;
if (value) {
field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`);
if (field.length) {
selected = true;
}
} else {
field = this.dropdown.parent().find(`input[name='${fieldName}']`);
selected = !field.length;
}
}
// Set URL
if (this.options.url != null) {
url = this.options.url(data);
} else {
url = data.url != null ? data.url : '#';
}
// Set Text
if (this.options.text != null) {
text = this.options.text(data);
} else {
text = data.text != null ? data.text : '';
}
if (this.highlight) {
text = data.template
? this.highlightTemplate(text, data.template)
: this.highlightTextMatches(text, this.filterInput.val());
}
// Create the list item & the link
var link = document.createElement('a');
link.href = url;
if (this.icon) {
text = `<span>${text}</span>`;
link.classList.add('d-flex', 'align-items-center');
link.innerHTML = data.icon ? data.icon + text : text;
} else if (this.highlight) {
link.innerHTML = text;
} else {
link.textContent = text;
}
if (selected) {
link.classList.add('is-active');
}
if (group) {
link.dataset.group = group;
link.dataset.index = index;
}
html.appendChild(link);
}
return html;
let parent;
if (this.dropdown && this.dropdown[0]) {
parent = this.dropdown[0].parentNode;
}
return item({
options: Object.assign({}, this.options, {
icon: this.icon,
highlight: this.highlight,
highlightText: text => this.highlightTextMatches(text, this.filterInput.val()),
highlightTemplate: this.highlightTemplate.bind(this),
parent,
}),
data,
group,
index,
});
};
GitLabDropdown.prototype.highlightTemplate = function(text, template) {
......@@ -809,7 +721,6 @@ GitLabDropdown = (function() {
};
GitLabDropdown.prototype.noResults = function() {
var html;
return '<li class="dropdown-menu-empty-item"><a>No matching results</a></li>';
};
......
/* eslint no-param-reassign: [ "error", { "props": true, "ignorePropertyModificationsFor": [ row, element, link ] } ] */
const specialProcessors = new Map([
[
'divider',
row => {
row.classList.add('divider');
return row;
},
],
[
'separator',
row => {
row.classList.add('separator');
return row;
},
],
[
'header',
(row, chunk) => {
row.classList.add('dropdown-header');
row.innerHTML = chunk.content;
return row;
},
],
]);
let propertyGetters;
function defaultPropertyGetter({ property, chunk, options, defaultValue = '' }) {
let result;
if (options[property] != null) {
result = options[property](chunk);
} else {
result = chunk[property] != null ? chunk[property] : defaultValue;
}
return result;
}
function resolveMixedPropertyToValue(property, chunk, options) {
let resultingValue;
if (propertyGetters.has(property)) {
resultingValue = propertyGetters.get(property)(chunk, options);
}
return resultingValue;
}
propertyGetters = new Map([
[
'url',
(chunk, options) =>
defaultPropertyGetter({
property: 'url',
defaultValue: '#',
chunk,
options,
}),
],
[
'text',
(chunk, options) =>
defaultPropertyGetter({
property: 'text',
chunk,
options,
}),
],
[
'highlight',
(chunk, options) => {
let text = resolveMixedPropertyToValue('text', chunk, options);
if (options.highlight) {
text = chunk.template
? options.highlightTemplate(text, chunk.template)
: options.highlightText(text);
}
return text;
},
],
[
'icon',
(chunk, options) => {
let text = resolveMixedPropertyToValue('highlight', chunk, options);
if (options.icon) {
text = `<span>${text}</span>`;
text = chunk.icon ? `${chunk.icon}${text}` : text;
}
return text;
},
],
]);
function escape(text) {
return text ? String(text).replace(/'/g, "\\'") : text;
}
function getOptionValue(chunk, options) {
let value;
if (!options.renderRow) {
value = escape(options.id ? options.id(chunk) : chunk.id);
}
return value;
}
function shouldHide(chunk, options) {
const value = getOptionValue(chunk, options);
return options.hideRow && options.hideRow(value);
}
function hideElement(element) {
element.style.display = 'none';
return element;
}
function ingestOptions(options, group, index) {
const ingested = Object.assign({}, options, {
params: {
group,
index,
},
});
if (options.renderRow) {
ingested.renderRow = options.renderRow.bind(options);
}
return ingested;
}
function checkSelected(chunk, options) {
const value = getOptionValue(chunk, options);
let selected = !chunk.id;
if (options.parent) {
selected = options.parent.querySelector(`input[name='${options.fieldName}']`) == null;
if (value) {
selected =
options.parent.querySelector(`input[name='${options.fieldName}'][value='${value}']`) !=
null;
}
}
return selected;
}
function createLink(url, selected, options) {
const link = document.createElement('a');
link.href = url;
link.classList.toggle('is-active', selected);
if (options.icon) {
link.classList.add('d-flex', 'align-items-center');
}
return link;
}
function assignTextToLink(link, chunk, options) {
const text = resolveMixedPropertyToValue('icon', chunk, options);
if (options.icon || options.highlight) {
link.innerHTML = text;
} else {
link.textContent = text;
}
return link;
}
function generateLink(row, chunk, options) {
const selected = checkSelected(chunk, options);
const url = resolveMixedPropertyToValue('url', chunk, options);
let link = createLink(url, selected, options);
link = assignTextToLink(link, chunk, options);
if (options.params.group) {
link.dataset.group = options.params.group;
link.dataset.index = options.params.index;
}
row.appendChild(link);
return row;
}
function standardRender(li, chunk, options) {
let row;
if (options.renderRow) {
// Arbitrary consumer override
row = options.renderRow(chunk);
} else {
// Default render logic
row = generateLink(li, chunk, options);
}
return row;
}
export default function item({ data: chunk, options = {}, group = false, index = false }) {
const opts = ingestOptions(options, group, index);
let li = document.createElement('li');
if (shouldHide(chunk, opts)) {
li = hideElement(li);
} else if (specialProcessors.has(chunk.type)) {
li = specialProcessors.get(chunk.type)(li, chunk);
} else {
li = standardRender(li, chunk, opts);
}
return li;
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment