Merge branch 'feat/asynchronous-mass-input' into 'master'

Basic short-circuit support for mass-input

See merge request !187
This commit is contained in:
Felix Hamann 2019-05-04 22:16:09 +02:00
commit 09b1d61458
15 changed files with 231 additions and 52 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View 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
});
}
})();

View File

@ -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)}

View File

@ -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)}

View File

@ -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}

View File

@ -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}>

View File

@ -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}>

View File

@ -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)}

View File

@ -0,0 +1,8 @@
$newline never
$# Wrapper around massinput-standalone
$# pageTitle :: Html
$# pageHead :: HtmlUrl url
$# pageBody :: HtmlUrl url
$#
$# Probably only `pageBody` is relevant
^{pageBody}

View 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}

View File

@ -1,5 +1,5 @@
$newline never
<div .massinput ##{fvId}>
<div .massinput uw-mass-input ##{fvId}>
#{csrf}
^{shapeInput}
^{miWidget}

View File

@ -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);
}
});

View File

@ -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}