提交 055bc410 编写于 作者: D Daniel Beck

Merge pull request #2006 from tfennelly/2.0-with-config-tabs

[JENKINS-32357] Section tabs for job configs - phase 1
......@@ -31,8 +31,11 @@ THE SOFTWARE.
<st:include page="sidepanel.jelly" />
<f:breadcrumb-config-outline />
<l:main-panel>
<l:js src="jsbundles/config-tabbar.js" />
<l:css src="jsbundles/jenkins-widgets.css" />
<div class="behavior-loading">${%LOADING}</div>
<f:form method="post" action="configSubmit" name="config">
<f:form method="post" action="configSubmit" name="config" tableClass="job-config tabbed">
<j:set var="descriptor" value="${it.descriptor}" />
<j:set var="instance" value="${it}" />
......@@ -54,7 +57,7 @@ THE SOFTWARE.
<f:bottomButtonBar>
<!--<input type="button" name="StructureTest" value="Test" onclick="buildFormTree(this.form)" />-->
<f:submit value="${%Save}"/>
<f:apply />
<f:apply value="${%Apply}"/>
</f:bottomButtonBar>
</j:if>
</f:form>
......
......@@ -20,4 +20,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
name={0} name
\ No newline at end of file
name={0} name
Save=Save All
Apply=Apply All
\ No newline at end of file
......@@ -32,8 +32,11 @@ THE SOFTWARE.
When this button is pressed, the FORM element fires the "jenkins:apply" event
that allows interested parties to write back whatever states back into the INPUT
elements.
<s:attribute name="value">
The text of the apply button.
</s:attribute>
</s:documentation>
<st:adjunct includes="lib.form.apply.apply"/>
<input type="hidden" name="core:apply" value="" />
<input type="button" value="${%Apply}" class="apply-button applyButton" name="Apply"/><!-- applyButton is legacy -->
<input type="button" value="${attrs.value ?: 'Apply'}" class="apply-button applyButton" name="Apply"/><!-- applyButton is legacy -->
</j:jelly>
\ No newline at end of file
......@@ -63,7 +63,7 @@ THE SOFTWARE.
name="${name}"
value="${attrs.value}"
title="${attrs.tooltip}"
onclick="${attrs.onclick}" id="${attrs.id}" class="${attrs.negative!=null ? 'negative' : null} ${attrs.checkUrl!=null?'validated':''}"
onclick="${attrs.onclick}" id="${attrs.id}" class="${attrs.class} ${attrs.negative!=null ? 'negative' : null} ${attrs.checkUrl!=null?'validated':''}"
checkUrl="${attrs.checkUrl}" checkDependsOn="${attrs.checkDependsOn}" json="${attrs.json}"
checked="${value ? 'true' : null}"/>
<j:if test="${attrs.title!=null}">
......
......@@ -66,9 +66,9 @@ THE SOFTWARE.
<j:choose>
<j:when test="${attrs.title!=null}">
<tr class="optional-block-start ${attrs.inline?'':'row-set-start'}" hasHelp="${attrs.help!=null}"><!-- this ID marks the beginning -->
<tr class="optional-block-start row-group-start ${attrs.inline?'':'row-set-start'}" hasHelp="${attrs.help!=null}"><!-- this ID marks the beginning -->
<td colspan="3">
<f:checkbox name="${attrs.name}" onclick="javascript:updateOptionalBlock(this,true)"
<f:checkbox name="${attrs.name}" class="optional-block-control block-control" onclick="javascript:updateOptionalBlock(this,true)"
negative="${attrs.negative}" checked="${attrs.checked}" field="${attrs.field}" title="${title}" />
</td>
<f:helpLink url="${attrs.help}" featureName="${title}"/>
......@@ -79,7 +79,7 @@ THE SOFTWARE.
<tr class="rowvg-start" />
<d:invokeBody />
<!-- end marker -->
<tr class="${attrs.inline?'':'row-set-end'} rowvg-end optional-block-end" />
<tr class="${attrs.inline?'':'row-set-end'} rowvg-end optional-block-end row-group-end" />
</j:when>
<j:otherwise>
......
......@@ -54,11 +54,11 @@ THE SOFTWARE.
<st:adjunct includes="lib.form.radioBlock.radioBlock"/>
<tr class="radio-block-start ${attrs.inline?'':'row-set-start'}" hasHelp="${attrs.help!=null}"><!-- this ID marks the beginning -->
<tr class="radio-block-start row-group-start ${attrs.inline?'':'row-set-start'}" hasHelp="${attrs.help!=null}"><!-- this ID marks the beginning -->
<td colspan="3">
<label>
<input type="radio" name="${name}" value="${value}"
class="radio-block-control" checked="${checked?'true':null}" />
class="radio-block-control block-control" checked="${checked?'true':null}" />
${title}
</label>
</td>
......@@ -69,5 +69,5 @@ THE SOFTWARE.
</j:if>
<d:invokeBody />
<!-- end marker -->
<tr class="${attrs.inline?'':'row-set-end'} radio-block-end" style="display:none" />
<tr class="${attrs.inline?'':'row-set-end'} radio-block-end row-group-end" style="display:none" />
</j:jelly>
......@@ -43,9 +43,9 @@ THE SOFTWARE.
<d:invokeBody />
</j:when>
<j:otherwise>
<tr ref="${attrs.ref}" class="row-set-start" style="display:none" name="${attrs.name}" />
<tr ref="${attrs.ref}" class="row-set-start row-group-start" style="display:none" name="${attrs.name}" />
<d:invokeBody />
<tr class="row-set-end" />
<tr class="row-set-end row-group-end" />
</j:otherwise>
</j:choose>
</j:jelly>
\ No newline at end of file
<!--
The MIT License
Copyright (c) 2016, CloudBees
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
<st:documentation>
Client-side CSS loading tag. Similar to adjunct, but driven from the client. See page-init.js.
@since 2.0
<st:attribute name="src" required="true">
CSS source path (relative to Jenkins).
</st:attribute>
</st:documentation>
<div class="jenkins-css-load" data-src="${attrs.src}" />
</j:jelly>
\ No newline at end of file
<!--
The MIT License
Copyright (c) 2016, CloudBees
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
<st:documentation>
Client-side JavaScript loading tag. Similar to adjunct, but driven from the client. See page-init.js.
@since 2.0
<st:attribute name="src" required="true">
JavaScript source path (relative to Jenkins).
</st:attribute>
</st:documentation>
<div class="jenkins-js-load" data-src="${attrs.src}" />
</j:jelly>
\ No newline at end of file
......@@ -166,6 +166,8 @@ ${h.initPageVariables(context)}
<script src="${resURL}/scripts/msie.js" type="text/javascript"/>
</j:if>
<script src="${resURL}/jsbundles/page-init.js" type="text/javascript"/>
</head>
<body id="jenkins" class="yui-skin-sam jenkins-${h.version}" data-version="jenkins-${h.version}" data-model-type="${it.class.name}">
<!-- for accessibility, skip the entire navigation bar and etc and go straight to the head of the content -->
......
......@@ -3,6 +3,14 @@
//
var builder = require('jenkins-js-builder');
//
// Bundle the page init script.
// See https://github.com/jenkinsci/js-builder#bundling
//
builder.bundle('src/main/js/page-init.js')
.withExternalModuleMapping('jquery-detached', 'core-assets/jquery-detached:jquery2')
.inDir('src/main/webapp/jsbundles');
//
// Bundle the Install Wizard.
// See https://github.com/jenkinsci/js-builder#bundling
......@@ -13,3 +21,12 @@ builder.bundle('src/main/js/pluginSetupWizard.js')
.withExternalModuleMapping('handlebars', 'core-assets/handlebars:handlebars3')
.less('src/main/less/pluginSetupWizard.less')
.inDir('src/main/webapp/jsbundles');
//
// Bundle the Config Tab Bar.
// See https://github.com/jenkinsci/js-builder#bundling
//
builder.bundle('src/main/js/config-tabbar.js')
.withExternalModuleMapping('jquery-detached', 'core-assets/jquery-detached:jquery2')
.less('src/main/js/widgets/jenkins-widgets.less')
.inDir('src/main/webapp/jsbundles');
......@@ -8,13 +8,13 @@
"handlebars": "^3.0.3",
"hbsfy": "^2.4.1",
"jenkins-handlebars-rt": "^1.0.1",
"jenkins-js-builder": "0.0.37",
"jenkins-js-test": "0.0.16"
"jenkins-js-builder": "0.0.40",
"jenkins-js-test": "^1.0.0"
},
"dependencies": {
"bootstrap-detached": "^3.3.5-v1",
"jenkins-js-modules": "^1.4.0",
"jenkins-js-modules": "^1.5.0",
"jquery-detached": "^2.1.4-v2",
"window-handle": "0.0.6"
"window-handle": "^1.0.0"
}
}
var $ = require('jquery-detached').getJQuery();
var jenkinsLocalStorage = require('./util/jenkinsLocalStorage.js');
var configMetadata = require('./widgets/config/model/ConfigTableMetaData.js');
$(function() {
// Horrible ugly hack...
// We need to use Behaviour.js to wait until after radioBlock.js Behaviour.js rules
// have been applied, otherwise row-set rows become visible across sections.
var done = false;
Behaviour.specify(".block-control", 'row-set-block-control', 1000, function() { // jshint ignore:line
if (done) {
return;
}
done = true;
// Only do job configs for now.
var configTables = $('.job-config.tabbed');
if (configTables.size() > 0) {
var tabBarShowPreferenceKey = 'config:usetabs';
var tabBarShowPreference = jenkinsLocalStorage.getGlobalItem(tabBarShowPreferenceKey, "yes");
var tabBarWidget = require('./widgets/config/tabbar.js');
if (tabBarShowPreference === "yes") {
configTables.each(function() {
var configTable = $(this);
var tabBar = tabBarWidget.addTabs(configTable);
// We want to merge some sections together.
// Merge the "Advanced" section into the "General" section.
var generalSection = tabBar.getSection('config_general');
if (generalSection) {
generalSection.adoptSection('config_advanced_project_options');
}
addFinderToggle(tabBar);
tabBar.onShowSection(function() {
// Hook back into hudson-behavior.js
fireBottomStickerAdjustEvent();
});
tabBar.deactivator.click(function() {
jenkinsLocalStorage.setGlobalItem(tabBarShowPreferenceKey, "no");
require('window-handle').getWindow().location.reload();
});
$('.jenkins-config-widgets .find-container input').focus(function() {
fireBottomStickerAdjustEvent();
});
if (tabBar.hasSections()) {
var tabBarLastSectionKey = 'config:' + tabBar.configForm.attr('name') + ':last-tab';
var tabBarLastSection = jenkinsLocalStorage.getPageItem(tabBarLastSectionKey, tabBar.sections[0].id);
tabBar.onShowSection(function() {
jenkinsLocalStorage.setPageItem(tabBarLastSectionKey, this.id);
});
tabBar.showSection(tabBarLastSection);
}
});
} else {
configTables.each(function() {
var configTable = $(this);
var activator = tabBarWidget.addTabsActivator(configTable);
configMetadata.markConfigTableParentForm(configTable);
activator.click(function() {
jenkinsLocalStorage.setGlobalItem(tabBarShowPreferenceKey, "yes");
require('window-handle').getWindow().location.reload();
});
});
}
}
});
});
function addFinderToggle(configTableMetadata) {
var findToggle = $('<div class="find-toggle" title="Find"></div>');
var finderShowPreferenceKey = 'config:showfinder';
$('.tabBar', configTableMetadata.configWidgets).append(findToggle);
findToggle.click(function() {
var findContainer = $('.find-container', configTableMetadata.configWidgets);
if (findContainer.hasClass('visible')) {
findContainer.removeClass('visible');
jenkinsLocalStorage.setGlobalItem(finderShowPreferenceKey, "no");
} else {
findContainer.addClass('visible');
$('input', findContainer).focus();
jenkinsLocalStorage.setGlobalItem(finderShowPreferenceKey, "yes");
}
});
if (jenkinsLocalStorage.getGlobalItem(finderShowPreferenceKey, "yes") === 'yes') {
findToggle.click();
}
}
function fireBottomStickerAdjustEvent() {
Event.fire(window, 'jenkins:bottom-sticker-adjust'); // jshint ignore:line
}
/*
* Page initialisation tasks.
*/
var $ = require('jquery-detached').getJQuery();
var jsModules = require('jenkins-js-modules');
$(function() {
loadScripts();
loadCSS();
});
function loadScripts() {
$('.jenkins-js-load').each(function () {
var scriptUrl = $(this).attr('data-src');
if (scriptUrl) {
// jsModules.addScript will ensure that the script is
// loaded once and once only. So, this can be considered
// analogous to a client-side adjunct.
jsModules.addScript(scriptUrl);
$(this).remove();
}
});
}
function loadCSS() {
$('.jenkins-css-load').each(function () {
var cssUrl = $(this).attr('data-src');
if (cssUrl) {
// jsModules.addCSSToPage will ensure that the CSS is
// loaded once and once only. So, this can be considered
// analogous to a client-side adjunct.
jsModules.addCSSToPage(cssUrl);
$(this).remove();
}
});
}
var windowHandle = require('window-handle');
var storage = require('./localStorage.js');
/**
* Store a Jenkins globally scoped value.
*/
exports.setGlobalItem = function(name, value) {
storage.setItem('jenkins:' + name, value);
};
/**
* Get a Jenkins globally scoped value.
*/
exports.getGlobalItem = function(name, defaultVal) {
return storage.getItem('jenkins:' + name, defaultVal);
};
/**
* Store a Jenkins page scoped value.
*/
exports.setPageItem = function(name, value) {
name = 'jenkins:' + name + ':' + windowHandle.getWindow().location.href;
storage.setItem(name, value);
};
/**
* Get a Jenkins page scoped value.
*/
exports.getPageItem = function(name, defaultVal) {
name = 'jenkins:' + name + ':' + windowHandle.getWindow().location.href;
return storage.getItem(name, defaultVal);
};
\ No newline at end of file
/*
* Some internal jQuery extensions.
*/
var jQD = require('jquery-detached');
var $ext;
exports.getJQuery = function() {
if (!$ext) {
initJQueryExt();
}
return $ext;
};
/*
* Clear the $ext instance if the window changes. Primarily for unit testing.
*/
var windowHandle = require('window-handle');
windowHandle.getWindow(function() {
$ext = undefined;
});
function initJQueryExt() {
// We are going to be adding "stuff" to jQuery. We create a totally new jQuery instance
// because we do NOT want to run the risk of polluting the shared instance.
$ext = jQD.newJQuery();
/**
* A pseudo selector that performs a case insensitive text contains search i.e. the same
* as the standard ':contains' selector, but case insensitive.
*/
$ext.expr[":"].containsci = $ext.expr.createPseudo(function (text) {
return function (element) {
var elementText = $ext(element).text();
var result = (elementText.toUpperCase().indexOf(text.toUpperCase()) !== -1);
return result;
};
});
}
initJQueryExt();
var windowHandle = require('window-handle');
var win = windowHandle.getWindow();
var storage = win.localStorage;
if (typeof storage === "undefined") {
console.warn('HTML5 localStorage not supported by this browser.');
// mock it...
storage = {
storage: {},
setItem: function(name, value) {
this.storage[name] = value;
},
getItem: function(name) {
return this.storage[name];
},
removeItem: function(name) {
delete this.storage[name];
}
};
}
exports.setItem = function(name, value) {
storage.setItem(name, value);
};
exports.getItem = function(name, defaultVal) {
var value = storage.getItem(name);
if (!value) {
value = defaultVal;
}
return value;
};
exports.removeItem = function(name) {
return storage.removeItem(name);
};
\ No newline at end of file
var jQD = require('../../../util/jquery-ext.js');
module.exports = ConfigRowGrouping;
/*
* =======================================================================================
* Configuration table row grouping i.e. row-set-*, optional-block-*, radio-block-* etc
*
* A ConfigSection maintains a list of ConfigRowGrouping and then ConfigRowGrouping
* itself maintains a list i.e. it's hierarchical. See ConfigSection.gatherRowGroups().
* =======================================================================================
*/
function ConfigRowGrouping(startRow, parentRowGroupContainer) {
this.startRow = startRow;
this.parentRowGroupContainer = parentRowGroupContainer;
this.endRow = undefined;
this.rows = [];
this.rowGroups = []; // Support groupings nested inside groupings
this.toggleWidget = undefined;
this.label = undefined;
}
ConfigRowGrouping.prototype.getRowCount = function() {
var count = this.rows.length;
for (var i = 0; i < this.rowGroups.length; i++) {
count += this.rowGroups[i].getRowCount();
}
return count;
};
ConfigRowGrouping.prototype.getLabels = function() {
var labels = [];
if (this.label) {
labels.push(this.label);
}
for (var i = 0; i < this.rowGroups.length; i++) {
var rowSet = this.rowGroups[i];
labels.push(rowSet.getLabels());
}
return labels;
};
ConfigRowGrouping.prototype.updateVisibility = function() {
if (this.toggleWidget !== undefined) {
var isChecked = this.toggleWidget.is(':checked');
for (var i = 0; i < this.rows.length; i++) {
if (isChecked) {
this.rows[i].show();
} else {
this.rows[i].hide();
}
}
}
for (var ii = 0; ii < this.rowGroups.length; ii++) {
var rowSet = this.rowGroups[ii];
rowSet.updateVisibility();
}
};
/*
* Find the row-set toggle widget i.e. the input element that indicates that
* the row-set rows should be made visible or not.
*/
ConfigRowGrouping.prototype.findToggleWidget = function(row) {
var $ = jQD.getJQuery();
var input = $(':input.block-control', row);
if (input.size() === 1) {
this.toggleWidget = input;
this.label = input.parent().find('label').text();
input.addClass('disable-behavior');
}
};
var jQD = require('../../../util/jquery-ext.js');
var util = require('./util.js');
var ConfigRowGrouping = require('./ConfigRowGrouping.js');
module.exports = ConfigSection;
/*
* =======================================================================================
* Configuration table section.
* =======================================================================================
*/
function ConfigSection(headerRow, parentCMD) {
this.headerRow = headerRow;
this.parentCMD = parentCMD;
this.title = headerRow.attr('title');
this.id = util.toId(this.title);
this.rowGroups = undefined;
this.activator = undefined;
this.subSections = [];
}
ConfigSection.prototype.isTopLevelSection = function() {
return (this.parentCMD.getSection(this.id) !== undefined);
};
/**
* Move another top-level section into this section i.e. adopt it.
* <p>
* This allows us to take a top level section (by id) and push it down
* into another section e.g. pushing the "Advanced" section into the
* "General" section.
* @param sectionId The id of the top-level section to be adopted.
*/
ConfigSection.prototype.adoptSection = function(sectionId) {
if (!this.isTopLevelSection()) {
// Only top-level sections can adopt.
return;
}
var child = this.parentCMD.getSection(sectionId);
if (child && this.parentCMD.removeSection(child.id)) {
this.subSections.push(child);
}
};
/*
* Get the section rows.
*/
ConfigSection.prototype.getRows = function() {
var curTr = this.headerRow.next();
var rows = [];
var numNewRows = 0;
rows.push(curTr);
while(curTr.size() === 1 && !curTr.hasClass('section-header-row')) {
rows.push(curTr);
if (!curTr.hasClass(this.id)) {
numNewRows++;
curTr.addClass(this.id);
}
curTr = curTr.next();
}
if (numNewRows > 0) {
// We have new rows in the section ... reset cached info.
if (this.rowGroups !== undefined) {
this.gatherRowGroups(rows);
}
}
return rows;
};
/*
* Set the element (jquery) that activates the section (on click).
*/
ConfigSection.prototype.setActivator = function(activator) {
this.activator = activator;
var section = this;
section.activator.click(function() {
section.parentCMD.showSection(section);
});
};
ConfigSection.prototype.activate = function() {
if (this.activator) {
this.activator.click();
} else {
console.warn('No activator attached to config section object.');
}
};
ConfigSection.prototype.markRowsAsActive = function() {
var rows = this.getRows();
for (var i = 0; i < rows.length; i++) {
rows[i].addClass('active').show();
}
for (var ii = 0; ii < this.subSections.length; ii++) {
this.subSections[ii].markRowsAsActive();
}
this.updateRowGroupVisibility();
};
ConfigSection.prototype.activeRowCount = function() {
var activeRowCount = 0;
var rows = this.getRows();
for (var i = 0; i < rows.length; i++) {
if (rows[i].hasClass('active')) {
activeRowCount++;
}
}
return activeRowCount;
};
ConfigSection.prototype.updateRowGroupVisibility = function() {
if (this.rowGroups === undefined) {
// Lazily gather row grouping information.
this.gatherRowGroups();
}
for (var i = 0; i < this.rowGroups.length; i++) {
var rowGroup = this.rowGroups[i];
rowGroup.updateVisibility();
}
for (var ii = 0; ii < this.subSections.length; ii++) {
this.subSections[ii].updateRowGroupVisibility();
}
};
ConfigSection.prototype.gatherRowGroups = function(rows) {
this.rowGroups = [];
// Only tracking row-sets that are bounded by 'row-set-start' and 'row-set-end' (for now).
// Also, only capturing the rows after the 'block-control' input (checkbox, radio etc)
// and before the 'row-set-end'.
// TODO: Find out how these actually work. It seems like they can be nested into a hierarchy :(
// Also seems like you can have these "optional-block" thingies which are not wrapped
// in 'row-set-start' etc. Grrrrrr :(
if (rows === undefined) {
rows = this.getRows();
}
if (rows.length > 0) {
// Create a top level "fake" ConfigRowGrouping just to capture
// the top level groupings. We copy the rowGroups info out
// of this and use it in the top "this" ConfigSection instance.
var rowGroupContainer = new ConfigRowGrouping(rows[0], undefined);
this.rowGroups = rowGroupContainer.rowGroups;
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
if (row.hasClass('row-group-start')) {
var newRowGroup = new ConfigRowGrouping(row, rowGroupContainer);
if (rowGroupContainer) {
rowGroupContainer.rowGroups.push(newRowGroup);
}
rowGroupContainer = newRowGroup;
newRowGroup.findToggleWidget(row);
} else {
if (row.hasClass('row-group-end')) {
rowGroupContainer.endRow = row;
rowGroupContainer = rowGroupContainer.parentRowGroupContainer; // pop back off the "stack"
} else if (rowGroupContainer.toggleWidget === undefined) {
rowGroupContainer.findToggleWidget(row);
} else {
// we have the toggleWidget, which means that this row is
// one of the rows after that row and is one of the rows that's
// subject to being made visible/hidden when the input is
// checked or unchecked.
rowGroupContainer.rows.push(row);
}
}
}
}
};
ConfigSection.prototype.getRowGroupLabels = function() {
var labels = [];
for (var i = 0; i < this.rowGroups.length; i++) {
var rowGroup = this.rowGroups[i];
labels.push(rowGroup.getLabels());
}
return labels;
};
ConfigSection.prototype.highlightText = function(text) {
var $ = jQD.getJQuery();
var selector = ":containsci('" + text + "')";
var rows = this.getRows();
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
/*jshint loopfunc: true */
$('span.highlight-split', row).each(function() { // jshint ignore:line
var highlightSplit = $(this);
highlightSplit.before(highlightSplit.text());
highlightSplit.remove();
});
if (text !== '') {
var regex = new RegExp('(' + text + ')',"gi");
/*jshint loopfunc: true */
$(selector, row).find(':not(:input)').each(function() {
var $this = $(this);
$this.contents().each(function () {
// We specifically only mess with text nodes
if (this.nodeType === 3) {
var highlightedMarkup = this.wholeText.replace(regex, '<span class="highlight">$1</span>');
$(this).replaceWith('<span class="highlight-split">' + highlightedMarkup + '</span>');
}
});
});
}
}
};
/*
* Internal support module for config tables.
*/
var jQD = require('../../../util/jquery-ext.js');
var ConfigSection = require('./ConfigSection.js');
var util = require('./util.js');
exports.markConfigTableParentForm = function(configTable) {
var form = configTable.closest('form');
form.addClass('jenkins-config');
return form;
};
exports.findConfigTables = function() {
var $ = jQD.getJQuery();
// The config tables are the immediate child <table> elements of <form> elements
// with a name of "config"?
return $('form[name="config"] > table');
};
exports.fromConfigTable = function(configTable) {
var $ = jQD.getJQuery();
var sectionHeaders = $('.section-header', configTable);
var configForm = exports.markConfigTableParentForm(configTable);
// Mark the ancestor <tr>s of the section headers and add a title
sectionHeaders.each(function () {
var sectionHeader = $(this);
var sectionRow = sectionHeader.closest('tr');
var sectionTitle = sectionRow.text();
// Remove leading hash from accumulated text in title (from <a> element).
if (sectionTitle.indexOf('#') === 0) {
sectionTitle = sectionTitle.substring(1);
}
sectionRow.addClass('section-header-row');
sectionRow.attr('title', sectionTitle);
});
var configTableMetadata = new ConfigTableMetaData(configForm, configTable);
var topRows = configTableMetadata.getTopRows();
var firstRow = configTableMetadata.getFirstRow();
var curSection;
// The first set of rows don't have a 'section-header-row', so we manufacture one,
// calling it a "General" section. We do this by marking the first row in the table.
// See the next block of code.
firstRow.addClass('section-header-row');
firstRow.attr('title', "General");
// Go through the top level <tr> elements (immediately inside the <tbody>)
// and group the related <tr>s based on the "section-header-row", using a "normalized"
// version of the section title as the section id.
topRows.each(function () {
var tr = $(this);
if (tr.hasClass('section-header-row')) {
// a new section
curSection = new ConfigSection(tr, configTableMetadata);
configTableMetadata.sections.push(curSection);
}
});
var buttonsRow = $('#bottom-sticker', configTable).closest('tr');
buttonsRow.removeClass(curSection.id);
buttonsRow.addClass(util.toId('buttons'));
return configTableMetadata;
};
/*
* =======================================================================================
* ConfigTable MetaData class.
* =======================================================================================
*/
function ConfigTableMetaData(configForm, configTable) {
this.$ = jQD.getJQuery();
this.configForm = configForm;
this.configTable = configTable;
this.configTableBody = this.$('> tbody', configTable);
this.activatorContainer = undefined;
this.sections = [];
this.findInput = undefined;
this.showListeners = [];
this.configWidgets = undefined;
this.addWidgetsContainer();
this.addFindWidget();
}
ConfigTableMetaData.prototype.getTopRows = function() {
return this.configTableBody.children('tr');
};
ConfigTableMetaData.prototype.getFirstRow = function() {
return this.getTopRows().first();
};
ConfigTableMetaData.prototype.addWidgetsContainer = function() {
var $ = jQD.getJQuery();
this.configWidgets = $('<div class="jenkins-config-widgets"></div>');
this.configWidgets.insertBefore(this.configForm);
};
ConfigTableMetaData.prototype.addFindWidget = function() {
var $ = jQD.getJQuery();
var thisTMD = this;
var findWidget = $('<div class="find-container"><div class="find"><span title="Clear" class="clear">x</span><input placeholder="find"/></div></div>');
thisTMD.findInput = $('input', findWidget);
// Add the find text clearer
$('.clear', findWidget).click(function() {
thisTMD.findInput.val('');
thisTMD.showSections('');
thisTMD.findInput.focus();
});
var findTimeout;
thisTMD.findInput.keydown(function() {
if (findTimeout) {
clearTimeout(findTimeout);
findTimeout = undefined;
}
findTimeout = setTimeout(function() {
findTimeout = undefined;
thisTMD.showSections(thisTMD.findInput.val());
}, 300);
});
this.configWidgets.append(findWidget);
};
ConfigTableMetaData.prototype.sectionCount = function() {
return this.sections.length;
};
ConfigTableMetaData.prototype.hasSections = function() {
var hasSections = (this.sectionCount() > 0);
if (!hasSections) {
console.warn('Jenkins configuration without sections?');
}
return hasSections;
};
ConfigTableMetaData.prototype.sectionIds = function() {
var sectionIds = [];
for (var i = 0; i < this.sections.length; i++) {
sectionIds.push(this.sections[i].id);
}
return sectionIds;
};
ConfigTableMetaData.prototype.activateSection = function(sectionId) {
if (!sectionId) {
throw 'Invalid section id "' + sectionId + '"';
}
var section = this.getSection(sectionId);
if (section) {
section.activate();
}
};
ConfigTableMetaData.prototype.activeSection = function() {
if (this.hasSections()) {
for (var i = 0; i < this.sections.length; i++) {
var section = this.sections[i];
if (section.activator.hasClass('active')) {
return section;
}
}
}
};
ConfigTableMetaData.prototype.getSection = function(sectionId) {
if (this.hasSections()) {
for (var i = 0; i < this.sections.length; i++) {
var section = this.sections[i];
if (section.id === sectionId) {
return section;
}
}
}
return undefined;
};
ConfigTableMetaData.prototype.removeSection = function(sectionId) {
if (this.hasSections()) {
for (var i = 0; i < this.sections.length; i++) {
var section = this.sections[i];
if (section.id === sectionId) {
this.sections.splice(i, 1);
if (section.activator) {
section.activator.remove();
}
return true;
}
}
}
return false;
};
ConfigTableMetaData.prototype.activateFirstSection = function() {
if (this.hasSections()) {
this.activateSection(this.sections[0].id);
}
};
ConfigTableMetaData.prototype.activeSectionCount = function() {
var activeSectionCount = 0;
if (this.hasSections()) {
for (var i = 0; i < this.sections.length; i++) {
var section = this.sections[i];
if (section.activator.hasClass('active')) {
activeSectionCount++;
}
}
}
return activeSectionCount;
};
ConfigTableMetaData.prototype.showSection = function(section) {
if (typeof section === 'string') {
section = this.getSection(section);
}
if (section) {
var topRows = this.getTopRows();
// Deactivate currently active section ...
this.deactivateActiveSection();
// Active the specified section
section.activator.addClass('active');
section.markRowsAsActive();
// and always show the buttons
topRows.filter('.config_buttons').show();
// Update text highlighting
section.highlightText(this.findInput.val());
fireListeners(this.showListeners, section);
}
};
ConfigTableMetaData.prototype.deactivateActiveSection = function() {
var topRows = this.getTopRows();
var $ = jQD.getJQuery();
$('.config-section-activator.active', this.activatorContainer).removeClass('active');
topRows.filter('.active').removeClass('active');
topRows.hide();
};
ConfigTableMetaData.prototype.onShowSection = function(listener) {
this.showListeners.push(listener);
};
ConfigTableMetaData.prototype.showSections = function(withText) {
if (withText === '') {
if (this.hasSections()) {
for (var i1 = 0; i1 < this.sections.length; i1++) {
this.sections[i1].activator.show();
}
var activeSection = this.activeSection();
if (!activeSection) {
this.showSection(this.sections[0]);
} else {
activeSection.highlightText(this.findInput.val());
}
}
} else {
if (this.hasSections()) {
var $ = jQD.getJQuery();
var selector = ":containsci('" + withText + "')";
var sectionsWithText = [];
for (var i2 = 0; i2 < this.sections.length; i2++) {
var section = this.sections[i2];
var containsText = false;
var sectionRows = section.getRows();
for (var i3 = 0; i3 < sectionRows.length; i3++) {
var row = sectionRows[i3];
var elementsWithText = $(selector, row);
if (elementsWithText.size() > 0) {
containsText = true;
break;
}
}
if (containsText) {
section.activator.show();
sectionsWithText.push(section);
} else {
section.activator.hide();
}
}
// Select the first section to contain the text.
if (sectionsWithText.length > 0) {
this.showSection(sectionsWithText[0]);
} else {
this.deactivateActiveSection();
}
}
}
};
function fireListeners(listeners, contextObject) {
for (var i = 0; i < listeners.length; i++) {
fireListener(listeners[i], contextObject);
}
function fireListener(listener, contextObject) {
setTimeout(function() {
listener.call(contextObject);
}, 1);
}
}
\ No newline at end of file
exports.toId = function(string) {
string = string.trim();
return 'config_' + string.replace(/[\W_]+/g, '_').toLowerCase();
};
\ No newline at end of file
var jQD = require('jquery-detached');
var tableMetadata = require('./model/ConfigTableMetaData.js');
exports.addTabsOnFirst = function() {
return exports.addTabs(tableMetadata.findConfigTables().first());
};
exports.addTabs = function(configTable) {
var $ = jQD.getJQuery();
var configTableMetadata;
if ($.isArray(configTable)) {
// It's a config <table> metadata block
configTableMetadata = configTable;
} else if (typeof configTable === 'string') {
// It's a config <table> selector
var configTableEl = $(configTable);
if (configTableEl.size() === 0) {
throw "No config table found using selector '" + configTable + "'";
} else {
configTableMetadata = tableMetadata.fromConfigTable(configTableEl);
}
} else {
// It's a config <table> element
configTableMetadata = tableMetadata.fromConfigTable(configTable);
}
var tabBar = $('<div class="tabBar config-section-activators"></div>');
configTableMetadata.activatorContainer = tabBar;
function newTab(section) {
var tab = $('<div class="tab config-section-activator"></div>');
tab.text(section.title);
tab.addClass(section.id);
return tab;
}
var section;
for (var i = 0; i < configTableMetadata.sections.length; i++) {
section = configTableMetadata.sections[i];
var tab = newTab(section);
tabBar.append(tab);
section.setActivator(tab);
}
var tabs = $('<div class="form-config tabBarFrame"></div>');
var noTabs = $('<div class="noTabs" title="Remove configuration tabs and revert to the &quot;classic&quot; configuration view">untab</div>');
configTableMetadata.configWidgets.append(tabs);
tabs.append(noTabs);
tabs.append(tabBar);
tabs.mouseenter(function() {
tabs.addClass('mouse-over');
});
tabs.mouseleave(function() {
tabs.removeClass('mouse-over');
});
configTableMetadata.deactivator = noTabs;
// Always activate the first section by default.
configTableMetadata.activateFirstSection();
return configTableMetadata;
};
exports.addTabsActivator = function(configTable) {
var $ = jQD.getJQuery();
var configWidgets = $('<div class="jenkins-config-widgets"><span class="showTabs" title="Add configuration section tabs">tab</span></div>');
configWidgets.insertBefore(configTable);
return configWidgets;
};
/*
* Tab bar specific rules.
*/
.jenkins-config-widgets {
position: relative;
.showTabs {
float: right;
margin-bottom: 5px;
margin-right: 20px;
cursor: pointer;
opacity: 0.4;
}
.showTabs:hover {
opacity: 0.8;
}
@find-container-height: 40px;
@find-container-width: 200px;
.find-container {
.border-radius(@find-container-height/2);
.find-container(@height: @find-container-height, @width: @find-container-width);
display: none;
background-color: @light-backgrond;
margin-bottom: 15px;
.find {
left: 50%;
margin-left: -100px;
}
}
.find-container.visible {
display: block;
}
.form-config.tabBarFrame {
position: relative;
.noTabs {
display: none;
position: absolute;
margin: 3px;
right: 0px;
top: 7px;
cursor: pointer;
opacity: 0.4;
}
.config-section-activators {
margin-right: 80px;
}
.tabBar {
.tab {
border: solid 1px @light-border;
border-bottom: none;
color: #999;
padding: 7px 10px;
.border-radius-top(5px);
cursor: pointer;
}
.tab.active {
background: #eee;
color: #000;
font-weight: bold;
z-index: 2;
}
.find-toggle {
background-image: url("../images/16x16/search.png");
background-repeat: no-repeat;
width: 16px;
height: 16px;
margin: 8px;
float: left;
cursor: pointer;
}
}
border-bottom: solid 1px @light-border;
margin-bottom: 10px;
}
.form-config.tabBarFrame.mouse-over {
.noTabs {
display: inline;
}
.noTabs:hover {
opacity: 0.8;
}
}
}
.jenkins-config {
span.highlight {
background-color: #ffff00;
}
}
\ No newline at end of file
/*
* Mixins for form elements
*/
/*
* Search input field
*/
.search(@height) {
height: @height;
.border-radius(@height/2);
padding: 0px 15px;
outline: none;
border: 1px solid #DDD;
box-shadow: 0 0 5px #DDD inset;
background-color: #ffffff;
}
.find-container(@height: 40px, @width: 200px) {
height: @height;
padding: 5px;
.find {
position: absolute;
top: 5px;
width: @width;
vertical-align: middle;
input {
width: 100%;
.search(@height - 10);
}
.clear {
position: absolute;
padding: 0px 4px;
cursor: pointer;
top: (@height/2 - 13);
right: 5px;
opacity: 0.5;
font-weight: bold;
}
.clear:hover {
opacity: 1.0;
}
}
}
\ No newline at end of file
@import "variables";
/*
* Mixins
* http://lesscss.org/features/#mixins-feature
*/
@import "layout-mixins";
@import "form/form-mixins";
/*
* Widget styles
*/
@import "config/tabbar";
\ No newline at end of file
/*
* General/layout mixins
*/
/*
* Border radius
*/
.border-radius(@radius) {
-webkit-border-radius: @radius;
-moz-border-radius: @radius;
border-radius: @radius;
}
.border-radius-top-left(@radius) {
-webkit-border-top-left-radius: @radius;
-moz-border-top-left-radius: @radius;
border-top-left-radius: @radius;
}
.border-radius-top-right(@radius) {
-webkit-border-top-right-radius: @radius;
-moz-border-top-right-radius: @radius;
border-top-right-radius: @radius;
}
.border-radius-bottom-left(@radius) {
-webkit-border-bottom-left-radius: @radius;
-moz-border-bottom-left-radius: @radius;
border-top-bottom-radius: @radius;
}
.border-radius-bottom-right(@radius) {
-webkit-border-bottom-right-radius: @radius;
-moz-border-bottom-right-radius: @radius;
border-top-bottom-radius: @radius;
}
.border-radius-top(@radius) {
.border-radius-top-left(@radius);
.border-radius-top-right(@radius);
}
.border-radius-bottom(@radius) {
.border-radius-bottom-left(@radius);
.border-radius-bottom-right(@radius);
}
.border-radius-left(@radius) {
.border-radius-top-left(@radius);
.border-radius-bottom-left(@radius);
}
.border-radius-right(@radius) {
.border-radius-top-right(@radius);
.border-radius-bottom-right(@radius);
}
\ No newline at end of file
@light-border: #f3f3f3;
@light-backgrond: #eee;
\ No newline at end of file
......@@ -1195,6 +1195,7 @@ var jenkinsRules = {
Element.observe(window,"resize",adjustSticker);
// initial positioning
Element.observe(window,"load",adjustSticker);
Event.observe(window, 'jenkins:bottom-sticker-adjust', adjustSticker);
adjustSticker();
layoutUpdateCallback.add(adjustSticker);
},
......
var jsTest = require("jenkins-js-test");
describe("tabbar-spec tests", function () {
it("- test section count", function (done) {
jsTest.onPage(function() {
var configTabBar = jsTest.requireSrcModule('widgets/config/tabbar');
var firstTableMetadata = configTabBar.addTabsOnFirst();
var jQD = require('jquery-detached');
var $ = jQD.getJQuery();
expect($('.section-header-row', firstTableMetadata.configTable).size()).toBe(6);
expect(firstTableMetadata.sectionCount()).toBe(4);
expect($('.tabBar .tab').size()).toBe(8);
expect(firstTableMetadata.sectionIds().toString())
.toBe('config_general,config__build_triggers,config__advanced_project_options,config__workflow');
done();
}, 'widgets/config/workflow-config.html');
});
it("- test section activation", function (done) {
jsTest.onPage(function() {
var configTabBar = jsTest.requireSrcModule('widgets/config/tabbar');
var firstTableMetadata = configTabBar.addTabsOnFirst();
// The first section ("General") should be active by default
expect(firstTableMetadata.activeSection().id).toBe('config_general');
expect(firstTableMetadata.activeSectionCount()).toBe(1);
firstTableMetadata.onShowSection(function() {
expect(this.id).toBe('config__workflow');
expect(firstTableMetadata.activeSectionCount()).toBe(1);
var activeSection = firstTableMetadata.activeSection();
expect(activeSection.id).toBe('config__workflow');
expect(activeSection.activeRowCount()).toBe(4);
expect(firstTableMetadata.getTopRows().filter('.active').size()).toBe(3); // should be the same as activeSection.activeRowCount()
done();
});
// Mimic the user clicking on one of the tabs. Should make that section active,
// with all of the rows in that section having an "active" class.
firstTableMetadata.activateSection('config__workflow');
// above 'firstTableMetadata.onShowSection' handler should get called now
}, 'widgets/config/workflow-config.html');
});
it("- test row-set activation", function (done) {
jsTest.onPage(function() {
var configTabBar = jsTest.requireSrcModule('widgets/config/tabbar');
var firstTableMetadata = configTabBar.addTabsOnFirst();
var generalSection = firstTableMetadata.activeSection();
expect(generalSection.id).toBe('config_general');
expect(generalSection.rowGroups.length).toBe(3);
expect(generalSection.getRowGroupLabels().toString()).toBe('Discard Old Builds,This build is parameterized,Execute concurrent builds if necessary,Quiet period');
expect(generalSection.rowGroups[0].getRowCount()).toBe(6);
expect(generalSection.rowGroups[1].getRowCount()).toBe(1);
done();
}, 'widgets/config/workflow-config.html');
});
});
// TODO: lots more tests !!!
\ No newline at end of file
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册