manhattan-manage
Version:
Support for the manhattan web framework's manage UI
509 lines (407 loc) • 14.6 kB
JavaScript
import {field, gallery, imageSet} from 'manhattan-assets'
import {CharacterCount} from 'manhattan-character-count'
import {datePicker} from 'manhattan-date-picker'
import * as $ from 'manhattan-essentials'
import {addFilled, removeFilled} from 'manhattan-field-filled'
import {timePicker} from 'manhattan-time-picker'
import {tokenizer, typeahead} from 'manhattan-typeahead'
// The number of assets currently being uploaded
let uploading = 0
// -- Handlers --
function anchored(frameElm, formElm, btnsElm) {
/**
* Add/remove the anchored flag to a form when the btns bottom of the form
* is visible to the user.
*/
function _anchored() {
// Determing if the form buttons should be affixed to the page
let affix = frameElm !== null
affix = affix && frameElm.clientHeight > window.innerHeight
affix = affix && !formElm.classList
.contains('mh-form--fixed-btns-disallowed')
if (affix) {
// Flag the form as having fixed buttons
formElm.classList.add('mh-form--fixed-btns')
if (window.innerHeight + window.scrollY >= frameElm.clientHeight) {
btnsElm.classList.add('mh-field--anchored')
} else {
btnsElm.classList.remove('mh-field--anchored')
}
} else {
// Remove fixed buttons
formElm.classList.remove('mh-form--fixed-btns')
btnsElm.classList.remove('mh-field--anchored')
}
}
return _anchored
}
/**
* If there are active uploads when the user tries to leave the page then ask
* them to confirm the action.
*/
function confirmLeave(event) {
const confirmMsg = 'Not all files/images have been uploaded'
if (uploading > 0) {
(event || window.event).returnValue = confirmMsg
return confirmMsg
}
return null
}
export function getUploadCount() {
return uploading
}
/**
* Decrease the uploading counter by 1, if the count reaches 0 enable .
*/
function decUploads() {
uploading = Math.max(uploading - 1, 0)
}
/**
* Increase the uploading counter by 1.
*/
function incUploads() {
uploading += 1
}
/**
* Pin a form error to the page.
*/
function pinFieldError(inputElm, errorElm) {
if (errorElm._pinnedErrorElm) {
return
}
const textElm = $.one('.mh-field__error-text', errorElm)
// Create a pinned error element
const pinnedErrorElm = $.create(
'div',
{'class': 'mh-pinned-field-error'}
)
pinnedErrorElm.innerHTML = textElm.innerHTML
// Set the position of the pinned error
let textRect = textElm.getBoundingClientRect()
pinnedErrorElm.style.left = `${textRect.left + window.pageXOffset}px`
pinnedErrorElm.style.top = `${textRect.top + window.pageYOffset}px`
// Prevent the text element from being displayed
textElm.style.visibility = 'hidden'
// Add the pinned error element to the body
document.body.appendChild(pinnedErrorElm)
// Store a reference to the pinned error element against the error element
// so we can unpin it when the field is blurred.
errorElm._pinnedErrorElm = pinnedErrorElm
// Handle events that require the pinned error's position to be updated
errorElm._onUpdatePinnedError = () => {
console.log(1)
textRect = textElm.getBoundingClientRect()
pinnedErrorElm.style.left = `${textRect.left + window.pageXOffset}px`
pinnedErrorElm.style.top = `${textRect.top + window.pageYOffset}px`
}
$.listen(window, {'resize': errorElm._onUpdatePinnedError})
errorElm._pinnedErrorObserver
= new MutationObserver(errorElm._onUpdatePinnedError)
errorElm._pinnedErrorObserver.observe(
inputElm,
{
'attributes': true,
'attributeFilter': ['style']
}
)
}
/**
* Unpin a form error from the page.
*/
function unpinFieldError(inputElm, errorElm) {
if (!errorElm._pinnedErrorElm) {
return
}
const pinnedElm = errorElm._pinnedErrorElm
delete errorElm._pinnedErrorElm
const textElm = $.one('.mh-field__error-text', errorElm)
// Allow the text element to be displayed
textElm.style.removeProperty('visibility')
// Remove the pinned error element
pinnedElm.remove()
// Remove the resize behaviour
$.ignore(window, {'resize': errorElm._onUpdatePinnedError})
errorElm._pinnedErrorObserver.disconnect()
delete errorElm._pinnedErrorObserver
}
/**
* Return a handler that will prevent the advanced filter being closed when
* the element clicked on is related to an input element within the filter
* (such as a date picker).
*/
function preventAdvFilterClosing(inputElm) {
function _preventAdvFilterClosing(event) {
if ($.closest(inputElm, '.mh-filter-adv')) {
document.body.dataset.navSupressClick = ''
}
}
return _preventAdvFilterClosing
}
// -- Initializers --
function setup() {
// Assets
function mhFormData(inst, file, version) {
let dataPrefix = null
let inputElm = null
const data = new FormData()
if ($.one('[name="csrf_token"]')) {
data.append('csrf_token', $.one('[name="csrf_token"]').value)
}
data.append('file', file)
if (inst.gallery) {
dataPrefix = 'data-mh-gallery'
inputElm = inst.gallery.input
} else if (inst instanceof imageSet.ImageSet) {
dataPrefix = 'data-mh-image-set'
inputElm = inst.input
} else {
dataPrefix = 'data-mh-file-field'
inputElm = inst.input
}
data.append('field_name', inputElm.name)
if (inst.parentOptions) {
data.append('file_type', inst.parentOptions.fileType)
} else {
data.append('file_type', inst._options.fileType)
}
if (inputElm.getAttribute(`${dataPrefix}--blueprint`)) {
data.append(
'blueprint',
inputElm.getAttribute(`${dataPrefix}--blueprint`)
)
}
if (inputElm.hasAttribute(`${dataPrefix}--secure`)) {
data.append('secure', 'secure')
}
if (version && version !== inst.baseVersion) {
data.append('version', version)
}
return data
}
field.FileField.behaviours.formData['mhFormData'] = mhFormData
gallery.Gallery.behaviours.formData['mhFormData'] = mhFormData
imageSet.ImageSet.behaviours.formData['mhFormData'] = mhFormData
// Character count
CharacterCount.behaviours.counter['manhattan'] = (inst) => {
// Create a counter element
const cls = inst.constructor
const counter = $.create('div', {'class': cls.css['counter']})
// Insert the counter after the last element next to the
// input/textarea.
inst.input.parentNode.appendChild(counter)
return counter
}
// Tokenizer
tokenizer.Tokenizer.behaviours.store['jsonObjects'] = (inst) => {
const hiddenSelector = $.one(inst._options.hiddenSelector)
hiddenSelector.value = JSON.stringify(inst.tokens)
}
// Ask the user to confirm they want to leave the page if there are assets
// still uploading.
$.listen(window, {'beforeunload': confirmLeave})
}
export function applyFormBehaviours(container) {
// Addix form butons to the bottom of the page if the form is longer than
// the frame containing it.
const formElm = $.one('.mh-form--primary')
if (formElm) {
const frameElm = $.closest(formElm, '.mh-frame')
const btnsElm = $.one('.mh-field--btns', formElm)
// Flag whenever the buttons are anchored to the bottom of the form
const anchoredHandler = anchored(frameElm, formElm, btnsElm)
$.listen(window, {'resize scroll': anchoredHandler})
anchoredHandler()
}
}
export function applyFieldBehaviours(containerElm) {
let inputElm = null
// Squeeze labels
const inputsSelector
= '.mh-field__text, .mh-field__textarea, .mh-field__select'
for (inputElm of $.many(inputsSelector, containerElm)) {
if ($.closest(inputElm, '.mh-field__control')) {
$.listen(
inputElm,
{
'blur': (event) => {
const controlElm = $.closest(
event.target,
'.mh-field__control'
)
controlElm
.classList
.remove('mh-field__control--focused')
if (controlElm
.classList
.contains('mh-field__control--error')) {
const errorElm = $.one(
'.mh-field__error',
controlElm
)
unpinFieldError(event.currentTarget, errorElm)
}
},
'empty': (event) => {
$.closest(event.target, '.mh-field__control')
.classList.remove('mh-field__control--filled')
},
'filled': (event) => {
$.closest(event.target, '.mh-field__control')
.classList.add('mh-field__control--filled')
},
'focus': (event) => {
const controlElm = $.closest(
event.target,
'.mh-field__control'
)
controlElm.classList.add('mh-field__control--focused')
if (controlElm
.classList
.contains('mh-field__control--error')) {
const errorElm = $.one(
'.mh-field__error',
controlElm
)
pinFieldError(event.currentTarget, errorElm)
}
}
}
)
}
}
removeFilled(inputsSelector)
addFilled(inputsSelector)
// Character counters
for(inputElm of $.many('[data-mh-character-count]', containerElm)) {
let characterCount = new CharacterCount(
inputElm,
{'counter': 'manhattan'}
)
characterCount.init()
}
// Date pickers
for (inputElm of $.many('[data-mh-date-picker]', containerElm)) {
let picker = new datePicker.DatePicker(inputElm)
picker.init()
// Disable auto-complete as we're showing the picker
inputElm.setAttribute('autocomplete', 'off')
// Prevent selecting a date closing the advanced filter
$.listen(
picker.picker,
{'click': preventAdvFilterClosing(inputElm)}
)
}
// Time pickers
for (inputElm of $.many('[data-mh-time-picker]', containerElm)) {
let picker = new timePicker.TimePicker(inputElm)
picker.init()
// Disable auto-complete as we're showing the picker
inputElm.setAttribute('autocomplete', 'off')
// Prevent selecting a time closing the advanced filter
$.listen(
picker.picker,
{'click': preventAdvFilterClosing(inputElm)}
)
}
// Typeaheads
for (inputElm of $.many('[data-mh-typeahead]', containerElm)) {
let typeaheadInst = new typeahead.Typeahead(inputElm)
typeaheadInst.init()
// Prevent selecting a suggestion closing the advanced filter
$.listen(
typeaheadInst.typeahead,
{'mousedown': preventAdvFilterClosing(inputElm)}
)
}
// Tokenizers
for (inputElm of $.many('[data-mh-tokenizer]', containerElm)) {
let tokenizerInst = new tokenizer.Tokenizer(
inputElm,
{'store': 'json'}
)
let hiddenSelector = inputElm
.getAttribute('data-mh-tokenizer--hidden-selector')
let hiddenElm = $.one(hiddenSelector)
let tokens = JSON.parse(hiddenElm.value || '[]')
tokens = tokens.map((token) => {
if (typeof token === 'string') {
return {
'label': token,
'value': token
}
}
return token
})
tokenizerInst.init(tokens)
}
// Assets
// Fields
for (inputElm of $.many('[data-mh-file-field]', containerElm)) {
let fileField = new field.FileField(
inputElm,
{
'allowDrop': true,
'uploadUrl': '/manage/upload-asset',
'editing': '--draft--',
'preview': '--thumb--',
'formData': 'mhFormData'
}
)
fileField.init()
$.listen(
inputElm,
{
'uploading': incUploads,
'uploaded uploadfailed': decUploads
}
)
}
// Galleries
for (inputElm of $.many('[data-mh-gallery]', containerElm)) {
let galleryInst = new gallery.Gallery(
inputElm,
{
'allowDrop': true,
'uploadUrl': '/manage/upload-asset',
'editing': '--draft--',
'preview': '--thumb--',
'formData': 'mhFormData',
'maxUploads': 1
}
)
galleryInst.init()
$.listen(
inputElm,
{
'uploading': incUploads,
'uploaded uploadfailed': decUploads
}
)
}
// Image sets
for (inputElm of $.many('[data-mh-image-set]', containerElm)) {
let imageSetInst = new imageSet.ImageSet(
inputElm,
{
'allowDrop': true,
'uploadUrl': '/manage/upload-asset',
'editing': '--draft--',
'preview': '--thumb--',
'formData': 'mhFormData'
}
)
imageSetInst.init()
$.listen(
inputElm,
{
'uploading': incUploads,
'uploaded uploadfailed': decUploads
}
)
}
}
export function init() {
setup()
applyFormBehaviours()
applyFieldBehaviours()
}