Merge branch 'feat/asynchronous-mass-input' into 'master'
Basic short-circuit support for mass-input See merge request !187
This commit is contained in:
commit
09b1d61458
@ -1311,6 +1311,7 @@ siteLayout' headingOverride widget = do
|
||||
addScript $ StaticR js_utils_checkAll_js
|
||||
addScript $ StaticR js_utils_form_js
|
||||
addScript $ StaticR js_utils_inputs_js
|
||||
addScript $ StaticR js_utils_massInput_js
|
||||
addScript $ StaticR js_utils_modal_js
|
||||
addScript $ StaticR js_utils_showHide_js
|
||||
-- addScript $ StaticR js_utils_tabber_js
|
||||
|
||||
@ -37,6 +37,8 @@ import qualified Data.Foldable as Fold
|
||||
|
||||
import Control.Monad.Reader.Class (MonadReader(local))
|
||||
|
||||
import Text.Hamlet (hamletFile)
|
||||
|
||||
|
||||
$(mapM tupleBoxCoord [2..4])
|
||||
|
||||
@ -413,6 +415,12 @@ massInput MassInput{..} FieldSettings{..} fvRequired initialResult csrf = do
|
||||
|
||||
MsgRenderer mr <- getMsgRenderer
|
||||
|
||||
whenM (hasCustomHeader HeaderMassInputShortcircuit) . liftHandlerT $ do
|
||||
PageContent{..} <- widgetToPageContent $(widgetFile "widgets/massinput/massinput-standalone")
|
||||
ur <- getUrlRenderParams
|
||||
|
||||
sendResponse $ $(hamletFile "templates/widgets/massinput/massinput-standalone-wrapper.hamlet") ur
|
||||
|
||||
let
|
||||
fvLabel = toHtml $ mr fsLabel
|
||||
fvTooltip = toHtml . mr <$> fsTooltip
|
||||
|
||||
@ -678,7 +678,7 @@ takeSessionJson key = lookupSessionJson key <* deleteSession (toPathPiece key)
|
||||
-- Custom HTTP Request-Headers --
|
||||
---------------------------------
|
||||
|
||||
data CustomHeader = HeaderIsModal | HeaderDBTableShortcircuit
|
||||
data CustomHeader = HeaderIsModal | HeaderDBTableShortcircuit | HeaderMassInputShortcircuit
|
||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
||||
|
||||
instance Universe CustomHeader
|
||||
|
||||
193
static/js/utils/massInput.js
Normal file
193
static/js/utils/massInput.js
Normal file
@ -0,0 +1,193 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
*
|
||||
* Mass Input Utility
|
||||
* allows form shapes to be manipulated asynchronously:
|
||||
* will asynchronously submit the containing form and replace the contents
|
||||
* of the mass input element with the one from the BE response
|
||||
* The utility will only trigger an AJAX request if the mass input element has
|
||||
* an active/focused element whilst the form is being submitted.
|
||||
*
|
||||
* Attribute: uw-mass-input
|
||||
*
|
||||
* Example usage:
|
||||
* <form method="POST" action="...">
|
||||
* <input type="text">
|
||||
* <div uw-mass-input>
|
||||
* <input type="text">
|
||||
* <button type="submit">
|
||||
*/
|
||||
|
||||
var MASS_INPUT_UTIL_NAME = 'massInput';
|
||||
var MASS_INPUT_UTIL_SELECTOR = '[uw-mass-input]';
|
||||
|
||||
var MASS_INPUT_CELL_SELECTOR = '.massinput__cell';
|
||||
var MASS_INPUT_ADD_CELL_SELECTOR = '.massinput__cell--add';
|
||||
var MASS_INPUT_INITIALIZED_CLASS = 'mass-input--initialized';
|
||||
|
||||
var massInputUtil = function(element) {
|
||||
var massInputId;
|
||||
var massInputFormSubmitHandler;
|
||||
var massInputForm;
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Mass Input utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
massInputId = element.id;
|
||||
massInputForm = element.closest('form');
|
||||
|
||||
if (!massInputForm) {
|
||||
throw new Error('Mass Input utility cannot be setup without being wrapped in a <form>!');
|
||||
}
|
||||
|
||||
massInputFormSubmitHandler = makeSubmitHandler();
|
||||
massInputForm.addEventListener('submit', massInputFormSubmitHandler);
|
||||
|
||||
// mark initialized
|
||||
element.classList.add(MASS_INPUT_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: MASS_INPUT_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {
|
||||
reset();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeSubmitHandler() {
|
||||
if (!HttpClient) {
|
||||
throw new Error('HttpClient not found!');
|
||||
}
|
||||
|
||||
var method = massInputForm.getAttribute('method') || 'POST';
|
||||
var url = massInputForm.getAttribute('action') || window.location.href;
|
||||
var enctype = massInputForm.getAttribute('enctype') || 'application/json';
|
||||
|
||||
var requestFn;
|
||||
if (HttpClient[method.toLowerCase()]) {
|
||||
requestFn = HttpClient[method.toLowerCase()];
|
||||
}
|
||||
|
||||
return function(event) {
|
||||
// check if event occured from either a mass input add/delete button or
|
||||
// from inside one of massinput's inputs (i.e. they are focused/active)
|
||||
var activeElement = element.querySelector(':focus, :active');
|
||||
if (!activeElement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// find the according massinput cell thats hosts the element that triggered the submit
|
||||
var massInputCell = activeElement.closest(MASS_INPUT_CELL_SELECTOR);
|
||||
if (!massInputCell) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var submitButton = massInputCell.querySelector('button[type="submit"][name][value]');
|
||||
if (!submitButton) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var isAddCell = massInputCell.matches(MASS_INPUT_ADD_CELL_SELECTOR);
|
||||
var submitButtonIsActive = submitButton.matches(':focus, :active');
|
||||
// if the cell is not an add cell the active element must at least be the cells submit button
|
||||
if (!isAddCell && !submitButtonIsActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
var requestBody = serializeForm(submitButton, enctype);
|
||||
|
||||
if (requestFn && requestBody) {
|
||||
requestFn(
|
||||
url,
|
||||
{
|
||||
'Content-Type': enctype,
|
||||
'Mass-Input-Shortcircuit': massInputId,
|
||||
},
|
||||
requestBody,
|
||||
).then(function(response) {
|
||||
return response.text();
|
||||
}).then(function(response) {
|
||||
processResponse(response);
|
||||
if (isAddCell) {
|
||||
reFocusAddCell();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function processResponse(response) {
|
||||
element.innerHTML = response;
|
||||
|
||||
prefixInputIds();
|
||||
reset()
|
||||
|
||||
if (UtilRegistry) {
|
||||
UtilRegistry.setupAll(element);
|
||||
}
|
||||
}
|
||||
|
||||
function prefixInputIds() {
|
||||
var idAttrs = ['id', 'for', 'data-conditional-input'];
|
||||
idAttrs.forEach(function(attr) {
|
||||
Array.from(element.querySelectorAll('[' + attr + ']')).forEach(function(input) {
|
||||
var value = element.id + '__' + input.getAttribute(attr);
|
||||
input.setAttribute(attr, value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function serializeForm(submitButton, enctype) {
|
||||
var formData = new FormData(massInputForm);
|
||||
|
||||
// manually add name and value of submit button to formData
|
||||
formData.append(submitButton.name, submitButton.value);
|
||||
|
||||
if (enctype === 'application/x-www-form-urlencoded') {
|
||||
return new URLSearchParams(formData);
|
||||
} else if (enctype === 'multipart/form-data') {
|
||||
return formData;
|
||||
} else {
|
||||
throw new Error('Unsupported form enctype: ' + enctype);
|
||||
}
|
||||
}
|
||||
|
||||
function reFocusAddCell() {
|
||||
var addCell = element.querySelector(MASS_INPUT_ADD_CELL_SELECTOR);
|
||||
if (!addCell) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var addCellInput = addCell.querySelector('input:not([type="hidden"])');
|
||||
if (addCellInput) {
|
||||
// clear the inputs value
|
||||
// TBD: make this work for checkboxes and radioboxes
|
||||
// where the value should not be cleared
|
||||
addCellInput.value = '';
|
||||
addCellInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
element.classList.remove(MASS_INPUT_INITIALIZED_CLASS);
|
||||
massInputForm.removeEventListener('submit', massInputFormSubmitHandler)
|
||||
}
|
||||
|
||||
return init();
|
||||
};
|
||||
|
||||
// register mass input util
|
||||
if (UtilRegistry) {
|
||||
UtilRegistry.register({
|
||||
name: MASS_INPUT_UTIL_NAME,
|
||||
selector: MASS_INPUT_UTIL_SELECTOR,
|
||||
setup: massInputUtil
|
||||
});
|
||||
}
|
||||
})();
|
||||
@ -2,10 +2,10 @@ $newline never
|
||||
<table>
|
||||
<tbody>
|
||||
$forall coord <- review liveCoords lLength
|
||||
<tr .massinput--cell>
|
||||
<tr .massinput__cell>
|
||||
^{cellWdgts ! coord}
|
||||
<td>
|
||||
^{fvInput (delButtons ! coord)}
|
||||
<tfoot>
|
||||
<tr .massinput--add>
|
||||
<tr .massinput__cell.massinput__cell--add>
|
||||
^{addWdgts ! (0, 0)}
|
||||
|
||||
@ -9,10 +9,10 @@ $newline never
|
||||
<td>
|
||||
<tbody>
|
||||
$forall coord <- review liveCoords lLength
|
||||
<tr .massinput--cell .table__row>
|
||||
<tr .massinput__cell .table__row>
|
||||
^{cellWdgts ! coord}
|
||||
<td>
|
||||
^{fvInput (delButtons ! coord)}
|
||||
<tfoot>
|
||||
<tr .massinput--add>
|
||||
<tr .massinput__cell.massinput__cell--add>
|
||||
^{addWdgts ! (0, 0)}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
$newline never
|
||||
<div .recipient-category__option-add>
|
||||
<div .recipient-category__option-add.massinput__cell.massinput__cell--add>
|
||||
#{csrf}
|
||||
^{fvInput addView}
|
||||
^{fvInput submitView}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
$newline never
|
||||
<div .recipient-category__option>
|
||||
<div .recipient-category__option.massinput__cell>
|
||||
#{csrf}
|
||||
^{fvInput tickView}
|
||||
<label .recipient-category__option-label for=#{fvId tickView}>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
$newline never
|
||||
<div .recipient-category__option>
|
||||
<div .recipient-category__option.massinput__cell>
|
||||
#{csrf}
|
||||
^{fvInput tickView}
|
||||
<label .recipient-category__option-label for=#{fvId tickView}>
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
$newline never
|
||||
<table>
|
||||
<tbody>
|
||||
<tr .massinput--cell>
|
||||
<tr .massinput__cell>
|
||||
$forall coord <- review liveCoords lLength
|
||||
<td>
|
||||
^{cellWdgts ! coord}
|
||||
<td>
|
||||
^{fvInput (delButtons ! coord)}
|
||||
<tfoot>
|
||||
<tr>
|
||||
<tr .massinput__cell.massinput__cell--add>
|
||||
<td>
|
||||
<td>
|
||||
<td .massinput--add>
|
||||
^{addWdgts ! (0, 0)}
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
$newline never
|
||||
$# Wrapper around massinput-standalone
|
||||
$# pageTitle :: Html
|
||||
$# pageHead :: HtmlUrl url
|
||||
$# pageBody :: HtmlUrl url
|
||||
$#
|
||||
$# Probably only `pageBody` is relevant
|
||||
^{pageBody}
|
||||
6
templates/widgets/massinput/massinput-standalone.hamlet
Normal file
6
templates/widgets/massinput/massinput-standalone.hamlet
Normal file
@ -0,0 +1,6 @@
|
||||
$newline never
|
||||
$# Version of `widgets/massinput/massinput` for when short-circuiting happens
|
||||
$# i.e. the response is only this widget wrapped in `massinput-standalone-wrapper.hamlet`
|
||||
#{csrf}
|
||||
^{shapeInput}
|
||||
^{miWidget}
|
||||
@ -1,5 +1,5 @@
|
||||
$newline never
|
||||
<div .massinput ##{fvId}>
|
||||
<div .massinput uw-mass-input ##{fvId}>
|
||||
#{csrf}
|
||||
^{shapeInput}
|
||||
^{miWidget}
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var form = document.getElementById(#{String fvId}).closest('form');
|
||||
|
||||
|
||||
var formSubmit = form.querySelector('input[type=submit], button[type=submit]:not(.btn-mass-input-add):not(.btn-mass-input-delete)');
|
||||
var cellInputs = Array.from(form.querySelectorAll('.massinput--cell input:not([type=hidden])'));
|
||||
|
||||
cellInputs.forEach(function(input) {
|
||||
makeImplicitSubmit(input, formSubmit);
|
||||
});
|
||||
|
||||
|
||||
Array.from(form.querySelectorAll('.massinput--add')).forEach(function(wrapper) {
|
||||
var addSubmit = wrapper.querySelector('.btn-mass-input-add');
|
||||
var addInputs = Array.from(wrapper.querySelectorAll('input:not([type=hidden]):not(.btn-mass-input-add)'));
|
||||
|
||||
addInputs.forEach(function(input) {
|
||||
makeImplicitSubmit(input, addSubmit);
|
||||
});
|
||||
});
|
||||
|
||||
// Override implicit submit (pressing enter) behaviour to trigger a specified submit button instead of the default
|
||||
function makeImplicitSubmit(input, submit) {
|
||||
if (!submit) {
|
||||
throw new Error('implicitSubmit(input, options) needs to be passed a submit element via options');
|
||||
}
|
||||
|
||||
var doSubmit = function(event) {
|
||||
if (event.keyCode == 13) {
|
||||
event.preventDefault();
|
||||
submit.click();
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('keypress', doSubmit);
|
||||
}
|
||||
});
|
||||
@ -1,7 +1,7 @@
|
||||
<ul .massinput--row .#{"massinput--dim" <> toPathPiece dimIx}>
|
||||
<ul .massinput__row .#{"massinput--dim" <> toPathPiece dimIx}>
|
||||
$forall (cellCoord, cell) <- cells
|
||||
<li .massinput--cell data-massinput-coord=#{toPathPiece cellCoord}>
|
||||
<li .massinput__cell data-massinput-coord=#{toPathPiece cellCoord}>
|
||||
^{cell}
|
||||
$maybe add <- addWidget
|
||||
<li .massinput--add>
|
||||
<li .massinput__cell.massinput__cell--add>
|
||||
^{add}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user