Commit e4eaa1fb authored by Carsten  Rose's avatar Carsten Rose
Browse files

Merge remote-tracking branch 'origin/raos_work' into crose_work

parents 8cd9955c e12b61b8
var path = require('path');
module.exports = function (grunt) { module.exports = function (grunt) {
var typo3_css = 'extension/Resources/Public/Css/'; var typo3_css = 'extension/Resources/Public/Css/';
var typo3_js = 'extension/Resources/Public/JavaScript/'; var typo3_js = 'extension/Resources/Public/JavaScript/';
...@@ -181,14 +182,50 @@ module.exports = function (grunt) { ...@@ -181,14 +182,50 @@ module.exports = function (grunt) {
'javascript/src/*.js' 'javascript/src/*.js'
] ]
}, },
concat: { concat_in_order: {
debug_standalone: { debug_standalone: {
src: js_sources, options: {
dest: 'js/<%= pkg.name %>.debug.js' extractRequired: function (filepath, filecontent) {
var workingdir = path.normalize(filepath).split(path.sep);
workingdir.pop();
var deps = this.getMatches(/\*\s*@depend\s(.*\.js)/g, filecontent);
deps.forEach(function (dep, i) {
var dependency = workingdir.concat([dep]);
deps[i] = path.join.apply(null, dependency);
});
return deps;
},
extractDeclared: function (filepath) {
return [filepath];
},
onlyConcatRequiredFiles: false
},
files: {
'js/<%= pkg.name %>.debug.js': js_sources
}
}, },
debug_extension: { debug_extension: {
src: js_sources, options: {
dest: typo3_js + '<%= pkg.name %>.debug.js' extractRequired: function (filepath, filecontent) {
var workingdir = path.normalize(filepath).split(path.sep);
workingdir.pop();
var deps = this.getMatches(/\*\s*@depend\s(.*\.js)/g, filecontent);
deps.forEach(function (dep, i) {
var dependency = workingdir.concat([dep]);
deps[i] = path.join.apply(null, dependency);
});
return deps;
},
extractDeclared: function (filepath) {
return [filepath];
},
onlyConcatRequiredFiles: false
},
files: {
'extension/Resources/Public/JavaScript/<%= pkg.name %>.debug.js': js_sources
}
} }
}, },
less: { less: {
...@@ -234,7 +271,7 @@ module.exports = function (grunt) { ...@@ -234,7 +271,7 @@ module.exports = function (grunt) {
'javascript/src/Element/*.js', 'javascript/src/Element/*.js',
'less/qfq-bs.css.less' 'less/qfq-bs.css.less'
], ],
tasks: [ 'default' ], tasks: ['default'],
options: { options: {
spawn: true spawn: true
} }
...@@ -247,14 +284,15 @@ module.exports = function (grunt) { ...@@ -247,14 +284,15 @@ module.exports = function (grunt) {
grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-concat-in-order');
grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-less'); grunt.loadNpmTasks('grunt-contrib-less');
grunt.loadNpmTasks('grunt-contrib-jasmine'); grunt.loadNpmTasks('grunt-contrib-jasmine');
// Default task(s). // Default task(s).
grunt.registerTask('default', ['jshint', 'concat', 'uglify', 'copy', 'less']); grunt.registerTask('default', ['jshint', 'concat_in_order', 'uglify', 'copy', 'less']);
grunt.registerTask('run-jasmine', ['jshint', 'concat', 'jasmine']); grunt.registerTask('run-jasmine', ['jshint', 'concat_in_order', 'jasmine']);
}; };
\ No newline at end of file
{ {
"source": { "source": {
"include": [ "include": [
"javascript/src/" "javascript/src/",
"javascript/src/Helper/",
"javascript/src/Element/"
], ],
"includePattern": ".+\\.js" "includePattern": ".+\\.js"
}, },
......
...@@ -44,8 +44,6 @@ if (!QfqNS) { ...@@ -44,8 +44,6 @@ if (!QfqNS) {
* functions of the `Cancel` or `No` button are added by calling Alert#addCancelButtonHandler(). Lastly, * functions of the `Cancel` or `No` button are added by calling Alert#addCancelButtonHandler(). Lastly,
* Alert#addSaveButtonHandler() adds callback functions to the `Save` button. * Alert#addSaveButtonHandler() adds callback functions to the `Save` button.
* *
* Regardless of the
*
* *
* @param message {string} message to be displayed * @param message {string} message to be displayed
* @param messageType {string} type of message, can either be `"info"`, `"warning"`, or `"error"`. * @param messageType {string} type of message, can either be `"info"`, `"warning"`, or `"error"`.
...@@ -200,6 +198,7 @@ if (!QfqNS) { ...@@ -200,6 +198,7 @@ if (!QfqNS) {
var $alertContainer = this.makeAlertContainerSingleton(); var $alertContainer = this.makeAlertContainerSingleton();
this.$alertDiv = $("<div>") this.$alertDiv = $("<div>")
.hide()
.addClass("alert") .addClass("alert")
.addClass(this.getAlertClassBasedOnMessageType()) .addClass(this.getAlertClassBasedOnMessageType())
.attr("role", "alert") .attr("role", "alert")
...@@ -216,7 +215,7 @@ if (!QfqNS) { ...@@ -216,7 +215,7 @@ if (!QfqNS) {
} }
$alertContainer.append(this.$alertDiv); $alertContainer.append(this.$alertDiv);
this.$alertDiv.fadeIn(this.fadeInDuration, this.afterFadeIn.bind(this)); this.$alertDiv.slideDown(this.fadeInDuration, this.afterFadeIn.bind(this));
this.shown = true; this.shown = true;
...@@ -233,22 +232,18 @@ if (!QfqNS) { ...@@ -233,22 +232,18 @@ if (!QfqNS) {
/** /**
* *
* @param event
* *
* @private * @private
*/ */
n.Alert.prototype.removeAlert = function (event) { n.Alert.prototype.removeAlert = function () {
if (!event || event.type !== "click") { // In case we have an armed timer (or expired timer, for that matter), disarm it.
// No user click, so it must be a timer event if (this.timerId) {
if (this.timerId) { window.clearTimeout(this.timerId);
window.clearTimeout(this.timerId); this.timerId = null;
this.timerId = null;
} else {
QfqNS.Log.error("Alert.remove(): Identified timer event, but had no timer id");
}
} }
var that = this; var that = this;
this.$alertDiv.fadeOut(this.fadeOutDuration, function () { this.$alertDiv.slideUp(this.fadeOutDuration, function () {
that.$alertDiv.remove(); that.$alertDiv.remove();
that.$alertDiv = null; that.$alertDiv = null;
that.shown = false; that.shown = false;
......
/**
* @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
*/
/* @depend FormGroup.js */
if (!QfqNS) {
var QfqNS = {};
}
if (!QfqNS.Element) {
QfqNS.Element = {};
}
(function (n) {
'use strict';
/**
*
* @param $element
* @constructor
*/
function Checkbox($element) {
n.FormGroup.call(this, $element);
if (!this.isType("checkbox")) {
throw new Error("$element is not of type 'checkbox'");
}
}
Checkbox.prototype = Object.create(n.FormGroup.prototype);
Checkbox.prototype.constructor = Checkbox;
Checkbox.prototype.setValue = function (val) {
this.$element.prop('checked', val);
};
Checkbox.prototype.getValue = function () {
return this.$element.prop('checked');
};
n.Checkbox = Checkbox;
})(QfqNS.Element);
/**
* @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
*/
if (!QfqNS) {
var QfqNS = {};
}
if (!QfqNS.Element) {
QfqNS.Element = {};
}
(function (n) {
'use strict';
n.Element = function ($element) {
if (!$element || $element.length === 0) {
throw new Error("No element");
}
this.formGroup = new n.FormGroup($element);
};
/**
*
* @param type
* @returns {boolean}
*
* @protected
*/
n.Element.prototype.isType = function (type) {
var lowerCaseType = type.toLowerCase();
var isOfType = true;
this.formGroup.$element.each(function () {
if (this.hasAttribute('type')) {
if (this.getAttribute('type') === lowerCaseType) {
return true;
} else {
isOfType = false;
return false;
}
} else {
// <select> is not an attribute value, obviously, so check for nodename
if (this.nodeName.toLowerCase() === lowerCaseType) {
return true;
} else if (lowerCaseType === 'text') {
return true;
} else {
isOfType = false;
return false;
}
}
});
return isOfType;
};
})(QfqNS.Element);
\ No newline at end of file
...@@ -13,6 +13,17 @@ if (!QfqNS.Element) { ...@@ -13,6 +13,17 @@ if (!QfqNS.Element) {
(function (n) { (function (n) {
'use strict'; 'use strict';
/**
* Form Group represents a `<input>/<select>` element including the label and help block.
*
* It is not meant to be used directly. Use the specialized objects instead.
*
* @param $enclosedElement {jQuery} a jQuery object contained in the Form Group. It used to find the enclosing
* HTML element having the `.form-group` class assigned.
*
*
* @constructor
*/
n.FormGroup = function ($enclosedElement) { n.FormGroup = function ($enclosedElement) {
if (!$enclosedElement || $enclosedElement.length === 0) { if (!$enclosedElement || $enclosedElement.length === 0) {
throw new Error("No enclosed element"); throw new Error("No enclosed element");
...@@ -24,6 +35,33 @@ if (!QfqNS.Element) { ...@@ -24,6 +35,33 @@ if (!QfqNS.Element) {
this.$helpBlock = this.$formGroup.find(".help-block"); this.$helpBlock = this.$formGroup.find(".help-block");
}; };
n.FormGroup.prototype.isType = function (type) {
var lowerCaseType = type.toLowerCase();
var isOfType = true;
this.$element.each(function () {
if (this.hasAttribute('type')) {
if (this.getAttribute('type') === lowerCaseType) {
return true;
} else {
isOfType = false;
return false;
}
} else {
// <select> is not an attribute value, obviously, so check for nodename
if (this.nodeName.toLowerCase() === lowerCaseType) {
return true;
} else if (lowerCaseType === 'text') {
return true;
} else {
isOfType = false;
return false;
}
}
});
return isOfType;
};
/** /**
* *
* @param $enclosedElement * @param $enclosedElement
...@@ -58,7 +96,55 @@ if (!QfqNS.Element) { ...@@ -58,7 +96,55 @@ if (!QfqNS.Element) {
}; };
n.FormGroup.prototype.setReadOnly = function (readonly) { n.FormGroup.prototype.setReadOnly = function (readonly) {
this.$element.propr('readonly', readonly); this.$element.prop('readonly', readonly);
this.handleReadOnlyEmulationIfRequired(readonly);
};
/**
* @private
* @param readonlyState
*/
n.FormGroup.prototype.handleReadOnlyEmulationIfRequired = function (readonlyState) {
if (!this.readOnlyEmulationRequired()) {
return;
}
if (readonlyState) {
// In case we're called with readonlyState===true twice in a row, make sure only one handler will be
// active at a time
this.$element.off('click', this.readOnlyHandler);
this.$element.on('click', this.readOnlyHandler);
} else {
this.$element.off('click', this.readOnlyHandler);
}
};
n.FormGroup.prototype.readOnlyEmulationRequired = function () {
// Keep this at top, since select does not feature the type attribute.
if (n.readOnlyIgnored.indexOf(this.$element[0].nodeName.toLowerCase())) {
return true;
}
if (!this.$element[0].hasAttribute('type')) {
// if there is no type attribute, browsers default to text, which is `properly implements` read only.
return false;
}
if (n.readOnlyIgnored.indexOf(this.$element[0].getAttribute('type').toLowerCase())) {
return true;
}
return false;
};
/**
* Read Only click handler.
*
* Since the readonly attribute does not work as expected on certain input types, emulate read only
*/
n.FormGroup.prototype.readOnlyHandler = function () {
return false;
}; };
......
/**
* @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
*/
/* global $ */
if (!QfqNS) {
var QfqNS = {};
}
if (!QfqNS.Element) {
QfqNS.Element = {};
}
(function (n) {
'use strict';
n.getElement = function (name) {
var $element = $('[name=' + name + ']');
if ($element.length === 0) {
throw Error('No element with name "' + name + '" found.');
}
if ($element[0].nodeName.toLowerCase() === "select") {
return new n.Select($element);
}
if (!$element[0].hasAttribute('type')) {
return new n.Text($element);
}
var type = $element[0].getAttribute('type').toLowerCase();
switch (type) {
case 'checkbox':
return new n.Checkbox($element);
case 'radio':
return new n.Radio($element);
case 'text':
return new n.Text($element);
default:
throw new Error("Don't know how to handle <input> of type '" + type + "'");
}
};
})(QfqNS.Element);
\ No newline at end of file
...@@ -20,23 +20,23 @@ if (!QfqNS.Element) { ...@@ -20,23 +20,23 @@ if (!QfqNS.Element) {
* @constructor * @constructor
*/ */
function Radio($element) { function Radio($element) {
n.Element.call(this, $element); n.FormGroup.call(this, $element);
if (!this.isType("radio")) { if (!this.isType("radio")) {
throw new Error("$element is not of type 'radio'"); throw new Error("$element is not of type 'radio'");
} }
} }
Radio.prototype = Object.create(n.Element.prototype); Radio.prototype = Object.create(n.FormGroup.prototype);
Radio.prototype.constructor = Radio; Radio.prototype.constructor = Radio;
Radio.prototype.setValue = function (val) { Radio.prototype.setValue = function (val) {
this.formGroup.$element.prop('checked', false); this.$element.prop('checked', false);
this.formGroup.$element.filter('[value=' + val + "]").prop('checked', true); this.$element.filter('[value=' + val + "]").prop('checked', true);
}; };
Radio.prototype.getValue = function () { Radio.prototype.getValue = function () {
return this.formGroup.$element.filter(':checked').val(); return this.$element.filter(':checked').val();
}; };
n.Radio = Radio; n.Radio = Radio;
......
...@@ -22,14 +22,14 @@ if (!QfqNS.Element) { ...@@ -22,14 +22,14 @@ if (!QfqNS.Element) {
* @constructor * @constructor
*/ */
function Select($element) { function Select($element) {
n.Element.call(this, $element); n.FormGroup.call(this, $element);
if (!this.isType("select")) { if (!this.isType("select")) {
throw new Error("$element is not of type 'select'"); throw new Error("$element is not of type 'select'");
} }
} }
Select.prototype = Object.create(n.Element.prototype); Select.prototype = Object.create(n.FormGroup.prototype);
Select.prototype.constructor = Select; Select.prototype.constructor = Select;
/** /**
...@@ -39,22 +39,22 @@ if (!QfqNS.Element) { ...@@ -39,22 +39,22 @@ if (!QfqNS.Element) {
* array of objects, `<select>` will have its `<option>` tags set correspondingly. * array of objects, `<select>` will have its `<option>` tags set correspondingly.
*/ */
Select.prototype.setValue = function (val) { Select.prototype.setValue = function (val) {
if (typeof(val) in ['string', 'number']) { if (['string', 'number'].indexOf(typeof(val)) !== -1) {
this.setSelection(val); this.setSelection(val);
} else if (Array.isArray(val)) { } else if (Array.isArray(val)) {
this.formGroup.$element.empty(); this.$element.empty();
// Fill array with new <select> elements first and add it to the dom in one step, instead of appending // Fill array with new <select> elements first and add it to the dom in one step, instead of appending
// each '<select>' separately. // each '<select>' separately.
var selectArray; var selectArray = [];
val.forEach(function (selectObj) { val.forEach(function (selectObj) {
var $option = $('<option>') var $option = $('<option>')
.addAttribute('value', selectObj.value ? selectObj.value : selectObj.text) .attr('value', selectObj.value ? selectObj.value : selectObj.text)
.prop('selected', selectObj.selected ? selectObj.selected : false) .prop('selected', selectObj.selected ? selectObj.selected : false)
.append(selectObj.text); .append(selectObj.text);
selectArray.append($option); selectArray.push($option);
}); });
this.formGroup.$element.append(selectArray); this.$element.append(selectArray);
} else { } else {
throw Error('Unsupported type of argument in Select.setValue: "' + typeof(val) + '". Expected either' + throw Error('Unsupported type of argument in Select.setValue: "' + typeof(val) + '". Expected either' +
' "string" or "array"'); ' "string" or "array"');
...@@ -72,11 +72,11 @@ if (!QfqNS.Element) { ...@@ -72,11 +72,11 @@ if (!QfqNS.Element) {
// First, see if we find an <option> tag having an attribute 'value' matching val. If that doesn't work, // First, see if we find an <option> tag having an attribute 'value' matching val. If that doesn't work,
// fall back to comparing text content of <option> tags. // fall back to comparing text content of <option> tags.
var $selectionByValue = this.formGroup.$element.find('option[value=' + val); var $selectionByValue = this.$element.find('option[value=' + val + ']');
if ($selectionByValue.length > 0) { if ($selectionByValue.length > 0) {
$selectionByValue.prop('selected', true); $selectionByValue.prop('selected', true);
} else { } else {
this.formGroup.$element.find('option').each(function () { this.$element.find('option').each(function () {
var $element = $(this); var $element = $(this);
if ($element.text() === val) {