move js and css to frontend folder
This commit is contained in:
parent
fb5be3995e
commit
4ee48ca268
466
frontend/polyfills/fetch.js
Normal file
466
frontend/polyfills/fetch.js
Normal file
@ -0,0 +1,466 @@
|
||||
(function(self) {
|
||||
'use strict';
|
||||
|
||||
if (self.fetch) {
|
||||
return
|
||||
}
|
||||
|
||||
var support = {
|
||||
searchParams: 'URLSearchParams' in self,
|
||||
iterable: 'Symbol' in self && 'iterator' in Symbol,
|
||||
blob: 'FileReader' in self && 'Blob' in self && (function() {
|
||||
try {
|
||||
new Blob()
|
||||
return true
|
||||
} catch(e) {
|
||||
return false
|
||||
}
|
||||
})(),
|
||||
formData: 'FormData' in self,
|
||||
arrayBuffer: 'ArrayBuffer' in self
|
||||
}
|
||||
|
||||
if (support.arrayBuffer) {
|
||||
var viewClasses = [
|
||||
'[object Int8Array]',
|
||||
'[object Uint8Array]',
|
||||
'[object Uint8ClampedArray]',
|
||||
'[object Int16Array]',
|
||||
'[object Uint16Array]',
|
||||
'[object Int32Array]',
|
||||
'[object Uint32Array]',
|
||||
'[object Float32Array]',
|
||||
'[object Float64Array]'
|
||||
]
|
||||
|
||||
var isDataView = function(obj) {
|
||||
return obj && DataView.prototype.isPrototypeOf(obj)
|
||||
}
|
||||
|
||||
var isArrayBufferView = ArrayBuffer.isView || function(obj) {
|
||||
return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeName(name) {
|
||||
if (typeof name !== 'string') {
|
||||
name = String(name)
|
||||
}
|
||||
if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) {
|
||||
throw new TypeError('Invalid character in header field name')
|
||||
}
|
||||
return name.toLowerCase()
|
||||
}
|
||||
|
||||
function normalizeValue(value) {
|
||||
if (typeof value !== 'string') {
|
||||
value = String(value)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// Build a destructive iterator for the value list
|
||||
function iteratorFor(items) {
|
||||
var iterator = {
|
||||
next: function() {
|
||||
var value = items.shift()
|
||||
return {done: value === undefined, value: value}
|
||||
}
|
||||
}
|
||||
|
||||
if (support.iterable) {
|
||||
iterator[Symbol.iterator] = function() {
|
||||
return iterator
|
||||
}
|
||||
}
|
||||
|
||||
return iterator
|
||||
}
|
||||
|
||||
function Headers(headers) {
|
||||
this.map = {}
|
||||
|
||||
if (headers instanceof Headers) {
|
||||
headers.forEach(function(value, name) {
|
||||
this.append(name, value)
|
||||
}, this)
|
||||
} else if (Array.isArray(headers)) {
|
||||
headers.forEach(function(header) {
|
||||
this.append(header[0], header[1])
|
||||
}, this)
|
||||
} else if (headers) {
|
||||
Object.getOwnPropertyNames(headers).forEach(function(name) {
|
||||
this.append(name, headers[name])
|
||||
}, this)
|
||||
}
|
||||
}
|
||||
|
||||
Headers.prototype.append = function(name, value) {
|
||||
name = normalizeName(name)
|
||||
value = normalizeValue(value)
|
||||
var oldValue = this.map[name]
|
||||
this.map[name] = oldValue ? oldValue+','+value : value
|
||||
}
|
||||
|
||||
Headers.prototype['delete'] = function(name) {
|
||||
delete this.map[normalizeName(name)]
|
||||
}
|
||||
|
||||
Headers.prototype.get = function(name) {
|
||||
name = normalizeName(name)
|
||||
return this.has(name) ? this.map[name] : null
|
||||
}
|
||||
|
||||
Headers.prototype.has = function(name) {
|
||||
return this.map.hasOwnProperty(normalizeName(name))
|
||||
}
|
||||
|
||||
Headers.prototype.set = function(name, value) {
|
||||
this.map[normalizeName(name)] = normalizeValue(value)
|
||||
}
|
||||
|
||||
Headers.prototype.forEach = function(callback, thisArg) {
|
||||
for (var name in this.map) {
|
||||
if (this.map.hasOwnProperty(name)) {
|
||||
callback.call(thisArg, this.map[name], name, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Headers.prototype.keys = function() {
|
||||
var items = []
|
||||
this.forEach(function(value, name) { items.push(name) })
|
||||
return iteratorFor(items)
|
||||
}
|
||||
|
||||
Headers.prototype.values = function() {
|
||||
var items = []
|
||||
this.forEach(function(value) { items.push(value) })
|
||||
return iteratorFor(items)
|
||||
}
|
||||
|
||||
Headers.prototype.entries = function() {
|
||||
var items = []
|
||||
this.forEach(function(value, name) { items.push([name, value]) })
|
||||
return iteratorFor(items)
|
||||
}
|
||||
|
||||
if (support.iterable) {
|
||||
Headers.prototype[Symbol.iterator] = Headers.prototype.entries
|
||||
}
|
||||
|
||||
function consumed(body) {
|
||||
if (body.bodyUsed) {
|
||||
return Promise.reject(new TypeError('Already read'))
|
||||
}
|
||||
body.bodyUsed = true
|
||||
}
|
||||
|
||||
function fileReaderReady(reader) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
reader.onload = function() {
|
||||
resolve(reader.result)
|
||||
}
|
||||
reader.onerror = function() {
|
||||
reject(reader.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function readBlobAsArrayBuffer(blob) {
|
||||
var reader = new FileReader()
|
||||
var promise = fileReaderReady(reader)
|
||||
reader.readAsArrayBuffer(blob)
|
||||
return promise
|
||||
}
|
||||
|
||||
function readBlobAsText(blob) {
|
||||
var reader = new FileReader()
|
||||
var promise = fileReaderReady(reader)
|
||||
reader.readAsText(blob)
|
||||
return promise
|
||||
}
|
||||
|
||||
function readArrayBufferAsText(buf) {
|
||||
var view = new Uint8Array(buf)
|
||||
var chars = new Array(view.length)
|
||||
|
||||
for (var i = 0; i < view.length; i++) {
|
||||
chars[i] = String.fromCharCode(view[i])
|
||||
}
|
||||
return chars.join('')
|
||||
}
|
||||
|
||||
function bufferClone(buf) {
|
||||
if (buf.slice) {
|
||||
return buf.slice(0)
|
||||
} else {
|
||||
var view = new Uint8Array(buf.byteLength)
|
||||
view.set(new Uint8Array(buf))
|
||||
return view.buffer
|
||||
}
|
||||
}
|
||||
|
||||
function Body() {
|
||||
this.bodyUsed = false
|
||||
|
||||
this._initBody = function(body) {
|
||||
this._bodyInit = body
|
||||
if (!body) {
|
||||
this._bodyText = ''
|
||||
} else if (typeof body === 'string') {
|
||||
this._bodyText = body
|
||||
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
|
||||
this._bodyBlob = body
|
||||
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
|
||||
this._bodyFormData = body
|
||||
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
|
||||
this._bodyText = body.toString()
|
||||
} else if (support.arrayBuffer && support.blob && isDataView(body)) {
|
||||
this._bodyArrayBuffer = bufferClone(body.buffer)
|
||||
// IE 10-11 can't handle a DataView body.
|
||||
this._bodyInit = new Blob([this._bodyArrayBuffer])
|
||||
} else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
|
||||
this._bodyArrayBuffer = bufferClone(body)
|
||||
} else {
|
||||
throw new Error('unsupported BodyInit type')
|
||||
}
|
||||
|
||||
if (!this.headers.get('content-type')) {
|
||||
if (typeof body === 'string') {
|
||||
this.headers.set('content-type', 'text/plain;charset=UTF-8')
|
||||
} else if (this._bodyBlob && this._bodyBlob.type) {
|
||||
this.headers.set('content-type', this._bodyBlob.type)
|
||||
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
|
||||
this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (support.blob) {
|
||||
this.blob = function() {
|
||||
var rejected = consumed(this)
|
||||
if (rejected) {
|
||||
return rejected
|
||||
}
|
||||
|
||||
if (this._bodyBlob) {
|
||||
return Promise.resolve(this._bodyBlob)
|
||||
} else if (this._bodyArrayBuffer) {
|
||||
return Promise.resolve(new Blob([this._bodyArrayBuffer]))
|
||||
} else if (this._bodyFormData) {
|
||||
throw new Error('could not read FormData body as blob')
|
||||
} else {
|
||||
return Promise.resolve(new Blob([this._bodyText]))
|
||||
}
|
||||
}
|
||||
|
||||
this.arrayBuffer = function() {
|
||||
if (this._bodyArrayBuffer) {
|
||||
return consumed(this) || Promise.resolve(this._bodyArrayBuffer)
|
||||
} else {
|
||||
return this.blob().then(readBlobAsArrayBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.text = function() {
|
||||
var rejected = consumed(this)
|
||||
if (rejected) {
|
||||
return rejected
|
||||
}
|
||||
|
||||
if (this._bodyBlob) {
|
||||
return readBlobAsText(this._bodyBlob)
|
||||
} else if (this._bodyArrayBuffer) {
|
||||
return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer))
|
||||
} else if (this._bodyFormData) {
|
||||
throw new Error('could not read FormData body as text')
|
||||
} else {
|
||||
return Promise.resolve(this._bodyText)
|
||||
}
|
||||
}
|
||||
|
||||
if (support.formData) {
|
||||
this.formData = function() {
|
||||
return this.text().then(decode)
|
||||
}
|
||||
}
|
||||
|
||||
this.json = function() {
|
||||
return this.text().then(JSON.parse)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
// HTTP methods whose capitalization should be normalized
|
||||
var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']
|
||||
|
||||
function normalizeMethod(method) {
|
||||
var upcased = method.toUpperCase()
|
||||
return (methods.indexOf(upcased) > -1) ? upcased : method
|
||||
}
|
||||
|
||||
function Request(input, options) {
|
||||
options = options || {}
|
||||
var body = options.body
|
||||
|
||||
if (input instanceof Request) {
|
||||
if (input.bodyUsed) {
|
||||
throw new TypeError('Already read')
|
||||
}
|
||||
this.url = input.url
|
||||
this.credentials = input.credentials
|
||||
if (!options.headers) {
|
||||
this.headers = new Headers(input.headers)
|
||||
}
|
||||
this.method = input.method
|
||||
this.mode = input.mode
|
||||
if (!body && input._bodyInit != null) {
|
||||
body = input._bodyInit
|
||||
input.bodyUsed = true
|
||||
}
|
||||
} else {
|
||||
this.url = String(input)
|
||||
}
|
||||
|
||||
this.credentials = options.credentials || this.credentials || 'omit'
|
||||
if (options.headers || !this.headers) {
|
||||
this.headers = new Headers(options.headers)
|
||||
}
|
||||
this.method = normalizeMethod(options.method || this.method || 'GET')
|
||||
this.mode = options.mode || this.mode || null
|
||||
this.referrer = null
|
||||
|
||||
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
|
||||
throw new TypeError('Body not allowed for GET or HEAD requests')
|
||||
}
|
||||
this._initBody(body)
|
||||
}
|
||||
|
||||
Request.prototype.clone = function() {
|
||||
return new Request(this, { body: this._bodyInit })
|
||||
}
|
||||
|
||||
function decode(body) {
|
||||
var form = new FormData()
|
||||
body.trim().split('&').forEach(function(bytes) {
|
||||
if (bytes) {
|
||||
var split = bytes.split('=')
|
||||
var name = split.shift().replace(/\+/g, ' ')
|
||||
var value = split.join('=').replace(/\+/g, ' ')
|
||||
form.append(decodeURIComponent(name), decodeURIComponent(value))
|
||||
}
|
||||
})
|
||||
return form
|
||||
}
|
||||
|
||||
function parseHeaders(rawHeaders) {
|
||||
var headers = new Headers()
|
||||
// Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
|
||||
// https://tools.ietf.org/html/rfc7230#section-3.2
|
||||
var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ')
|
||||
preProcessedHeaders.split(/\r?\n/).forEach(function(line) {
|
||||
var parts = line.split(':')
|
||||
var key = parts.shift().trim()
|
||||
if (key) {
|
||||
var value = parts.join(':').trim()
|
||||
headers.append(key, value)
|
||||
}
|
||||
})
|
||||
return headers
|
||||
}
|
||||
|
||||
Body.call(Request.prototype)
|
||||
|
||||
function Response(bodyInit, options) {
|
||||
if (!options) {
|
||||
options = {}
|
||||
}
|
||||
|
||||
this.type = 'default'
|
||||
this.status = options.status === undefined ? 200 : options.status
|
||||
this.ok = this.status >= 200 && this.status < 300
|
||||
this.statusText = 'statusText' in options ? options.statusText : 'OK'
|
||||
this.headers = new Headers(options.headers)
|
||||
this.url = options.url || ''
|
||||
this._initBody(bodyInit)
|
||||
}
|
||||
|
||||
Body.call(Response.prototype)
|
||||
|
||||
Response.prototype.clone = function() {
|
||||
return new Response(this._bodyInit, {
|
||||
status: this.status,
|
||||
statusText: this.statusText,
|
||||
headers: new Headers(this.headers),
|
||||
url: this.url
|
||||
})
|
||||
}
|
||||
|
||||
Response.error = function() {
|
||||
var response = new Response(null, {status: 0, statusText: ''})
|
||||
response.type = 'error'
|
||||
return response
|
||||
}
|
||||
|
||||
var redirectStatuses = [301, 302, 303, 307, 308]
|
||||
|
||||
Response.redirect = function(url, status) {
|
||||
if (redirectStatuses.indexOf(status) === -1) {
|
||||
throw new RangeError('Invalid status code')
|
||||
}
|
||||
|
||||
return new Response(null, {status: status, headers: {location: url}})
|
||||
}
|
||||
|
||||
self.Headers = Headers
|
||||
self.Request = Request
|
||||
self.Response = Response
|
||||
|
||||
self.fetch = function(input, init) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var request = new Request(input, init)
|
||||
var xhr = new XMLHttpRequest()
|
||||
|
||||
xhr.onload = function() {
|
||||
var options = {
|
||||
status: xhr.status,
|
||||
statusText: xhr.statusText,
|
||||
headers: parseHeaders(xhr.getAllResponseHeaders() || '')
|
||||
}
|
||||
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL')
|
||||
var body = 'response' in xhr ? xhr.response : xhr.responseText
|
||||
resolve(new Response(body, options))
|
||||
}
|
||||
|
||||
xhr.onerror = function() {
|
||||
reject(new TypeError('Network request failed'))
|
||||
}
|
||||
|
||||
xhr.ontimeout = function() {
|
||||
reject(new TypeError('Network request failed'))
|
||||
}
|
||||
|
||||
xhr.open(request.method, request.url, true)
|
||||
|
||||
if (request.credentials === 'include') {
|
||||
xhr.withCredentials = true
|
||||
} else if (request.credentials === 'omit') {
|
||||
xhr.withCredentials = false
|
||||
}
|
||||
|
||||
if ('responseType' in xhr && support.blob) {
|
||||
xhr.responseType = 'blob'
|
||||
}
|
||||
|
||||
request.headers.forEach(function(value, name) {
|
||||
xhr.setRequestHeader(name, value)
|
||||
})
|
||||
|
||||
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit)
|
||||
})
|
||||
}
|
||||
self.fetch.polyfill = true
|
||||
})(typeof self !== 'undefined' ? self : this);
|
||||
2
frontend/polyfills/main.js
Normal file
2
frontend/polyfills/main.js
Normal file
@ -0,0 +1,2 @@
|
||||
import './fetch';
|
||||
import './url-search-params';
|
||||
348
frontend/polyfills/url-search-params.js
Normal file
348
frontend/polyfills/url-search-params.js
Normal file
@ -0,0 +1,348 @@
|
||||
(function(global) {
|
||||
/**
|
||||
* Polyfill URLSearchParams
|
||||
*
|
||||
* Inspired from : https://github.com/WebReflection/url-search-params/blob/master/src/url-search-params.js
|
||||
*/
|
||||
|
||||
var checkIfIteratorIsSupported = function() {
|
||||
try {
|
||||
return !!Symbol.iterator;
|
||||
} catch(error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var iteratorSupported = checkIfIteratorIsSupported();
|
||||
|
||||
var createIterator = function(items) {
|
||||
var iterator = {
|
||||
next: function() {
|
||||
var value = items.shift();
|
||||
return { done: value === void 0, value: value };
|
||||
}
|
||||
};
|
||||
|
||||
if(iteratorSupported) {
|
||||
iterator[Symbol.iterator] = function() {
|
||||
return iterator;
|
||||
};
|
||||
}
|
||||
|
||||
return iterator;
|
||||
};
|
||||
|
||||
/**
|
||||
* Search param name and values should be encoded according to https://url.spec.whatwg.org/#urlencoded-serializing
|
||||
* encodeURIComponent() produces the same result except encoding spaces as `%20` instead of `+`.
|
||||
*/
|
||||
var serializeParam = function(value) {
|
||||
return encodeURIComponent(value).replace(/%20/g, '+');
|
||||
};
|
||||
|
||||
var deserializeParam = function(value) {
|
||||
return decodeURIComponent(value).replace(/\+/g, ' ');
|
||||
};
|
||||
|
||||
var polyfillURLSearchParams= function() {
|
||||
|
||||
var URLSearchParams = function(searchString) {
|
||||
Object.defineProperty(this, '_entries', { value: {} });
|
||||
|
||||
if(typeof searchString === 'string') {
|
||||
if(searchString !== '') {
|
||||
searchString = searchString.replace(/^\?/, '');
|
||||
var attributes = searchString.split('&');
|
||||
var attribute;
|
||||
for(var i = 0; i < attributes.length; i++) {
|
||||
attribute = attributes[i].split('=');
|
||||
this.append(
|
||||
deserializeParam(attribute[0]),
|
||||
(attribute.length > 1) ? deserializeParam(attribute[1]) : ''
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if(searchString instanceof URLSearchParams) {
|
||||
var _this = this;
|
||||
searchString.forEach(function(value, name) {
|
||||
_this.append(value, name);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var proto = URLSearchParams.prototype;
|
||||
|
||||
proto.append = function(name, value) {
|
||||
if(name in this._entries) {
|
||||
this._entries[name].push(value.toString());
|
||||
} else {
|
||||
this._entries[name] = [value.toString()];
|
||||
}
|
||||
};
|
||||
|
||||
proto.delete = function(name) {
|
||||
delete this._entries[name];
|
||||
};
|
||||
|
||||
proto.get = function(name) {
|
||||
return (name in this._entries) ? this._entries[name][0] : null;
|
||||
};
|
||||
|
||||
proto.getAll = function(name) {
|
||||
return (name in this._entries) ? this._entries[name].slice(0) : [];
|
||||
};
|
||||
|
||||
proto.has = function(name) {
|
||||
return (name in this._entries);
|
||||
};
|
||||
|
||||
proto.set = function(name, value) {
|
||||
this._entries[name] = [value.toString()];
|
||||
};
|
||||
|
||||
proto.forEach = function(callback, thisArg) {
|
||||
var entries;
|
||||
for(var name in this._entries) {
|
||||
if(this._entries.hasOwnProperty(name)) {
|
||||
entries = this._entries[name];
|
||||
for(var i = 0; i < entries.length; i++) {
|
||||
callback.call(thisArg, entries[i], name, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
proto.keys = function() {
|
||||
var items = [];
|
||||
this.forEach(function(value, name) { items.push(name); });
|
||||
return createIterator(items);
|
||||
};
|
||||
|
||||
proto.values = function() {
|
||||
var items = [];
|
||||
this.forEach(function(value) { items.push(value); });
|
||||
return createIterator(items);
|
||||
};
|
||||
|
||||
proto.entries = function() {
|
||||
var items = [];
|
||||
this.forEach(function(value, name) { items.push([name, value]); });
|
||||
return createIterator(items);
|
||||
};
|
||||
|
||||
if(iteratorSupported) {
|
||||
proto[Symbol.iterator] = proto.entries;
|
||||
}
|
||||
|
||||
proto.toString = function() {
|
||||
var searchString = '';
|
||||
this.forEach(function(value, name) {
|
||||
if(searchString.length > 0) searchString+= '&';
|
||||
searchString += serializeParam(name) + '=' + serializeParam(value);
|
||||
});
|
||||
return searchString;
|
||||
};
|
||||
|
||||
global.URLSearchParams = URLSearchParams;
|
||||
};
|
||||
|
||||
if(!('URLSearchParams' in global) || (new URLSearchParams('?a=1').toString() !== 'a=1')) {
|
||||
polyfillURLSearchParams();
|
||||
}
|
||||
|
||||
// HTMLAnchorElement
|
||||
|
||||
})(
|
||||
(typeof global !== 'undefined') ? global
|
||||
: ((typeof window !== 'undefined') ? window
|
||||
: ((typeof self !== 'undefined') ? self : this))
|
||||
);
|
||||
|
||||
(function(global) {
|
||||
/**
|
||||
* Polyfill URL
|
||||
*
|
||||
* Inspired from : https://github.com/arv/DOM-URL-Polyfill/blob/master/src/url.js
|
||||
*/
|
||||
|
||||
var checkIfURLIsSupported = function() {
|
||||
try {
|
||||
var u = new URL('b', 'http://a');
|
||||
u.pathname = 'c%20d';
|
||||
return (u.href === 'http://a/c%20d') && u.searchParams;
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var polyfillURL = function() {
|
||||
var _URL = global.URL;
|
||||
|
||||
var URL = function(url, base) {
|
||||
if(typeof url !== 'string') url = String(url);
|
||||
|
||||
var doc = document.implementation.createHTMLDocument('');
|
||||
window.doc = doc;
|
||||
if(base) {
|
||||
var baseElement = doc.createElement('base');
|
||||
baseElement.href = base;
|
||||
doc.head.appendChild(baseElement);
|
||||
}
|
||||
|
||||
var anchorElement = doc.createElement('a');
|
||||
anchorElement.href = url;
|
||||
doc.body.appendChild(anchorElement);
|
||||
anchorElement.href = anchorElement.href; // force href to refresh
|
||||
|
||||
if(anchorElement.protocol === ':' || !/:/.test(anchorElement.href)) {
|
||||
throw new TypeError('Invalid URL');
|
||||
}
|
||||
|
||||
Object.defineProperty(this, '_anchorElement', {
|
||||
value: anchorElement
|
||||
});
|
||||
};
|
||||
|
||||
var proto = URL.prototype;
|
||||
|
||||
var linkURLWithAnchorAttribute = function(attributeName) {
|
||||
Object.defineProperty(proto, attributeName, {
|
||||
get: function() {
|
||||
return this._anchorElement[attributeName];
|
||||
},
|
||||
set: function(value) {
|
||||
this._anchorElement[attributeName] = value;
|
||||
},
|
||||
enumerable: true
|
||||
});
|
||||
};
|
||||
|
||||
['hash', 'host', 'hostname', 'port', 'protocol', 'search']
|
||||
.forEach(function(attributeName) {
|
||||
linkURLWithAnchorAttribute(attributeName);
|
||||
});
|
||||
|
||||
Object.defineProperties(proto, {
|
||||
|
||||
'toString': {
|
||||
get: function() {
|
||||
var _this = this;
|
||||
return function() {
|
||||
return _this.href;
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
'href' : {
|
||||
get: function() {
|
||||
return this._anchorElement.href.replace(/\?$/,'');
|
||||
},
|
||||
set: function(value) {
|
||||
this._anchorElement.href = value;
|
||||
},
|
||||
enumerable: true
|
||||
},
|
||||
|
||||
'pathname' : {
|
||||
get: function() {
|
||||
return this._anchorElement.pathname.replace(/(^\/?)/,'/');
|
||||
},
|
||||
set: function(value) {
|
||||
this._anchorElement.pathname = value;
|
||||
},
|
||||
enumerable: true
|
||||
},
|
||||
|
||||
'origin': {
|
||||
get: function() {
|
||||
// get expected port from protocol
|
||||
var expectedPort = {'http:': 80, 'https:': 443, 'ftp:': 21}[this._anchorElement.protocol];
|
||||
// add port to origin if, expected port is different than actual port
|
||||
// and it is not empty f.e http://foo:8080
|
||||
// 8080 != 80 && 8080 != ''
|
||||
var addPortToOrigin = this._anchorElement.port != expectedPort &&
|
||||
this._anchorElement.port !== ''
|
||||
|
||||
return this._anchorElement.protocol +
|
||||
'//' +
|
||||
this._anchorElement.hostname +
|
||||
(addPortToOrigin ? (':' + this._anchorElement.port) : '');
|
||||
},
|
||||
enumerable: true
|
||||
},
|
||||
|
||||
'password': { // TODO
|
||||
get: function() {
|
||||
return '';
|
||||
},
|
||||
set: function(value) {
|
||||
},
|
||||
enumerable: true
|
||||
},
|
||||
|
||||
'username': { // TODO
|
||||
get: function() {
|
||||
return '';
|
||||
},
|
||||
set: function(value) {
|
||||
},
|
||||
enumerable: true
|
||||
},
|
||||
|
||||
'searchParams': {
|
||||
get: function() {
|
||||
var searchParams = new URLSearchParams(this.search);
|
||||
var _this = this;
|
||||
['append', 'delete', 'set'].forEach(function(methodName) {
|
||||
var method = searchParams[methodName];
|
||||
searchParams[methodName] = function() {
|
||||
method.apply(searchParams, arguments);
|
||||
_this.search = searchParams.toString();
|
||||
};
|
||||
});
|
||||
return searchParams;
|
||||
},
|
||||
enumerable: true
|
||||
}
|
||||
});
|
||||
|
||||
URL.createObjectURL = function(blob) {
|
||||
return _URL.createObjectURL.apply(_URL, arguments);
|
||||
};
|
||||
|
||||
URL.revokeObjectURL = function(url) {
|
||||
return _URL.revokeObjectURL.apply(_URL, arguments);
|
||||
};
|
||||
|
||||
global.URL = URL;
|
||||
|
||||
};
|
||||
|
||||
if(!checkIfURLIsSupported()) {
|
||||
polyfillURL();
|
||||
}
|
||||
|
||||
if((global.location !== void 0) && !('origin' in global.location)) {
|
||||
var getOrigin = function() {
|
||||
return global.location.protocol + '//' + global.location.hostname + (global.location.port ? (':' + global.location.port) : '');
|
||||
};
|
||||
|
||||
try {
|
||||
Object.defineProperty(global.location, 'origin', {
|
||||
get: getOrigin,
|
||||
enumerable: true
|
||||
});
|
||||
} catch(e) {
|
||||
setInterval(function() {
|
||||
global.location.origin = getOrigin();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
})(
|
||||
(typeof global !== 'undefined') ? global
|
||||
: ((typeof window !== 'undefined') ? window
|
||||
: ((typeof self !== 'undefined') ? self : this))
|
||||
);
|
||||
43
frontend/src/main.js
Normal file
43
frontend/src/main.js
Normal file
@ -0,0 +1,43 @@
|
||||
import { HttpClient } from './services/http-client/http-client';
|
||||
import { HtmlHelpers } from './services/html-helpers/html-helpers';
|
||||
import { I18n } from './services/i18n/i18n';
|
||||
import { UtilRegistry } from './services/util-registry/util-registry';
|
||||
|
||||
// import utils
|
||||
import Utils from './utils/utils';
|
||||
|
||||
class App {
|
||||
httpClient = new HttpClient();
|
||||
htmlHelpers = new HtmlHelpers();
|
||||
i18n = new I18n();
|
||||
utilRegistry = new UtilRegistry();
|
||||
|
||||
constructor() {
|
||||
this.utilRegistry.setApp(this);
|
||||
|
||||
Utils.forEach(util => {
|
||||
this.utilRegistry.register(util);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const app = new App();
|
||||
|
||||
// attach the app to window to be able to get a hold of the
|
||||
// app instance from the shakespearean templates
|
||||
window.App = app;
|
||||
|
||||
// dont know where to put this currently...
|
||||
// interceptor to throw an error if an http response does not match the expected content-type
|
||||
// function contentTypeInterceptor(response, options) {
|
||||
// if (!options || !options.headers.get('Accept')) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const contentType = response.headers.get("content-type");
|
||||
// if (!contentType.match(options.accept)) {
|
||||
// throw new Error('Server returned with "' + contentType + '" when "' + options.accept + '" was expected');
|
||||
// }
|
||||
// }
|
||||
|
||||
// window.HttpClient.addResponseInterceptor(contentTypeInterceptor);
|
||||
41
frontend/src/services/html-helpers/html-helpers.js
Normal file
41
frontend/src/services/html-helpers/html-helpers.js
Normal file
@ -0,0 +1,41 @@
|
||||
export class HtmlHelpers {
|
||||
|
||||
// `parseResponse` takes a raw HttpClient response and an options object.
|
||||
// Returns an object with `element` being an contextual fragment of the
|
||||
// HTML in the response and `ifPrefix` being the prefix that was used to
|
||||
// "unique-ify" the ids of the received HTML.
|
||||
// Original Response IDs can optionally be kept by adding `keepIds: true`
|
||||
// to the `options` object.
|
||||
parseResponse(response, options = {}) {
|
||||
return response.text()
|
||||
.then(
|
||||
(responseText) => {
|
||||
const docFrag = document.createRange().createContextualFragment(responseText);
|
||||
let idPrefix;
|
||||
if (!options.keepIds) {
|
||||
idPrefix = this._getIdPrefix();
|
||||
this._prefixIds(docFrag, idPrefix);
|
||||
}
|
||||
return Promise.resolve({ idPrefix, element: docFrag });
|
||||
},
|
||||
Promise.reject,
|
||||
).catch(console.error);
|
||||
}
|
||||
|
||||
_prefixIds(element, idPrefix) {
|
||||
const idAttrs = ['id', 'for', 'data-conditional-input', 'data-modal-trigger'];
|
||||
|
||||
idAttrs.forEach((attr) => {
|
||||
Array.from(element.querySelectorAll('[' + attr + ']')).forEach((input) => {
|
||||
const value = idPrefix + input.getAttribute(attr);
|
||||
input.setAttribute(attr, value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_getIdPrefix() {
|
||||
// leading 'r'(andom) to overcome the fact that IDs
|
||||
// starting with a numeric value are not valid in CSS
|
||||
return 'r' + Math.floor(Math.random() * 100000) + '__';
|
||||
}
|
||||
}
|
||||
41
frontend/src/services/http-client/http-client.js
Normal file
41
frontend/src/services/http-client/http-client.js
Normal file
@ -0,0 +1,41 @@
|
||||
export class HttpClient {
|
||||
|
||||
static ACCEPT = {
|
||||
TEXT_HTML: 'text/html',
|
||||
JSON: 'application/json',
|
||||
};
|
||||
|
||||
_responseInterceptors = [];
|
||||
|
||||
addResponseInterceptor(interceptor) {
|
||||
if (typeof interceptor === 'function') {
|
||||
this._responseInterceptors.push(interceptor);
|
||||
}
|
||||
}
|
||||
|
||||
get(args) {
|
||||
args.method = 'GET';
|
||||
return this._fetch(args);
|
||||
}
|
||||
|
||||
post(args) {
|
||||
args.method = 'POST';
|
||||
return this._fetch(args);
|
||||
}
|
||||
|
||||
_fetch(options) {
|
||||
const requestOptions = {
|
||||
credentials: 'same-origin',
|
||||
...options,
|
||||
};
|
||||
|
||||
return fetch(options.url, requestOptions)
|
||||
.then(
|
||||
(response) => {
|
||||
this._responseInterceptors.forEach((interceptor) => interceptor(response, options));
|
||||
return Promise.resolve(response);
|
||||
},
|
||||
Promise.reject,
|
||||
).catch(console.error);
|
||||
}
|
||||
}
|
||||
32
frontend/src/services/i18n/i18n.js
Normal file
32
frontend/src/services/i18n/i18n.js
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* I18n
|
||||
*
|
||||
* This module stores and serves translated strings, according to the users language settings.
|
||||
*
|
||||
* Translations are stored in /messages/frontend/*.msg.
|
||||
*
|
||||
* To make additions to any of these files accessible to JavaScrip Utilities
|
||||
* you need to add them to the respective *.msg file and to the list of FrontendMessages
|
||||
* in /src/Utils/Frontend/I18n.hs.
|
||||
*
|
||||
*/
|
||||
|
||||
export class I18n {
|
||||
|
||||
translations = {};
|
||||
|
||||
add(id, translation) {
|
||||
this.translations[id] = translation;
|
||||
}
|
||||
|
||||
addMany(manyTranslations) {
|
||||
Object.keys(manyTranslations).forEach((key) => this.add(key, manyTranslations[key]));
|
||||
}
|
||||
|
||||
get(id) {
|
||||
if (!this.translations[id]) {
|
||||
throw new Error('I18N Error: Translation missing for »' + id + '«!');
|
||||
}
|
||||
return this.translations[id];
|
||||
}
|
||||
}
|
||||
122
frontend/src/services/util-registry/util-registry.js
Normal file
122
frontend/src/services/util-registry/util-registry.js
Normal file
@ -0,0 +1,122 @@
|
||||
const DEBUG_MODE = /localhost/.test(window.location.href) && 0;
|
||||
|
||||
export class UtilRegistry {
|
||||
|
||||
_registeredUtils = [];
|
||||
_activeUtilInstances = [];
|
||||
_appInstance;
|
||||
|
||||
constructor() {
|
||||
document.addEventListener('DOMContentLoaded', () => this.setupAll());
|
||||
}
|
||||
|
||||
/**
|
||||
* function registerUtil
|
||||
*
|
||||
* utils need to have at least these properties:
|
||||
* name: string | utils name, e.g. 'example'
|
||||
* selector: string | utils selector, e.g. '[uw-example]'
|
||||
* setup: Function | utils setup function, see below
|
||||
*
|
||||
* setup function must return instance object with at least these properties:
|
||||
* name: string | utils name
|
||||
* element: HTMLElement | element the util is applied to
|
||||
* destroy: Function | function to destroy the util and remove any listeners
|
||||
*
|
||||
* @param util Object Utility that should be added to the registry
|
||||
*/
|
||||
register(util) {
|
||||
if (DEBUG_MODE > 2) {
|
||||
console.log('registering util "' + util.name + '"');
|
||||
console.log({ util });
|
||||
}
|
||||
this._registeredUtils.push(util);
|
||||
}
|
||||
|
||||
deregister(name, destroy) {
|
||||
const utilIndex = this._findUtilIndex(name);
|
||||
|
||||
if (utilIndex >= 0) {
|
||||
if (destroy === true) {
|
||||
this._destroyUtilInstances(name);
|
||||
}
|
||||
|
||||
this._registeredUtils.splice(utilIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
setApp(appInstance) {
|
||||
this.appInstance = appInstance;
|
||||
}
|
||||
|
||||
setupAll = (scope) => {
|
||||
if (DEBUG_MODE > 1) {
|
||||
console.info('registered js utilities:');
|
||||
console.table(this._registeredUtils);
|
||||
}
|
||||
|
||||
this._registeredUtils.forEach((util) => this.setup(util, scope));
|
||||
}
|
||||
|
||||
setup(util, scope = document.body) {
|
||||
if (DEBUG_MODE > 2) {
|
||||
console.log('setting up util', { util });
|
||||
}
|
||||
|
||||
if (util && typeof util.setup === 'function') {
|
||||
const elements = this._findUtilElements(util, scope);
|
||||
|
||||
elements.forEach((element) => {
|
||||
let utilInstance = null;
|
||||
|
||||
try {
|
||||
utilInstance = util.setup(element, this.appInstance);
|
||||
} catch(err) {
|
||||
if (DEBUG_MODE > 0) {
|
||||
console.warn('Error while trying to initialize a utility!', { util , element, err });
|
||||
}
|
||||
}
|
||||
|
||||
if (utilInstance) {
|
||||
if (DEBUG_MODE > 2) {
|
||||
console.info('Got utility instance for utility "' + util.name + '"', { utilInstance });
|
||||
}
|
||||
|
||||
this._activeUtilInstances.push(utilInstance);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
find(name) {
|
||||
return this._registeredUtils.find((util) => util.name === name);
|
||||
}
|
||||
|
||||
_findUtilElements(util, scope) {
|
||||
if (scope && scope.matches(util.selector)) {
|
||||
return [scope];
|
||||
}
|
||||
return Array.from(scope.querySelectorAll(util.selector));
|
||||
}
|
||||
|
||||
_findUtilIndex(name) {
|
||||
return this._registeredUtils.findIndex((util) => util.name === name);
|
||||
}
|
||||
|
||||
_destroyUtilInstances(name) {
|
||||
this._activeUtilInstances
|
||||
.map((util, index) => ({
|
||||
util: util,
|
||||
index: index,
|
||||
}))
|
||||
.filter((activeUtil) => activeUtil.util.name === name)
|
||||
.forEach((activeUtil) => {
|
||||
// destroy util instance
|
||||
activeUtil.util.destroy();
|
||||
delete this._activeUtilInstances[activeUtil.index];
|
||||
});
|
||||
|
||||
// get rid of now empty array slots
|
||||
this._activeUtilInstances = this._activeUtilInstances.filter((util) => !!util);
|
||||
}
|
||||
}
|
||||
206
frontend/src/utils/alerts/alerts.js
Normal file
206
frontend/src/utils/alerts/alerts.js
Normal file
@ -0,0 +1,206 @@
|
||||
import './alerts.scss';
|
||||
|
||||
/**
|
||||
*
|
||||
* Alerts Utility
|
||||
* makes alerts interactive
|
||||
*
|
||||
* Attribute: uw-alerts
|
||||
*
|
||||
* Types of alerts:
|
||||
* [default]
|
||||
* Regular Info Alert
|
||||
* Disappears automatically after 30 seconds
|
||||
* Disappears after x seconds if explicitly specified via data-decay='x'
|
||||
* Can be told not to disappear with data-decay='0'
|
||||
*
|
||||
* [success]
|
||||
* Currently no special visual appearance
|
||||
* Disappears automatically after 30 seconds
|
||||
*
|
||||
* [warning]
|
||||
* Will be coloured warning-orange regardless of user's selected theme
|
||||
* Does not disappear
|
||||
*
|
||||
* [error]
|
||||
* Will be coloured error-red regardless of user's selected theme
|
||||
* Does not disappear
|
||||
*
|
||||
* Example usage:
|
||||
* <div .alerts uw-alerts>
|
||||
* <div .alerts__toggler>
|
||||
* <div .alert.alert-info>
|
||||
* <div .alert__closer>
|
||||
* <div .alert__icon>
|
||||
* <div .alert__content>
|
||||
* This is some information
|
||||
*
|
||||
*/
|
||||
|
||||
var ALERTS_UTIL_NAME = 'alerts';
|
||||
var ALERTS_UTIL_SELECTOR = '[uw-alerts]';
|
||||
|
||||
var ALERTS_INITIALIZED_CLASS = 'alerts--initialized';
|
||||
var ALERTS_ELEVATED_CLASS = 'alerts--elevated';
|
||||
var ALERTS_TOGGLER_CLASS = 'alerts__toggler';
|
||||
var ALERTS_TOGGLER_VISIBLE_CLASS = 'alerts__toggler--visible';
|
||||
var ALERTS_TOGGLER_APPEAR_DELAY = 120;
|
||||
|
||||
var ALERT_CLASS = 'alert';
|
||||
var ALERT_INITIALIZED_CLASS = 'alert--initialized';
|
||||
var ALERT_CLOSER_CLASS = 'alert__closer';
|
||||
var ALERT_ICON_CLASS = 'alert__icon';
|
||||
var ALERT_CONTENT_CLASS = 'alert__content';
|
||||
var ALERT_INVISIBLE_CLASS = 'alert--invisible';
|
||||
var ALERT_AUTO_HIDE_DELAY = 10;
|
||||
var ALERT_AUTOCLOSING_MATCHER = '.alert-info, .alert-success';
|
||||
|
||||
var alertsUtil = function(element, app) {
|
||||
var togglerCheckRequested = false;
|
||||
var togglerElement;
|
||||
var alertElements;
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Alerts util has to be called with an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(ALERTS_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
togglerElement = element.querySelector('.' + ALERTS_TOGGLER_CLASS);
|
||||
alertElements = gatherAlertElements();
|
||||
|
||||
initToggler();
|
||||
initAlerts();
|
||||
|
||||
// register http client interceptor to filter out Alerts Header
|
||||
setupHttpInterceptor();
|
||||
|
||||
// mark initialized
|
||||
element.classList.add(ALERTS_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: ALERTS_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
function gatherAlertElements() {
|
||||
return Array.from(element.querySelectorAll('.' + ALERT_CLASS)).filter(function(alert) {
|
||||
return !alert.classList.contains(ALERT_INITIALIZED_CLASS);
|
||||
});
|
||||
}
|
||||
|
||||
function initToggler() {
|
||||
togglerElement.addEventListener('click', function() {
|
||||
alertElements.forEach(function(alertEl) {
|
||||
toggleAlert(alertEl, true);
|
||||
});
|
||||
togglerElement.classList.remove(ALERTS_TOGGLER_VISIBLE_CLASS);
|
||||
});
|
||||
}
|
||||
|
||||
function initAlerts() {
|
||||
alertElements.forEach(initAlert);
|
||||
}
|
||||
|
||||
function initAlert(alertElement) {
|
||||
var autoHideDelay = ALERT_AUTO_HIDE_DELAY;
|
||||
if (alertElement.dataset.decay) {
|
||||
autoHideDelay = parseInt(alertElement.dataset.decay, 10);
|
||||
}
|
||||
|
||||
var closeEl = alertElement.querySelector('.' + ALERT_CLOSER_CLASS);
|
||||
closeEl.addEventListener('click', function() {
|
||||
toggleAlert(alertElement);
|
||||
});
|
||||
|
||||
if (autoHideDelay > 0 && alertElement.matches(ALERT_AUTOCLOSING_MATCHER)) {
|
||||
window.setTimeout(function() {
|
||||
toggleAlert(alertElement);
|
||||
}, autoHideDelay * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAlert(alertEl, visible) {
|
||||
alertEl.classList.toggle(ALERT_INVISIBLE_CLASS, !visible);
|
||||
checkToggler();
|
||||
}
|
||||
|
||||
function checkToggler() {
|
||||
if (togglerCheckRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
var alertsHidden = alertElements.reduce(function(acc, alert) {
|
||||
return acc && alert.classList.contains(ALERT_INVISIBLE_CLASS);
|
||||
}, true);
|
||||
|
||||
window.setTimeout(function() {
|
||||
togglerElement.classList.toggle(ALERTS_TOGGLER_VISIBLE_CLASS, alertsHidden);
|
||||
togglerCheckRequested = false;
|
||||
}, ALERTS_TOGGLER_APPEAR_DELAY);
|
||||
}
|
||||
|
||||
function setupHttpInterceptor() {
|
||||
app.httpClient.addResponseInterceptor(responseInterceptor.bind(this));
|
||||
}
|
||||
|
||||
function elevateAlerts() {
|
||||
element.classList.add(ALERTS_ELEVATED_CLASS);
|
||||
}
|
||||
|
||||
function responseInterceptor(response) {
|
||||
var alerts;
|
||||
for (var header of response.headers) {
|
||||
if (header[0] === 'alerts') {
|
||||
var decodedHeader = decodeURIComponent(header[1]);
|
||||
alerts = JSON.parse(decodedHeader);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (alerts) {
|
||||
alerts.forEach(function(alert) {
|
||||
var alertElement = createAlertElement(alert.status, alert.content);
|
||||
element.appendChild(alertElement);
|
||||
alertElements.push(alertElement);
|
||||
initAlert(alertElement);
|
||||
});
|
||||
|
||||
elevateAlerts();
|
||||
}
|
||||
}
|
||||
|
||||
function createAlertElement(type, content) {
|
||||
var alertElement = document.createElement('div');
|
||||
alertElement.classList.add(ALERT_CLASS, 'alert-' + type);
|
||||
|
||||
var alertCloser = document.createElement('div');
|
||||
alertCloser.classList.add(ALERT_CLOSER_CLASS);
|
||||
|
||||
var alertIcon = document.createElement('div');
|
||||
alertIcon.classList.add(ALERT_ICON_CLASS);
|
||||
|
||||
var alertContent = document.createElement('div');
|
||||
alertContent.classList.add(ALERT_CONTENT_CLASS);
|
||||
alertContent.innerHTML = content;
|
||||
|
||||
alertElement.appendChild(alertCloser);
|
||||
alertElement.appendChild(alertIcon);
|
||||
alertElement.appendChild(alertContent);
|
||||
|
||||
return alertElement;
|
||||
}
|
||||
|
||||
return init();
|
||||
};
|
||||
|
||||
export default {
|
||||
name: ALERTS_UTIL_NAME,
|
||||
selector: ALERTS_UTIL_SELECTOR,
|
||||
setup: alertsUtil,
|
||||
};
|
||||
210
frontend/src/utils/alerts/alerts.scss
Normal file
210
frontend/src/utils/alerts/alerts.scss
Normal file
@ -0,0 +1,210 @@
|
||||
.alerts {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 5%;
|
||||
z-index: 20;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.alerts__toggler {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
position: absolute;
|
||||
top: 400px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
content: '\f077';
|
||||
position: absolute;
|
||||
font-family: "Font Awesome 5 Free";
|
||||
left: 50%;
|
||||
top: 0;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
color: var(--color-grey);
|
||||
font-size: 30px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.alerts--elevated {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.alerts__toggler--visible {
|
||||
top: -40px;
|
||||
opacity: 1;
|
||||
transition: top .5s cubic-bezier(0.73, 1.25, 0.61, 1),
|
||||
opacity .5s cubic-bezier(0.73, 1.25, 0.61, 1);
|
||||
}
|
||||
|
||||
@media (max-width: 425px) {
|
||||
|
||||
.alerts {
|
||||
left: 5%;
|
||||
}
|
||||
}
|
||||
|
||||
.alert {
|
||||
position: relative;
|
||||
display: block;
|
||||
background-color: var(--color-lightblack);
|
||||
font-size: 1rem;
|
||||
color: var(--color-lightwhite);
|
||||
z-index: 0;
|
||||
padding: 0 50px;
|
||||
padding-right: 60px;
|
||||
animation: slide-in-alert .2s ease-out forwards;
|
||||
margin-bottom: 10px;
|
||||
transition: margin-bottom .2s ease-out;
|
||||
}
|
||||
|
||||
.alert a {
|
||||
color: var(--color-lightwhite);
|
||||
}
|
||||
|
||||
@keyframes slide-in-alert {
|
||||
from {
|
||||
transform: translateY(120%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-out-alert {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
max-height: 200px;
|
||||
}
|
||||
to {
|
||||
transform: translateY(250%);
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 425px) {
|
||||
|
||||
.alert {
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.alert--invisible {
|
||||
animation: slide-out-alert .2s ease-out forwards;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.alert__content {
|
||||
padding: 8px 0;
|
||||
min-height: 40px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.alert__icon {
|
||||
text-align: right;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0;
|
||||
width: 50px;
|
||||
height: 100%;
|
||||
z-index: 40;
|
||||
|
||||
&::before {
|
||||
content: '\f05a';
|
||||
position: absolute;
|
||||
font-family: "Font Awesome 5 Free";
|
||||
font-size: 24px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.alert__closer {
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0;
|
||||
width: 60px;
|
||||
height: 100%;
|
||||
transition: all .3s ease;
|
||||
z-index: 40;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05, 1.05);
|
||||
|
||||
&::before {
|
||||
box-shadow: 0 0 4px white;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '\f00d';
|
||||
position: absolute;
|
||||
font-family: "Font Awesome 5 Free";
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
transition: all .15s ease;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
.alert__closer {
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: var(--color-success);
|
||||
|
||||
.alert__icon::before {
|
||||
content: '\f058';
|
||||
}
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: var(--color-warning);
|
||||
|
||||
.alert__icon::before {
|
||||
content: '\f06a';
|
||||
}
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background-color: var(--color-error);
|
||||
|
||||
.alert__icon::before {
|
||||
content: '\f071';
|
||||
}
|
||||
}
|
||||
105
frontend/src/utils/asidenav/asidenav.js
Normal file
105
frontend/src/utils/asidenav/asidenav.js
Normal file
@ -0,0 +1,105 @@
|
||||
import './asidenav.scss';
|
||||
|
||||
/**
|
||||
*
|
||||
* Asidenav Utility
|
||||
* Correctly positions hovered asidenav submenus and handles the favorites button on mobile
|
||||
*
|
||||
* Attribute: uw-asidenav
|
||||
*
|
||||
* Example usage:
|
||||
* <div uw-asidenav>
|
||||
* <div .asidenav>
|
||||
* <div .asidenav__box>
|
||||
* <ul .asidenav__list.list--iconless>
|
||||
* <li .asidenav__list-item>
|
||||
* <a .asidenav__link-wrapper href="#">
|
||||
* <div .asidenav__link-shorthand>EIP
|
||||
* <div .asidenav__link-label>Einführung in die Programmierung
|
||||
* <div .asidenav__nested-list-wrapper>
|
||||
* <ul .asidenav__nested-list.list--iconless>
|
||||
* Übungsblätter
|
||||
* ...
|
||||
*
|
||||
*/
|
||||
|
||||
var ASIDENAV_UTIL_NAME = 'asidenav';
|
||||
var ASIDENAV_UTIL_SELECTOR = '[uw-asidenav]';
|
||||
|
||||
var FAVORITES_BTN_CLASS = 'navbar__list-item--favorite';
|
||||
var FAVORITES_BTN_ACTIVE_CLASS = 'navbar__list-item--active';
|
||||
var ASIDENAV_INITIALIZED_CLASS = 'asidenav--initialized';
|
||||
var ASIDENAV_EXPANDED_CLASS = 'main__aside--expanded';
|
||||
var ASIDENAV_LIST_ITEM_CLASS = 'asidenav__list-item';
|
||||
var ASIDENAV_SUBMENU_CLASS = 'asidenav__nested-list-wrapper';
|
||||
|
||||
var asidenavUtil = function(element) {
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Asidenav utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(ASIDENAV_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
initFavoritesButton();
|
||||
initAsidenavSubmenus();
|
||||
|
||||
// mark initialized
|
||||
element.classList.add(ASIDENAV_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: ASIDENAV_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
function initFavoritesButton() {
|
||||
var favoritesBtn = document.querySelector('.' + FAVORITES_BTN_CLASS);
|
||||
favoritesBtn.addEventListener('click', function(event) {
|
||||
favoritesBtn.classList.toggle(FAVORITES_BTN_ACTIVE_CLASS);
|
||||
element.classList.toggle(ASIDENAV_EXPANDED_CLASS);
|
||||
event.preventDefault();
|
||||
}, true);
|
||||
}
|
||||
|
||||
function initAsidenavSubmenus() {
|
||||
var asidenavLinksWithSubmenus = Array.from(element.querySelectorAll('.' + ASIDENAV_LIST_ITEM_CLASS))
|
||||
.map(function(listItem) {
|
||||
var submenu = listItem.querySelector('.' + ASIDENAV_SUBMENU_CLASS);
|
||||
return { listItem, submenu };
|
||||
}).filter(function(union) {
|
||||
return union.submenu !== null;
|
||||
});
|
||||
|
||||
asidenavLinksWithSubmenus.forEach(function(union) {
|
||||
union.listItem.addEventListener('mouseover', createMouseoverHandler(union));
|
||||
});
|
||||
}
|
||||
|
||||
function createMouseoverHandler(union) {
|
||||
return function mouseoverHanlder() {
|
||||
var rectListItem = union.listItem.getBoundingClientRect();
|
||||
var rectSubMenu = union.submenu.getBoundingClientRect();
|
||||
|
||||
union.submenu.style.left = (rectListItem.left + rectListItem.width) + 'px';
|
||||
if (window.innerHeight - rectListItem.top < rectSubMenu.height) {
|
||||
union.submenu.style.top = (rectListItem.top + rectListItem.height - rectSubMenu.height) + 'px';
|
||||
} else {
|
||||
union.submenu.style.top = rectListItem.top + 'px';
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
return init();
|
||||
};
|
||||
|
||||
export default {
|
||||
name: ASIDENAV_UTIL_NAME,
|
||||
selector: ASIDENAV_UTIL_SELECTOR,
|
||||
setup: asidenavUtil,
|
||||
};
|
||||
364
frontend/src/utils/asidenav/asidenav.scss
Normal file
364
frontend/src/utils/asidenav/asidenav.scss
Normal file
@ -0,0 +1,364 @@
|
||||
.main__aside {
|
||||
position: fixed;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: var(--asidenav-width-lg, 20%);
|
||||
height: 100%;
|
||||
flex: 0 0 0;
|
||||
flex-basis: var(--asidenav-width-lg, 20%);
|
||||
transition: all .2s ease-out;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--color-dark);
|
||||
opacity: 0.05;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: -2;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--color-grey-light);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 425px) {
|
||||
|
||||
.main__aside {
|
||||
position: fixed;
|
||||
top: var(--header-height-collapsed);
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 100% !important;
|
||||
width: 100%;
|
||||
z-index: 5;
|
||||
overflow: hidden;
|
||||
transform: translateX(-110%);
|
||||
transition: transform .2s ease-out;
|
||||
|
||||
&.main__aside--expanded {
|
||||
transform: translateX(0%);
|
||||
}
|
||||
|
||||
.asidenav__box-title {
|
||||
font-size: 18px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.main__aside {
|
||||
width: var(--asidenav-width-xl, 250px)
|
||||
}
|
||||
}
|
||||
|
||||
.asidenav {
|
||||
color: var(--color-font);
|
||||
min-height: calc(100% - var(--header-height));
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.asidenav__box {
|
||||
transition: opacity .2s ease;
|
||||
|
||||
+ .asidenav__box {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.asidenav__box-title {
|
||||
padding: 7px 13px;
|
||||
margin-top: 30px;
|
||||
background-color: transparent;
|
||||
transition: all .2s ease;
|
||||
padding: 10px 13px;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid var(--color-grey);
|
||||
}
|
||||
|
||||
/* LOGO */
|
||||
|
||||
.asidenav__logo {
|
||||
height: var(--header-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
.asidenav__logo {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.asidenav__logo-link {
|
||||
flex: 1;
|
||||
top: 10px;
|
||||
left: 20px;
|
||||
height: 80px;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
flex-basis: var(--asidenav-width-xl, 250px);
|
||||
font-size: 16px;
|
||||
align-items: center;
|
||||
color: var(--color-dark);
|
||||
transform-origin: left;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.asidenav__logo-lmu {
|
||||
width: 80px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.asidenav__logo-uni2work {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
min-width: 70px;
|
||||
margin-left: 12px;
|
||||
text-transform: uppercase;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 2px 4px;
|
||||
border: 1px solid currentColor;
|
||||
letter-spacing: 2px;
|
||||
background-color: white;
|
||||
transition: background-color .3s ease;
|
||||
}
|
||||
|
||||
@media (max-width: 1199px) {
|
||||
|
||||
.asidenav__logo-link {
|
||||
flex-basis: var(--asidenav-width-lg, 20%);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.asidenav__logo-lmu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.asidenav__logo-uni2work {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* SEAL */
|
||||
|
||||
.asidenav__sigillum {
|
||||
position: absolute;
|
||||
bottom: -40px;
|
||||
right: 25px;
|
||||
opacity: 0.2;
|
||||
|
||||
> img {
|
||||
width: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.asidenav__sigillum {
|
||||
right: auto;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
/* LIST-ITEM */
|
||||
|
||||
.asidenav__list-item {
|
||||
color: var(--color-font);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
&:not(.asidenav__list-item--active):hover {
|
||||
background-color: var(--color-lightwhite);
|
||||
|
||||
> .asidenav__link-wrapper {
|
||||
color: var(--color-font);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
.asidenav__link-shorthand {
|
||||
transform: scale(1.05, 1.0);
|
||||
transform-origin: right;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.asidenav__nested-list-wrapper {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* small list-item-padding for medium to large screens */
|
||||
@media (min-width: 769px) {
|
||||
|
||||
.asidenav__list-item {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.asidenav__list-item--active {
|
||||
background-color: var(--color-lightwhite);
|
||||
|
||||
.asidenav__link-wrapper {
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.asidenav__link-shorthand {
|
||||
transform: scale(1.05, 1.0);
|
||||
transform-origin: right;
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.asidenav__link-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
padding: 8px 3px;
|
||||
justify-content: flex-start;
|
||||
color: var(--color-font);
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.asidenav__link-shorthand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.asidenav__link-label {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* hover sub-menus */
|
||||
.asidenav__nested-list-wrapper {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
display: none;
|
||||
color: var(--color-font);
|
||||
background-color: var(--color-grey-light);
|
||||
box-shadow: 1px 1px 1px 0px var(--color-grey);
|
||||
}
|
||||
|
||||
.asidenav__nested-list {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
@media (max-width: 425px) {
|
||||
|
||||
.asidenav__nested-list {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.asidenav__nested-list-item {
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-lightwhite);
|
||||
}
|
||||
|
||||
.asidenav__link-wrapper {
|
||||
padding-left: 13px;
|
||||
padding-right: 13px;
|
||||
transition: all .2s ease;
|
||||
color: var(--color-font);
|
||||
}
|
||||
}
|
||||
|
||||
/* TABLET */
|
||||
@media (min-width: 426px) and (max-width: 768px) {
|
||||
|
||||
.main__aside {
|
||||
width: var(--asidenav-width-md, 50px);
|
||||
flex-basis: var(--asidenav-width-md, 50px);
|
||||
overflow: hidden;
|
||||
min-height: calc(100% - var(--header-height-collapsed));
|
||||
top: var(--header-height-collapsed);
|
||||
|
||||
.asidenav__box-title {
|
||||
width: var(--asidenav-width-md, 50px);
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
padding: 10px 1px;
|
||||
word-break: break-all;
|
||||
background-color: var(--color-dark);
|
||||
color: var(--color-lightwhite);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.asidenav__link-shorthand {
|
||||
display: flex;
|
||||
position: static;
|
||||
height: 50px;
|
||||
width: var(--asidenav-width-md, 50px);
|
||||
text-align: center;
|
||||
opacity: 1;
|
||||
font-size: 15px;
|
||||
line-height: 1em;
|
||||
margin-right: 13px;
|
||||
flex-shrink: 0;
|
||||
padding: 1px;
|
||||
outline: 1px solid white;
|
||||
word-break: break-all;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.asidenav__list-item {
|
||||
padding-left: 0;
|
||||
|
||||
+ .asidenav__list-item {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.asidenav__link-wrapper {
|
||||
color: var(--color-font);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.asidenav__nested-list,
|
||||
.asidenav__link-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.asidenav__list-item--active {
|
||||
|
||||
.asidenav__link-wrapper {
|
||||
background-color: var(--color-lightwhite);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
117
frontend/src/utils/async-form/async-form.js
Normal file
117
frontend/src/utils/async-form/async-form.js
Normal file
@ -0,0 +1,117 @@
|
||||
import './async-form.scss';
|
||||
|
||||
/**
|
||||
*
|
||||
* Async Form Utility
|
||||
* prevents form submissions from reloading the page but instead firing an AJAX request
|
||||
*
|
||||
* Attribute: uw-async-form
|
||||
* (works only on <form> elements)
|
||||
*
|
||||
* Example usage:
|
||||
* <form uw-async-form method='POST' action='...'>
|
||||
* ...
|
||||
*
|
||||
* Internationalization:
|
||||
* This utility expects the following translations to be available:
|
||||
* asyncFormFailure: text that gets shown if an async form request fails
|
||||
* example: "Oops. Something went wrong."
|
||||
*/
|
||||
|
||||
var ASYNC_FORM_UTIL_NAME = 'asyncForm';
|
||||
var ASYNC_FORM_UTIL_SELECTOR = 'form[uw-async-form]';
|
||||
|
||||
var ASYNC_FORM_INITIALIZED_CLASS = 'check-all--initialized';
|
||||
var ASYNC_FORM_RESPONSE_CLASS = 'async-form__response';
|
||||
var ASYNC_FORM_LOADING_CLASS = 'async-form--loading';
|
||||
var ASYNC_FORM_MIN_DELAY = 600;
|
||||
|
||||
var MODAL_SELECTOR = '.modal';
|
||||
var MODAL_HEADER_KEY = 'Is-Modal';
|
||||
var MODAL_HEADER_VALUE = 'True';
|
||||
|
||||
var asyncFormUtil = function(element, app) {
|
||||
|
||||
var lastRequestTimestamp = 0;
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Async Form Utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(ASYNC_FORM_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
element.addEventListener('submit', submitHandler);
|
||||
|
||||
element.classList.add(ASYNC_FORM_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: ASYNC_FORM_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
function processResponse(response) {
|
||||
var responseElement = makeResponseElement(response.content, response.status);
|
||||
var parentElement = element.parentElement;
|
||||
|
||||
// make sure there is a delay between click and response
|
||||
var delay = Math.max(0, ASYNC_FORM_MIN_DELAY + lastRequestTimestamp - Date.now());
|
||||
|
||||
setTimeout(function() {
|
||||
parentElement.insertBefore(responseElement, element);
|
||||
element.remove();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
function makeResponseElement(content, status) {
|
||||
var responseElement = document.createElement('div');
|
||||
status = status || 'info';
|
||||
responseElement.classList.add(ASYNC_FORM_RESPONSE_CLASS);
|
||||
responseElement.classList.add(ASYNC_FORM_RESPONSE_CLASS + '--' + status);
|
||||
responseElement.innerHTML = content;
|
||||
return responseElement;
|
||||
}
|
||||
|
||||
function submitHandler(event) {
|
||||
event.preventDefault();
|
||||
|
||||
element.classList.add(ASYNC_FORM_LOADING_CLASS);
|
||||
lastRequestTimestamp = Date.now();
|
||||
|
||||
var url = element.getAttribute('action');
|
||||
var headers = { };
|
||||
var body = new FormData(element);
|
||||
|
||||
var isModal = element.closest(MODAL_SELECTOR);
|
||||
if (isModal) {
|
||||
headers[MODAL_HEADER_KEY] = MODAL_HEADER_VALUE;
|
||||
}
|
||||
|
||||
app.httpClient.post({
|
||||
url: url,
|
||||
headers: headers,
|
||||
body: body,
|
||||
}).then(function(response) {
|
||||
return response.json();
|
||||
}).then(function(response) {
|
||||
processResponse(response[0]);
|
||||
}).catch(function() {
|
||||
var failureMessage = app.i18n.get('asyncFormFailure');
|
||||
processResponse({ content: failureMessage });
|
||||
|
||||
element.classList.remove(ASYNC_FORM_LOADING_CLASS);
|
||||
});
|
||||
}
|
||||
|
||||
return init();
|
||||
};
|
||||
|
||||
export default {
|
||||
name: ASYNC_FORM_UTIL_NAME,
|
||||
selector: ASYNC_FORM_UTIL_SELECTOR,
|
||||
setup: asyncFormUtil,
|
||||
};
|
||||
78
frontend/src/utils/async-form/async-form.scss
Normal file
78
frontend/src/utils/async-form/async-form.scss
Normal file
@ -0,0 +1,78 @@
|
||||
.async-form__response {
|
||||
margin: 20px 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
padding-top: 60px;
|
||||
}
|
||||
|
||||
.async-form__response::before,
|
||||
.async-form__response::after {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 50%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.async-form__response--success::before {
|
||||
content: '';
|
||||
width: 17px;
|
||||
height: 28px;
|
||||
border: solid #069e04;
|
||||
border-width: 0 5px 5px 0;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.async-form__response--info::before {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 30px;
|
||||
top: 10px;
|
||||
background-color: #777;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.async-form__response--info::after {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background-color: #777;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.async-form__response--warning::before {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 30px;
|
||||
background-color: rgb(255, 187, 0);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.async-form__response--warning::after {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
top: 35px;
|
||||
background-color: rgb(255, 187, 0);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.async-form__response--error::before {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 40px;
|
||||
background-color: #940d0d;
|
||||
transform: translateX(-50%) rotate(-45deg);
|
||||
}
|
||||
.async-form__response--error::after {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 40px;
|
||||
background-color: #940d0d;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.async-form--loading {
|
||||
opacity: 0.1;
|
||||
transition: opacity 800ms ease-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
5
frontend/src/utils/async-table/async-table-filter.scss
Normal file
5
frontend/src/utils/async-table/async-table-filter.scss
Normal file
@ -0,0 +1,5 @@
|
||||
.async-table-filter--loading {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
transition: opacity 400ms ease-out;
|
||||
}
|
||||
413
frontend/src/utils/async-table/async-table.js
Normal file
413
frontend/src/utils/async-table/async-table.js
Normal file
@ -0,0 +1,413 @@
|
||||
import { HttpClient } from '../../services/http-client/http-client';
|
||||
import './async-table.scss';
|
||||
import './async-table-filter.scss';
|
||||
|
||||
/**
|
||||
*
|
||||
* Async Table Utility
|
||||
* makes table filters, sorting and pagination behave asynchronously via AJAX calls
|
||||
*
|
||||
* Attribute: uw-async-table
|
||||
*
|
||||
* Example usage:
|
||||
* (regular table)
|
||||
*/
|
||||
|
||||
var INPUT_DEBOUNCE = 600;
|
||||
var HEADER_HEIGHT = 80;
|
||||
|
||||
var ASYNC_TABLE_UTIL_NAME = 'asyncTable';
|
||||
var ASYNC_TABLE_UTIL_SELECTOR = '[uw-async-table]';
|
||||
|
||||
var ASYNC_TABLE_LOCAL_STORAGE_KEY = 'ASYNC_TABLE';
|
||||
var ASYNC_TABLE_SCROLLTABLE_SELECTOR = '.scrolltable';
|
||||
var ASYNC_TABLE_INITIALIZED_CLASS = 'async-table--initialized';
|
||||
var ASYNC_TABLE_LOADING_CLASS = 'async-table--loading';
|
||||
|
||||
var ASYNC_TABLE_FILTER_FORM_SELECTOR = '.table-filter-form';
|
||||
var ASYNC_TABLE_FILTER_FORM_ID_SELECTOR = '[name="form-identifier"]';
|
||||
|
||||
|
||||
var asyncTableUtil = function(element, app) {
|
||||
var asyncTableHeader;
|
||||
var asyncTableId;
|
||||
|
||||
var ths = [];
|
||||
var pageLinks = [];
|
||||
var pagesizeForm;
|
||||
var scrollTable;
|
||||
var cssIdPrefix = '';
|
||||
|
||||
var tableFilterInputs = {
|
||||
search: [],
|
||||
input: [],
|
||||
change: [],
|
||||
select: [],
|
||||
};
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Async Table utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(ASYNC_TABLE_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// param asyncTableDbHeader
|
||||
if (element.dataset.asyncTableDbHeader !== undefined) {
|
||||
asyncTableHeader = element.dataset.asyncTableDbHeader;
|
||||
}
|
||||
|
||||
var rawTableId = element.querySelector('table').id;
|
||||
cssIdPrefix = findCssIdPrefix(rawTableId);
|
||||
asyncTableId = rawTableId.replace(cssIdPrefix, '');
|
||||
|
||||
// find scrolltable wrapper
|
||||
scrollTable = element.querySelector(ASYNC_TABLE_SCROLLTABLE_SELECTOR);
|
||||
if (!scrollTable) {
|
||||
throw new Error('Async Table cannot be set up without a scrolltable element!');
|
||||
}
|
||||
|
||||
setupSortableHeaders();
|
||||
setupPagination();
|
||||
setupPageSizeSelect();
|
||||
setupTableFilter();
|
||||
|
||||
processLocalStorage();
|
||||
|
||||
// clear currentTableUrl from previous requests
|
||||
setLocalStorageParameter('currentTableUrl', null);
|
||||
|
||||
// mark initialized
|
||||
element.classList.add(ASYNC_TABLE_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: ASYNC_TABLE_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
function setupSortableHeaders() {
|
||||
ths = Array.from(scrollTable.querySelectorAll('th.sortable')).map(function(th) {
|
||||
return { element: th };
|
||||
});
|
||||
|
||||
ths.forEach(function(th) {
|
||||
th.clickHandler = function(event) {
|
||||
setLocalStorageParameter('horizPos', (scrollTable || {}).scrollLeft);
|
||||
linkClickHandler(event);
|
||||
};
|
||||
th.element.addEventListener('click', th.clickHandler);
|
||||
});
|
||||
}
|
||||
|
||||
function setupPagination() {
|
||||
var pagination = element.querySelector('#' + cssIdPrefix + asyncTableId + '-pagination');
|
||||
if (pagination) {
|
||||
pageLinks = Array.from(pagination.querySelectorAll('.page-link')).map(function(link) {
|
||||
return { element: link };
|
||||
});
|
||||
|
||||
pageLinks.forEach(function(link) {
|
||||
link.clickHandler = function(event) {
|
||||
var tableBoundingRect = scrollTable.getBoundingClientRect();
|
||||
if (tableBoundingRect.top < HEADER_HEIGHT) {
|
||||
var scrollTo = {
|
||||
top: (scrollTable.offsetTop || 0) - HEADER_HEIGHT,
|
||||
left: scrollTable.offsetLeft || 0,
|
||||
behavior: 'smooth',
|
||||
};
|
||||
setLocalStorageParameter('scrollTo', scrollTo);
|
||||
}
|
||||
linkClickHandler(event);
|
||||
};
|
||||
link.element.addEventListener('click', link.clickHandler);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setupPageSizeSelect() {
|
||||
// pagesize form
|
||||
pagesizeForm = element.querySelector('#' + cssIdPrefix + asyncTableId + '-pagesize-form');
|
||||
|
||||
if (pagesizeForm) {
|
||||
var pagesizeSelect = pagesizeForm.querySelector('[name=' + asyncTableId + '-pagesize]');
|
||||
pagesizeSelect.addEventListener('change', changePagesizeHandler);
|
||||
}
|
||||
}
|
||||
|
||||
function setupTableFilter() {
|
||||
var tableFilterForm = element.querySelector(ASYNC_TABLE_FILTER_FORM_SELECTOR);
|
||||
|
||||
if (tableFilterForm) {
|
||||
gatherTableFilterInputs(tableFilterForm);
|
||||
addTableFilterEventListeners(tableFilterForm);
|
||||
}
|
||||
}
|
||||
|
||||
function gatherTableFilterInputs(tableFilterForm) {
|
||||
Array.from(tableFilterForm.querySelectorAll('input[type="search"]')).forEach(function(input) {
|
||||
tableFilterInputs.search.push(input);
|
||||
});
|
||||
|
||||
Array.from(tableFilterForm.querySelectorAll('input[type="text"]')).forEach(function(input) {
|
||||
tableFilterInputs.input.push(input);
|
||||
});
|
||||
|
||||
Array.from(tableFilterForm.querySelectorAll('input:not([type="text"]):not([type="search"])')).forEach(function(input) {
|
||||
tableFilterInputs.change.push(input);
|
||||
});
|
||||
|
||||
Array.from(tableFilterForm.querySelectorAll('select')).forEach(function(input) {
|
||||
tableFilterInputs.select.push(input);
|
||||
});
|
||||
}
|
||||
|
||||
function addTableFilterEventListeners(tableFilterForm) {
|
||||
tableFilterInputs.search.forEach(function(input) {
|
||||
var debouncedInput = debounce(function() {
|
||||
if (input.value.length === 0 || input.value.length > 2) {
|
||||
updateFromTableFilter(tableFilterForm);
|
||||
}
|
||||
}, INPUT_DEBOUNCE);
|
||||
input.addEventListener('input', debouncedInput);
|
||||
});
|
||||
|
||||
tableFilterInputs.input.forEach(function(input) {
|
||||
var debouncedInput = debounce(function() {
|
||||
if (input.value.length === 0 || input.value.length > 2) {
|
||||
updateFromTableFilter(tableFilterForm);
|
||||
}
|
||||
}, INPUT_DEBOUNCE);
|
||||
input.addEventListener('input', debouncedInput);
|
||||
});
|
||||
|
||||
tableFilterInputs.change.forEach(function(input) {
|
||||
input.addEventListener('change', function() {
|
||||
updateFromTableFilter(tableFilterForm);
|
||||
});
|
||||
});
|
||||
|
||||
tableFilterInputs.select.forEach(function(input) {
|
||||
input.addEventListener('change', function() {
|
||||
updateFromTableFilter(tableFilterForm);
|
||||
});
|
||||
});
|
||||
|
||||
tableFilterForm.addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
updateFromTableFilter(tableFilterForm);
|
||||
});
|
||||
}
|
||||
|
||||
function updateFromTableFilter(tableFilterForm) {
|
||||
var url = serializeTableFilterToURL();
|
||||
var callback = null;
|
||||
|
||||
var focusedInput = tableFilterForm.querySelector(':focus, :active');
|
||||
// focus previously focused input
|
||||
if (focusedInput && focusedInput.selectionStart !== null) {
|
||||
var selectionStart = focusedInput.selectionStart;
|
||||
// remove the following part of the id to get rid of the random
|
||||
// (yet somewhat structured) prefix we got from nudging.
|
||||
var prefix = findCssIdPrefix(focusedInput.id);
|
||||
var focusId = focusedInput.id.replace(prefix, '');
|
||||
callback = function(wrapper) {
|
||||
var idPrefix = getLocalStorageParameter('cssIdPrefix');
|
||||
var toBeFocused = wrapper.querySelector('#' + idPrefix + focusId);
|
||||
if (toBeFocused) {
|
||||
toBeFocused.focus();
|
||||
toBeFocused.selectionStart = selectionStart;
|
||||
}
|
||||
};
|
||||
}
|
||||
updateTableFrom(url, callback);
|
||||
}
|
||||
|
||||
function serializeTableFilterToURL() {
|
||||
var url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href);
|
||||
|
||||
var formIdElement = element.querySelector(ASYNC_TABLE_FILTER_FORM_ID_SELECTOR);
|
||||
if (!formIdElement) {
|
||||
// cannot serialize the filter form without an identifier
|
||||
return;
|
||||
}
|
||||
|
||||
url.searchParams.set('form-identifier', formIdElement.value);
|
||||
url.searchParams.set('_hasdata', 'true');
|
||||
url.searchParams.set(asyncTableId + '-page', '0');
|
||||
|
||||
tableFilterInputs.search.forEach(function(input) {
|
||||
url.searchParams.set(input.name, input.value);
|
||||
});
|
||||
|
||||
tableFilterInputs.input.forEach(function(input) {
|
||||
url.searchParams.set(input.name, input.value);
|
||||
});
|
||||
|
||||
tableFilterInputs.change.forEach(function(input) {
|
||||
if (input.checked) {
|
||||
url.searchParams.set(input.name, input.value);
|
||||
}
|
||||
});
|
||||
|
||||
tableFilterInputs.select.forEach(function(select) {
|
||||
var options = Array.from(select.querySelectorAll('option'));
|
||||
var selected = options.find(function(option) { return option.selected; });
|
||||
if (selected) {
|
||||
url.searchParams.set(select.name, selected.value);
|
||||
}
|
||||
});
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
function processLocalStorage() {
|
||||
var scrollTo = getLocalStorageParameter('scrollTo');
|
||||
if (scrollTo && scrollTable) {
|
||||
window.scrollTo(scrollTo);
|
||||
}
|
||||
setLocalStorageParameter('scrollTo', null);
|
||||
|
||||
var horizPos = getLocalStorageParameter('horizPos');
|
||||
if (horizPos && scrollTable) {
|
||||
scrollTable.scrollLeft = horizPos;
|
||||
}
|
||||
setLocalStorageParameter('horizPos', null);
|
||||
}
|
||||
|
||||
function removeListeners() {
|
||||
ths.forEach(function(th) {
|
||||
th.element.removeEventListener('click', th.clickHandler);
|
||||
});
|
||||
|
||||
pageLinks.forEach(function(link) {
|
||||
link.element.removeEventListener('click', link.clickHandler);
|
||||
});
|
||||
|
||||
if (pagesizeForm) {
|
||||
var pagesizeSelect = pagesizeForm.querySelector('[name=' + asyncTableId + '-pagesize]');
|
||||
pagesizeSelect.removeEventListener('change', changePagesizeHandler);
|
||||
}
|
||||
}
|
||||
|
||||
function linkClickHandler(event) {
|
||||
event.preventDefault();
|
||||
var url = getClickDestination(event.target);
|
||||
if (!url.match(/^http/)) {
|
||||
url = window.location.origin + window.location.pathname + url;
|
||||
}
|
||||
updateTableFrom(url);
|
||||
}
|
||||
|
||||
function getClickDestination(el) {
|
||||
if (!el.matches('a') && !el.querySelector('a')) {
|
||||
return '';
|
||||
}
|
||||
return el.getAttribute('href') || el.querySelector('a').getAttribute('href');
|
||||
}
|
||||
|
||||
function changePagesizeHandler(event) {
|
||||
var pagesizeParamKey = asyncTableId + '-pagesize';
|
||||
var pageParamKey = asyncTableId + '-page';
|
||||
var url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href);
|
||||
url.searchParams.set(pagesizeParamKey, event.target.value);
|
||||
url.searchParams.set(pageParamKey, 0);
|
||||
updateTableFrom(url.href);
|
||||
}
|
||||
|
||||
// fetches new sorted element from url with params and replaces contents of current element
|
||||
function updateTableFrom(url, callback) {
|
||||
element.classList.add(ASYNC_TABLE_LOADING_CLASS);
|
||||
|
||||
var headers = {
|
||||
'Accept': HttpClient.ACCEPT.TEXT_HTML,
|
||||
[asyncTableHeader]: asyncTableId,
|
||||
};
|
||||
|
||||
app.httpClient.get({
|
||||
url: url,
|
||||
headers: headers,
|
||||
}).then(function(response) {
|
||||
return app.htmlHelpers.parseResponse(response);
|
||||
}).then(function(response) {
|
||||
setLocalStorageParameter('currentTableUrl', url.href);
|
||||
// reset table
|
||||
removeListeners();
|
||||
element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS);
|
||||
// update table with new
|
||||
updateWrapperContents(response);
|
||||
|
||||
app.utilRegistry.setupAll(element);
|
||||
|
||||
if (callback && typeof callback === 'function') {
|
||||
setLocalStorageParameter('cssIdPrefix', response.idPrefix);
|
||||
callback(element);
|
||||
setLocalStorageParameter('cssIdPrefix', '');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
console.error(err);
|
||||
}).finally(function() {
|
||||
element.classList.remove(ASYNC_TABLE_LOADING_CLASS);
|
||||
});
|
||||
}
|
||||
|
||||
function updateWrapperContents(response) {
|
||||
var newPage = document.createElement('div');
|
||||
newPage.appendChild(response.element);
|
||||
var newWrapperContents = newPage.querySelector('#' + response.idPrefix + element.id);
|
||||
element.innerHTML = newWrapperContents.innerHTML;
|
||||
}
|
||||
|
||||
return init();
|
||||
};
|
||||
|
||||
|
||||
// returns any random nudged prefix found in the given id
|
||||
function findCssIdPrefix(id) {
|
||||
var matcher = /r\d*?__/;
|
||||
var maybePrefix = id.match(matcher);
|
||||
if (maybePrefix && maybePrefix[0]) {
|
||||
return maybePrefix[0];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function setLocalStorageParameter(key, value) {
|
||||
var currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {};
|
||||
if (value !== null) {
|
||||
currentLSState[key] = value;
|
||||
} else {
|
||||
delete currentLSState[key];
|
||||
}
|
||||
window.localStorage.setItem(ASYNC_TABLE_LOCAL_STORAGE_KEY, JSON.stringify(currentLSState));
|
||||
}
|
||||
|
||||
function getLocalStorageParameter(key) {
|
||||
var currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {};
|
||||
return currentLSState[key];
|
||||
}
|
||||
|
||||
// debounce function, taken from Underscore.js
|
||||
function debounce(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this, args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(context, args);
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(context, args);
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
name: ASYNC_TABLE_UTIL_NAME,
|
||||
selector: ASYNC_TABLE_UTIL_SELECTOR,
|
||||
setup: asyncTableUtil,
|
||||
};
|
||||
5
frontend/src/utils/async-table/async-table.scss
Normal file
5
frontend/src/utils/async-table/async-table.scss
Normal file
@ -0,0 +1,5 @@
|
||||
.async-table--loading {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
transition: opacity 400ms ease-out;
|
||||
}
|
||||
141
frontend/src/utils/check-all/check-all.js
Normal file
141
frontend/src/utils/check-all/check-all.js
Normal file
@ -0,0 +1,141 @@
|
||||
/**
|
||||
*
|
||||
* Check All Checkbox Utility
|
||||
* adds a Check All Checkbox above columns with only checkboxes
|
||||
*
|
||||
* Attribute: [none]
|
||||
* (will be set up automatically on tables)
|
||||
*
|
||||
* Example usage:
|
||||
* (table with one column thats only checkboxes)
|
||||
*/
|
||||
|
||||
var CHECK_ALL_UTIL_NAME = 'checkAll';
|
||||
var CHECK_ALL_UTIL_SELECTOR = 'table';
|
||||
|
||||
var CHECKBOX_SELECTOR = '[type="checkbox"]';
|
||||
|
||||
var CHECK_ALL_INITIALIZED_CLASS = 'check-all--initialized';
|
||||
|
||||
var checkAllUtil = function(element, app) {
|
||||
var columns = [];
|
||||
var checkboxColumn = [];
|
||||
var checkAllCheckbox = null;
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Check All utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(CHECK_ALL_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
gatherColumns();
|
||||
setupCheckAllCheckbox();
|
||||
|
||||
// mark initialized
|
||||
element.classList.add(CHECK_ALL_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: CHECK_ALL_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
function getCheckboxId() {
|
||||
return 'check-all-checkbox-' + Math.floor(Math.random() * 100000);
|
||||
}
|
||||
|
||||
function gatherColumns() {
|
||||
var rows = Array.from(element.querySelectorAll('tr'));
|
||||
var cols = [];
|
||||
rows.forEach(function(tr) {
|
||||
var cells = Array.from(tr.querySelectorAll('td'));
|
||||
cells.forEach(function(cell, cellIndex) {
|
||||
if (!cols[cellIndex]) {
|
||||
cols[cellIndex] = [];
|
||||
}
|
||||
cols[cellIndex].push(cell);
|
||||
});
|
||||
});
|
||||
columns = cols;
|
||||
}
|
||||
|
||||
function findCheckboxColumn(columns) {
|
||||
var checkboxColumnId = null;
|
||||
columns.forEach(function(col, i) {
|
||||
if (isCheckboxColumn(col)) {
|
||||
checkboxColumnId = i;
|
||||
}
|
||||
});
|
||||
return checkboxColumnId;
|
||||
}
|
||||
|
||||
function isCheckboxColumn(col) {
|
||||
var onlyCheckboxes = true;
|
||||
col.forEach(function(cell) {
|
||||
if (onlyCheckboxes && !cell.querySelector(CHECKBOX_SELECTOR)) {
|
||||
onlyCheckboxes = false;
|
||||
}
|
||||
});
|
||||
return onlyCheckboxes;
|
||||
}
|
||||
|
||||
function setupCheckAllCheckbox() {
|
||||
var checkboxColumnId = findCheckboxColumn(columns);
|
||||
if (checkboxColumnId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkboxColumn = columns[checkboxColumnId];
|
||||
var firstRow = element.querySelector('tr');
|
||||
var th = Array.from(firstRow.querySelectorAll('th, td'))[checkboxColumnId];
|
||||
checkAllCheckbox = document.createElement('input');
|
||||
checkAllCheckbox.setAttribute('type', 'checkbox');
|
||||
checkAllCheckbox.setAttribute('id', getCheckboxId());
|
||||
th.insertBefore(checkAllCheckbox, th.firstChild);
|
||||
|
||||
// manually set up new checkbox
|
||||
app.utilRegistry.setup(app.utilRegistry.find('checkbox'), th);
|
||||
|
||||
checkAllCheckbox.addEventListener('input', onCheckAllCheckboxInput);
|
||||
setupCheckboxListeners();
|
||||
}
|
||||
|
||||
function onCheckAllCheckboxInput() {
|
||||
toggleAll(checkAllCheckbox.checked);
|
||||
}
|
||||
|
||||
function setupCheckboxListeners() {
|
||||
checkboxColumn
|
||||
.map(function(cell) {
|
||||
return cell.querySelector(CHECKBOX_SELECTOR);
|
||||
})
|
||||
.forEach(function(checkbox) {
|
||||
checkbox.addEventListener('input', updateCheckAllCheckboxState);
|
||||
});
|
||||
}
|
||||
|
||||
function updateCheckAllCheckboxState() {
|
||||
var allChecked = checkboxColumn.every(function(cell) {
|
||||
return cell.querySelector(CHECKBOX_SELECTOR).checked;
|
||||
});
|
||||
checkAllCheckbox.checked = allChecked;
|
||||
}
|
||||
|
||||
function toggleAll(checked) {
|
||||
checkboxColumn.forEach(function(cell) {
|
||||
cell.querySelector(CHECKBOX_SELECTOR).checked = checked;
|
||||
});
|
||||
}
|
||||
|
||||
return init();
|
||||
};
|
||||
|
||||
export default {
|
||||
name: CHECK_ALL_UTIL_NAME,
|
||||
selector: CHECK_ALL_UTIL_SELECTOR,
|
||||
setup: checkAllUtil,
|
||||
};
|
||||
599
frontend/src/utils/form/form.js
Normal file
599
frontend/src/utils/form/form.js
Normal file
@ -0,0 +1,599 @@
|
||||
import './form.scss';
|
||||
|
||||
/**
|
||||
*
|
||||
* Reactive Submit Button Utility
|
||||
* disables a forms LAST sumit button as long as the required inputs are invalid
|
||||
* (only checks if the value of the inputs are not empty)
|
||||
*
|
||||
* Attribute: [none]
|
||||
* (automatically setup on all form tags)
|
||||
*
|
||||
* Params:
|
||||
* data-formnorequired: string
|
||||
* If present the submit button will never get disabled
|
||||
*
|
||||
* Example usage:
|
||||
* <form uw-reactive-submit-button>
|
||||
* <input type="text" required>
|
||||
* <button type="submit">
|
||||
* </form>
|
||||
*/
|
||||
|
||||
var REACTIVE_SUBMIT_BUTTON_UTIL_NAME = 'reactiveSubmitButton';
|
||||
// var REACTIVE_SUBMIT_BUTTON_UTIL_SELECTOR = 'form';
|
||||
|
||||
var REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS = 'reactive-submit-button--initialized';
|
||||
|
||||
/* eslint-disable-next-line */
|
||||
var reactiveSubmitButtonUtil = function(element) {
|
||||
var requiredInputs;
|
||||
var submitButton;
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Reactive Submit Button utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// abort if form has param data-formnorequired
|
||||
if (element.dataset.formnorequired !== undefined) {
|
||||
throw new Error('Form has formnorequired data attribute. Will skip setup of reactive submit button.');
|
||||
}
|
||||
|
||||
requiredInputs = Array.from(element.querySelectorAll('[required]'));
|
||||
if (!requiredInputs) {
|
||||
// abort if form has no required inputs
|
||||
throw new Error('Submit button has formnorequired data attribute. Will skip setup of reactive submit button.');
|
||||
}
|
||||
|
||||
var submitButtons = Array.from(element.querySelectorAll('[type="submit"]'));
|
||||
if (!submitButtons || !submitButtons.length) {
|
||||
throw new Error('Reactive Submit Button utility couldn\'t find any submit buttons!');
|
||||
}
|
||||
submitButton = submitButtons.reverse()[0];
|
||||
// abort if form has param data-formnorequired
|
||||
if (submitButton.dataset.formnorequired !== undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setupInputs();
|
||||
updateButtonState();
|
||||
|
||||
element.classList.add(REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: REACTIVE_SUBMIT_BUTTON_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
function setupInputs() {
|
||||
requiredInputs.forEach(function(el) {
|
||||
var checkbox = el.getAttribute('type') === 'checkbox';
|
||||
var eventType = checkbox ? 'change' : 'input';
|
||||
el.addEventListener(eventType, function() {
|
||||
updateButtonState();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateButtonState() {
|
||||
if (inputsValid()) {
|
||||
submitButton.removeAttribute('disabled');
|
||||
} else {
|
||||
submitButton.setAttribute('disabled', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
function inputsValid() {
|
||||
var done = true;
|
||||
requiredInputs.forEach(function(inp) {
|
||||
var len = inp.value.trim().length;
|
||||
if (done && len === 0) {
|
||||
done = false;
|
||||
}
|
||||
});
|
||||
return done;
|
||||
}
|
||||
|
||||
return init();
|
||||
};
|
||||
|
||||
// skipping reactiveButtonUtil (for now)
|
||||
// the button did not properly re-enable after filling out a form for some safari users.
|
||||
// if maybe in the future there is going to be a proper way of (asynchronously) and
|
||||
// meaningfully validating forms this can be re-activated by commenting in the next few lines
|
||||
// eport const reactiveSubmitButton = {
|
||||
// name: REACTIVE_SUBMIT_BUTTON_UTIL_NAME,
|
||||
// selector: REACTIVE_SUBMIT_BUTTON_UTIL_SELECTOR,
|
||||
// setup: reactiveSubmitButtonUtil,
|
||||
// };
|
||||
|
||||
/**
|
||||
*
|
||||
* Interactive Fieldset Utility
|
||||
* shows/hides inputs based on value of particular input
|
||||
*
|
||||
* Attribute: uw-interactive-fieldset
|
||||
*
|
||||
* Params:
|
||||
* data-conditional-input: string
|
||||
* Selector for the input that this fieldset watches for changes
|
||||
* data-conditional-value: string
|
||||
* The value the conditional input needs to be set to for this fieldset to be shown
|
||||
* Can be omitted if conditionalInput is a checkbox
|
||||
*
|
||||
* Example usage:
|
||||
* ## example with text input
|
||||
* <input id="input-0" type="text">
|
||||
* <fieldset uw-interactive-fieldset data-conditional-input="#input-0" data-conditional-value="yes">...</fieldset>
|
||||
* <fieldset uw-interactive-fieldset data-conditional-input="#input-0" data-conditional-value="no">...</fieldset>
|
||||
* ## example with <select>
|
||||
* <select id="select-0">
|
||||
* <option value="0">Zero
|
||||
* <option value="1">One
|
||||
* <fieldset uw-interactive-fieldset data-conditional-input="#select-0" data-conditional-value="0">...</fieldset>
|
||||
* <fieldset uw-interactive-fieldset data-conditional-input="#select-0" data-conditional-value="1">...</fieldset>
|
||||
* ## example with checkbox
|
||||
* <input id="checkbox-0" type="checkbox">
|
||||
* <input id="checkbox-1" type="checkbox">
|
||||
* <fieldset uw-interactive-fieldset data-conditional-input="#checkbox-0">...</fieldset>
|
||||
* <fieldset uw-interactive-fieldset data-conditional-input="#checkbox-1">...</fieldset>
|
||||
*/
|
||||
|
||||
var INTERACTIVE_FIELDSET_UTIL_NAME = 'interactiveFieldset';
|
||||
var INTERACTIVE_FIELDSET_UTIL_SELECTOR = '[uw-interactive-fieldset]';
|
||||
var INTERACTIVE_FIELDSET_UTIL_TARGET_SELECTOR = '.interactive-fieldset__target';
|
||||
|
||||
var INTERACTIVE_FIELDSET_INITIALIZED_CLASS = 'interactive-fieldset--initialized';
|
||||
var INTERACTIVE_FIELDSET_CHILD_SELECTOR = 'input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled])';
|
||||
|
||||
var interactiveFieldsetUtil = function(element) {
|
||||
var conditionalInput;
|
||||
var conditionalValue;
|
||||
var target;
|
||||
var childInputs;
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Interactive Fieldset utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(INTERACTIVE_FIELDSET_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// param conditionalInput
|
||||
if (!element.dataset.conditionalInput) {
|
||||
throw new Error('Interactive Fieldset needs a selector for a conditional input!');
|
||||
}
|
||||
conditionalInput = document.querySelector('#' + element.dataset.conditionalInput);
|
||||
if (!conditionalInput) {
|
||||
// abort if form has no required inputs
|
||||
throw new Error('Couldn\'t find the conditional input. Aborting setup for interactive fieldset.');
|
||||
}
|
||||
|
||||
// param conditionalValue
|
||||
if (!element.dataset.conditionalValue && !isCheckbox()) {
|
||||
throw new Error('Interactive Fieldset needs a conditional value!');
|
||||
}
|
||||
conditionalValue = element.dataset.conditionalValue;
|
||||
|
||||
target = element.closest(INTERACTIVE_FIELDSET_UTIL_TARGET_SELECTOR);
|
||||
if (!target || element.matches(INTERACTIVE_FIELDSET_UTIL_TARGET_SELECTOR)) {
|
||||
target = element;
|
||||
}
|
||||
|
||||
childInputs = Array.from(element.querySelectorAll(INTERACTIVE_FIELDSET_CHILD_SELECTOR));
|
||||
|
||||
// add event listener
|
||||
var observer = new MutationObserver(() => updateVisibility());
|
||||
observer.observe(conditionalInput, { attributes: true, attributeFilter: ['disabled'] });
|
||||
conditionalInput.addEventListener('input', updateVisibility);
|
||||
|
||||
// initial visibility update
|
||||
updateVisibility();
|
||||
|
||||
// mark as initialized
|
||||
element.classList.add(INTERACTIVE_FIELDSET_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: INTERACTIVE_FIELDSET_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
function updateVisibility() {
|
||||
var active = matchesConditionalValue() && !conditionalInput.disabled;
|
||||
|
||||
target.classList.toggle('hidden', !active);
|
||||
|
||||
childInputs.forEach(function(el) {
|
||||
el.disabled = !active;
|
||||
if (el._flatpickr) {
|
||||
el._flatpickr.altInput.disabled = !active;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function matchesConditionalValue() {
|
||||
if (isCheckbox()) {
|
||||
return conditionalInput.checked === true;
|
||||
}
|
||||
|
||||
return conditionalInput.value === conditionalValue;
|
||||
}
|
||||
|
||||
function isCheckbox() {
|
||||
return conditionalInput.getAttribute('type') === 'checkbox';
|
||||
}
|
||||
|
||||
return init();
|
||||
};
|
||||
|
||||
export const interactiveFieldset = {
|
||||
name: INTERACTIVE_FIELDSET_UTIL_NAME,
|
||||
selector: INTERACTIVE_FIELDSET_UTIL_SELECTOR,
|
||||
setup: interactiveFieldsetUtil,
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Navigate Away Prompt Utility
|
||||
* This utility asks the user if (s)he really wants to navigate away
|
||||
* from a page containing a form if (s)he already touched an input.
|
||||
* Form-Submits will not trigger the prompt.
|
||||
* Utility will ignore forms that contain auto submit elements (buttons, inputs).
|
||||
*
|
||||
* Attribute: [none]
|
||||
* (automatically setup on all form tags that dont automatically submit, see AutoSubmitButtonUtil)
|
||||
*
|
||||
* Example usage:
|
||||
* (any page with a form)
|
||||
*/
|
||||
|
||||
var NAVIGATE_AWAY_PROMPT_UTIL_NAME = 'navigateAwayPrompt';
|
||||
var NAVIGATE_AWAY_PROMPT_UTIL_SELECTOR = 'form';
|
||||
|
||||
var NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS = 'navigate-away-prompt--initialized';
|
||||
|
||||
var navigateAwayPromptUtil = function(element) {
|
||||
var touched = false;
|
||||
var unloadDueToSubmit = false;
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Navigate Away Prompt utility needs to be passed an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ignore forms that get submitted automatically
|
||||
if (element.querySelector(AUTO_SUBMIT_BUTTON_UTIL_SELECTOR) || element.querySelector(AUTO_SUBMIT_INPUT_UTIL_SELECTOR)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', beforeUnloadHandler);
|
||||
|
||||
element.addEventListener('submit', function() {
|
||||
unloadDueToSubmit = true;
|
||||
});
|
||||
element.addEventListener('change', function() {
|
||||
touched = true;
|
||||
unloadDueToSubmit = false;
|
||||
});
|
||||
|
||||
// mark initialized
|
||||
element.classList.add(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: NAVIGATE_AWAY_PROMPT_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {
|
||||
window.removeEventListener('beforeunload', beforeUnloadHandler);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function beforeUnloadHandler(event) {
|
||||
// allow the event to happen if the form was not touched by the
|
||||
// user or the unload event was initiated by a form submit
|
||||
if (!touched || unloadDueToSubmit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// cancel the unload event. This is the standard to force the prompt to appear.
|
||||
event.preventDefault();
|
||||
// for all non standard compliant browsers we return a truthy value to activate the prompt.
|
||||
return true;
|
||||
}
|
||||
|
||||
return init();
|
||||
};
|
||||
|
||||
export const navigateAwayPrompt = {
|
||||
name: NAVIGATE_AWAY_PROMPT_UTIL_NAME,
|
||||
selector: NAVIGATE_AWAY_PROMPT_UTIL_SELECTOR,
|
||||
setup: navigateAwayPromptUtil,
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Auto Submit Button Utility
|
||||
* Hides submit buttons in forms that are submitted programmatically
|
||||
* We hide the button using JavaScript so no-js users will still be able to submit the form
|
||||
*
|
||||
* Attribute: uw-auto-submit-button
|
||||
*
|
||||
* Example usage:
|
||||
* <button type="submit" uw-auto-submit-button>Submit
|
||||
*/
|
||||
|
||||
var AUTO_SUBMIT_BUTTON_UTIL_NAME = 'autoSubmitButton';
|
||||
var AUTO_SUBMIT_BUTTON_UTIL_SELECTOR = '[uw-auto-submit-button]';
|
||||
|
||||
var AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS = 'auto-submit-button--initialized';
|
||||
var AUTO_SUBMIT_BUTTON_HIDDEN_CLASS = 'hidden';
|
||||
|
||||
var autoSubmitButtonUtil = function(element) {
|
||||
if (!element) {
|
||||
throw new Error('Auto Submit Button utility needs to be passed an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// hide and mark initialized
|
||||
element.classList.add(AUTO_SUBMIT_BUTTON_HIDDEN_CLASS, AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: AUTO_SUBMIT_BUTTON_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
};
|
||||
|
||||
export const autoSubmitButton = {
|
||||
name: AUTO_SUBMIT_BUTTON_UTIL_NAME,
|
||||
selector: AUTO_SUBMIT_BUTTON_UTIL_SELECTOR,
|
||||
setup: autoSubmitButtonUtil,
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Auto Submit Input Utility
|
||||
* Programmatically submits forms when a certain input changes value
|
||||
*
|
||||
* Attribute: uw-auto-submit-input
|
||||
*
|
||||
* Example usage:
|
||||
* <input type="text" uw-auto-submit-input />
|
||||
*/
|
||||
|
||||
var AUTO_SUBMIT_INPUT_UTIL_NAME = 'autoSubmitInput';
|
||||
var AUTO_SUBMIT_INPUT_UTIL_SELECTOR = '[uw-auto-submit-input]';
|
||||
|
||||
var AUTO_SUBMIT_INPUT_INITIALIZED_CLASS = 'auto-submit-input--initialized';
|
||||
|
||||
var autoSubmitInputUtil = function(element) {
|
||||
var form;
|
||||
var debouncedHandler;
|
||||
|
||||
function autoSubmit() {
|
||||
form.submit();
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Auto Submit Input utility needs to be passed an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
form = element.form;
|
||||
if (!form) {
|
||||
throw new Error('Could not determine associated form for auto submit input');
|
||||
}
|
||||
|
||||
debouncedHandler = debounce(autoSubmit, 500);
|
||||
|
||||
element.addEventListener('input', debouncedHandler);
|
||||
|
||||
element.classList.add(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: AUTO_SUBMIT_INPUT_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {
|
||||
element.removeEventListener('input', debouncedHandler);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return init();
|
||||
};
|
||||
|
||||
export const autoSubmitInput = {
|
||||
name: AUTO_SUBMIT_INPUT_UTIL_NAME,
|
||||
selector: AUTO_SUBMIT_INPUT_UTIL_SELECTOR,
|
||||
setup: autoSubmitInputUtil,
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Form Error Remover Utility
|
||||
* Removes errors from inputs when they are focused
|
||||
*
|
||||
* Attribute: [none]
|
||||
* (automatically setup on all form tags)
|
||||
*
|
||||
* Example usage:
|
||||
* (any regular form that can show input errors)
|
||||
*/
|
||||
|
||||
var FORM_ERROR_REMOVER_UTIL_NAME = 'formErrorRemover';
|
||||
var FORM_ERROR_REMOVER_UTIL_SELECTOR = 'form';
|
||||
|
||||
var FORM_ERROR_REMOVER_INITIALIZED_CLASS = 'form-error-remover--initialized';
|
||||
var FORM_ERROR_REMOVER_INPUTS_SELECTOR = 'input:not([type="hidden"]), textarea, select';
|
||||
|
||||
var FORM_GROUP_SELECTOR = '.form-group';
|
||||
var FORM_GROUP_WITH_ERRORS_CLASS = 'form-group--has-error';
|
||||
|
||||
|
||||
var formErrorRemoverUtil = function(element) {
|
||||
var formGroups;
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Form Error Remover utility needs to be passed an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(FORM_ERROR_REMOVER_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// find form groups
|
||||
formGroups = Array.from(element.querySelectorAll(FORM_GROUP_SELECTOR));
|
||||
|
||||
formGroups.forEach(function(formGroup) {
|
||||
if (!formGroup.classList.contains(FORM_GROUP_WITH_ERRORS_CLASS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var inputElements = Array.from(formGroup.querySelectorAll(FORM_ERROR_REMOVER_INPUTS_SELECTOR));
|
||||
if (!inputElements) {
|
||||
return false;
|
||||
}
|
||||
|
||||
inputElements.forEach(function(inputElement) {
|
||||
inputElement.addEventListener('input', function() {
|
||||
formGroup.classList.remove(FORM_GROUP_WITH_ERRORS_CLASS);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// mark initialized
|
||||
element.classList.add(FORM_ERROR_REMOVER_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: FORM_ERROR_REMOVER_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
return init();
|
||||
};
|
||||
|
||||
export const formErrorRemover = {
|
||||
name: FORM_ERROR_REMOVER_UTIL_NAME,
|
||||
selector: FORM_ERROR_REMOVER_UTIL_SELECTOR,
|
||||
setup: formErrorRemoverUtil,
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Datepicker Utility
|
||||
* Provides UI for entering dates and times
|
||||
*
|
||||
* Attribute: [none]
|
||||
* (automatically setup on all relevant input tags)
|
||||
*
|
||||
* Example usage:
|
||||
* (any form that uses inputs of type date, time, or datetime-local)
|
||||
*/
|
||||
|
||||
var DATEPICKER_UTIL_NAME = 'datepicker';
|
||||
var DATEPICKER_UTIL_SELECTOR = 'input[type="date"], input[type="time"], input[type="datetime-local"]';
|
||||
|
||||
var DATEPICKER_INITIALIZED_CLASS = 'datepicker--initialized';
|
||||
|
||||
var DATEPICKER_CONFIG = {
|
||||
"datetime-local": {
|
||||
enableTime: true,
|
||||
altInput: true,
|
||||
altFormat: "j. F Y, H:i", // maybe interpolate these formats for locale
|
||||
dateFormat: "Y-m-dTH:i",
|
||||
time_24hr: true,
|
||||
},
|
||||
"date": {
|
||||
altFormat: "j. F Y",
|
||||
dateFormat: "Y-m-d",
|
||||
altInput: true,
|
||||
},
|
||||
"time": {
|
||||
enableTime: true,
|
||||
noCalendar: true,
|
||||
altFormat: "H:i",
|
||||
dateFormat: "H:i",
|
||||
altInput: true,
|
||||
time_24hr: true,
|
||||
},
|
||||
};
|
||||
|
||||
var datepickerUtil = function(element) {
|
||||
var flatpickrInstance;
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Datepicker utility needs to be passed an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(DATEPICKER_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var flatpickrConfig = DATEPICKER_CONFIG[element.getAttribute("type")];
|
||||
|
||||
if (!flatpickrConfig) {
|
||||
throw new Error('Datepicker utility called on unsupported element!');
|
||||
}
|
||||
|
||||
flatpickrInstance = flatpickr(element, flatpickrConfig);
|
||||
|
||||
// mark initialized
|
||||
element.classList.add(DATEPICKER_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: DATEPICKER_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() { flatpickrInstance.destroy(); },
|
||||
};
|
||||
}
|
||||
|
||||
return init();
|
||||
};
|
||||
|
||||
export const datepicker = {
|
||||
name: DATEPICKER_UTIL_NAME,
|
||||
selector: DATEPICKER_UTIL_SELECTOR,
|
||||
setup: datepickerUtil,
|
||||
};
|
||||
|
||||
// debounce function, taken from Underscore.js
|
||||
function debounce(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this, args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(context, args);
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(context, args);
|
||||
};
|
||||
}
|
||||
45
frontend/src/utils/form/form.scss
Normal file
45
frontend/src/utils/form/form.scss
Normal file
@ -0,0 +1,45 @@
|
||||
fieldset {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
legend {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.form-group__input {
|
||||
grid-column: 2;
|
||||
}
|
||||
}
|
||||
|
||||
[uw-auto-submit-button][type="submit"] {
|
||||
animation: fade-in 500ms ease-in-out backwards;
|
||||
animation-delay: 500ms;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden !important;
|
||||
height: 0 !important;
|
||||
width: 0 !important;
|
||||
opacity: 0 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.select--pagesize {
|
||||
width: 5em;
|
||||
min-width: 75px;
|
||||
}
|
||||
|
||||
.label-pagesize {
|
||||
margin-right: 13px;
|
||||
}
|
||||
82
frontend/src/utils/inputs/checkbox.scss
Normal file
82
frontend/src/utils/inputs/checkbox.scss
Normal file
@ -0,0 +1,82 @@
|
||||
|
||||
/* CUSTOM CHECKBOXES */
|
||||
/* Completely replaces legacy checkbox */
|
||||
.checkbox {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
[type="checkbox"] {
|
||||
position: fixed;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background-color: #f3f3f3;
|
||||
box-shadow: inset 0 1px 2px 1px rgba(50, 50, 50, 0.05);
|
||||
border: 2px solid var(--color-primary);
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
label::before,
|
||||
label::after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 12px;
|
||||
left: 8px;
|
||||
height: 2px;
|
||||
width: 8px;
|
||||
background-color: var(--color-font);
|
||||
}
|
||||
|
||||
:checked + label {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
[type="checkbox"]:focus + label {
|
||||
border-color: #3273dc;
|
||||
box-shadow: 0 0 0 0.125em rgba(50,115,220,.25);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
:checked + label::before,
|
||||
:checked + label::after {
|
||||
content: '';
|
||||
}
|
||||
|
||||
:checked + label::before {
|
||||
background-color: white;
|
||||
transform: rotate(45deg);
|
||||
left: 2px;
|
||||
top: 11px;
|
||||
}
|
||||
|
||||
:checked + label::after {
|
||||
background-color: white;
|
||||
transform: rotate(-45deg);
|
||||
top: 9px;
|
||||
width: 12px;
|
||||
left: 7px;
|
||||
}
|
||||
|
||||
[disabled] + label {
|
||||
pointer-events: none;
|
||||
border: none;
|
||||
opacity: 0.6;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* special treatment for checkboxes in table headers */
|
||||
th .checkbox {
|
||||
margin-right: 7px;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
194
frontend/src/utils/inputs/inputs.js
Normal file
194
frontend/src/utils/inputs/inputs.js
Normal file
@ -0,0 +1,194 @@
|
||||
import './checkbox.scss';
|
||||
import './radio.scss';
|
||||
import './inputs.scss';
|
||||
|
||||
/**
|
||||
*
|
||||
* FileInput Utility
|
||||
* wraps native file input
|
||||
*
|
||||
* Attribute: uw-file-input
|
||||
* (element must be an input of type='file')
|
||||
*
|
||||
* Example usage:
|
||||
* <input type='file' uw-file-input>
|
||||
*
|
||||
* Internationalization:
|
||||
* This utility expects the following translations to be available:
|
||||
* »filesSelected«: label of multi-input button after selection
|
||||
* example: "Dateien ausgewählt" (will be prepended by number of selected files)
|
||||
* »selectFile«: label of single-input button before selection
|
||||
* example: "Datei auswählen"
|
||||
* »selectFiles«: label of multi-input button before selection
|
||||
* example: "Datei(en) auswählen"
|
||||
*
|
||||
*/
|
||||
|
||||
var FILE_INPUT_UTIL_NAME = 'fileInput';
|
||||
var FILE_INPUT_UTIL_SELECTOR = 'input[type="file"][uw-file-input]';
|
||||
|
||||
var FILE_INPUT_CLASS = 'file-input';
|
||||
var FILE_INPUT_INITIALIZED_CLASS = 'file-input--initialized';
|
||||
var FILE_INPUT_LIST_CLASS = 'file-input__list';
|
||||
var FILE_INPUT_UNPACK_CHECKBOX_CLASS = 'file-input__unpack';
|
||||
var FILE_INPUT_LABEL_CLASS = 'file-input__label';
|
||||
|
||||
var fileInputUtil = function(element, app) {
|
||||
var isMultiFileInput = false;
|
||||
var fileList;
|
||||
var label;
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('FileInput utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(FILE_INPUT_INITIALIZED_CLASS)) {
|
||||
throw new Error('FileInput utility already initialized!');
|
||||
}
|
||||
|
||||
// check if is multi-file input
|
||||
isMultiFileInput = element.hasAttribute('multiple');
|
||||
if (isMultiFileInput) {
|
||||
fileList = createFileList();
|
||||
}
|
||||
|
||||
label = createFileLabel();
|
||||
updateLabel();
|
||||
|
||||
// add change listener
|
||||
element.addEventListener('change', function() {
|
||||
updateLabel();
|
||||
renderFileList();
|
||||
});
|
||||
|
||||
// add util class for styling and mark as initialized
|
||||
element.classList.add(FILE_INPUT_CLASS);
|
||||
element.classList.add(FILE_INPUT_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: FILE_INPUT_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
function renderFileList() {
|
||||
if (!fileList) {
|
||||
return;
|
||||
}
|
||||
|
||||
var files = element.files;
|
||||
fileList.innerHTML = '';
|
||||
Array.from(files).forEach(function(file) {
|
||||
var fileDisplayEl = document.createElement('li');
|
||||
fileDisplayEl.innerHTML = file.name;
|
||||
fileList.appendChild(fileDisplayEl);
|
||||
});
|
||||
}
|
||||
|
||||
function createFileList() {
|
||||
var list = document.createElement('ol');
|
||||
list.classList.add(FILE_INPUT_LIST_CLASS);
|
||||
var unpackEl = element.parentElement.querySelector('.' + FILE_INPUT_UNPACK_CHECKBOX_CLASS);
|
||||
if (unpackEl) {
|
||||
element.parentElement.insertBefore(list, unpackEl);
|
||||
} else {
|
||||
element.parentElement.appendChild(list);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
function createFileLabel() {
|
||||
var label = document.createElement('label');
|
||||
label.classList.add(FILE_INPUT_LABEL_CLASS);
|
||||
label.setAttribute('for', element.id);
|
||||
element.parentElement.insertBefore(label, element);
|
||||
return label;
|
||||
}
|
||||
|
||||
function updateLabel() {
|
||||
var files = element.files;
|
||||
if (files && files.length) {
|
||||
label.innerText = isMultiFileInput ? files.length + ' ' + app.i18n.get('filesSelected') : files[0].name;
|
||||
} else {
|
||||
label.innerText = isMultiFileInput ? app.i18n.get('selectFiles') : app.i18n.get('selectFile');
|
||||
}
|
||||
}
|
||||
|
||||
return init();
|
||||
};
|
||||
|
||||
export const fileInput = {
|
||||
name: FILE_INPUT_UTIL_NAME,
|
||||
selector: FILE_INPUT_UTIL_SELECTOR,
|
||||
setup: fileInputUtil,
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Checkbox Utility
|
||||
* wraps native checkbox
|
||||
*
|
||||
* Attribute: (none)
|
||||
* (element must be an input of type="checkbox")
|
||||
*
|
||||
* Example usage:
|
||||
* <input type="checkbox">
|
||||
*
|
||||
*/
|
||||
|
||||
var CHECKBOX_UTIL_NAME = 'checkbox';
|
||||
var CHECKBOX_UTIL_SELECTOR = 'input[type="checkbox"]';
|
||||
|
||||
var CHECKBOX_CLASS = 'checkbox';
|
||||
var CHECKBOX_INITIALIZED_CLASS = 'checkbox--initialized';
|
||||
|
||||
var checkboxUtil = function(element) {
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Checkbox utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(CHECKBOX_INITIALIZED_CLASS)) {
|
||||
// throw new Error('Checkbox utility already initialized!');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (element.parentElement.classList.contains(CHECKBOX_CLASS)) {
|
||||
// throw new Error('Checkbox element\'s wrapper already has class "' + CHECKBOX_CLASS + '"!');
|
||||
return false;
|
||||
}
|
||||
|
||||
var siblingEl = element.nextSibling;
|
||||
var parentEl = element.parentElement;
|
||||
|
||||
var wrapperEl = document.createElement('div');
|
||||
wrapperEl.classList.add(CHECKBOX_CLASS);
|
||||
|
||||
var labelEl = document.createElement('label');
|
||||
labelEl.setAttribute('for', element.id);
|
||||
|
||||
wrapperEl.appendChild(element);
|
||||
wrapperEl.appendChild(labelEl);
|
||||
|
||||
parentEl.insertBefore(wrapperEl, siblingEl);
|
||||
|
||||
element.classList.add(CHECKBOX_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: CHECKBOX_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
return init();
|
||||
};
|
||||
|
||||
export const checkbox = {
|
||||
name: CHECKBOX_UTIL_NAME,
|
||||
selector: CHECKBOX_UTIL_SELECTOR,
|
||||
setup: checkboxUtil,
|
||||
};
|
||||
239
frontend/src/utils/inputs/inputs.scss
Normal file
239
frontend/src/utils/inputs/inputs.scss
Normal file
@ -0,0 +1,239 @@
|
||||
/* GENERAL STYLES FOR FORMS */
|
||||
|
||||
/* FORM GROUPS */
|
||||
.form-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 3fr;
|
||||
grid-gap: 5px;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
padding: 4px 0;
|
||||
border-left: 2px solid transparent;
|
||||
|
||||
+ .form-group {
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
+ .form-section-title {
|
||||
margin-top: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-section-title {
|
||||
color: var(--color-fontsec);
|
||||
}
|
||||
|
||||
.form-group-label {
|
||||
font-weight: 600;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.form-group-label__hint {
|
||||
margin-top: 7px;
|
||||
color: var(--color-fontsec);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-group--required {
|
||||
.form-group-label__caption::after {
|
||||
content: ' *';
|
||||
color: var(--color-error);
|
||||
}
|
||||
}
|
||||
|
||||
.form-group--optional {
|
||||
.form-group-label__caption::after {
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
.form-group--submit .form-group__input {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-group--submit .form-group__input {
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group--has-error {
|
||||
background-color: rgba(255, 0, 0, 0.1);
|
||||
|
||||
input, textarea {
|
||||
border-color: var(--color-error) !important;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.form-error {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-group {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: baseline;
|
||||
margin-top: 17px;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* TEXT INPUTS */
|
||||
input[type="text"],
|
||||
input[type="search"],
|
||||
input[type="password"],
|
||||
input[type="url"],
|
||||
input[type="number"],
|
||||
input[type="email"],
|
||||
input[type*="date"],
|
||||
input[type*="time"],
|
||||
select {
|
||||
/* from bulma.css */
|
||||
color: #363636;
|
||||
border-color: #dbdbdb;
|
||||
background-color: #f3f3f3;
|
||||
box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05);
|
||||
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
align-items: center;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
font-family: var(--font-base);
|
||||
line-height: 1.5;
|
||||
padding: 4px 13px;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
input[type*="date"],
|
||||
input[type*="time"],
|
||||
.flatpickr-input[type="text"] {
|
||||
width: 50%;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
/* BUTTON STYLE SEE default-layout.lucius */
|
||||
|
||||
/* TEXTAREAS */
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 170px;
|
||||
max-width: 600px;
|
||||
line-height: 1.5;
|
||||
color: #363636;
|
||||
background-color: #f3f3f3;
|
||||
padding: 4px 13px;
|
||||
font-size: 1rem;
|
||||
font-family: var(--font-base);
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
border: 1px solid #dbdbdb;
|
||||
border-radius: 2px;
|
||||
box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* SHARED STATE RELATED STYLES */
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
&:focus {
|
||||
border-color: #3273dc;
|
||||
box-shadow: 0 0 0 0.125em rgba(50,115,220,.25);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
background-color: #f3f3f3;
|
||||
color: #7a7a7a;
|
||||
box-shadow: none;
|
||||
border-color: #dbdbdb;
|
||||
}
|
||||
|
||||
&[readonly] {
|
||||
background-color: #f5f5f5;
|
||||
border-color: #dbdbdb;
|
||||
}
|
||||
}
|
||||
|
||||
/* OPTIONS */
|
||||
|
||||
select {
|
||||
-webkit-appearance: menulist;
|
||||
}
|
||||
|
||||
select,
|
||||
option {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
padding: 4px 13px;
|
||||
border: 1px solid #dbdbdb;
|
||||
border-radius: 2px;
|
||||
outline: 0;
|
||||
color: #363636;
|
||||
min-width: 250px;
|
||||
width: auto;
|
||||
background-color: #f3f3f3;
|
||||
box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05);
|
||||
}
|
||||
|
||||
@media (max-width: 425px) {
|
||||
|
||||
select, option {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* FILE INPUT */
|
||||
.file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-input__label {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
padding: 10px 17px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.file-input__info {
|
||||
font-size: .9rem;
|
||||
font-style: italic;
|
||||
margin: 10px 0;
|
||||
color: var(--color-fontsec);
|
||||
}
|
||||
|
||||
.file-input__list {
|
||||
margin-left: 40px;
|
||||
margin-top: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* PREVIOUSLY UPLOADED FILES */
|
||||
|
||||
.file-uploads-label {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.file-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.checkbox {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
72
frontend/src/utils/inputs/radio.scss
Normal file
72
frontend/src/utils/inputs/radio.scss
Normal file
@ -0,0 +1,72 @@
|
||||
/* CUSTOM RADIO BOXES */
|
||||
/* Completely replaces native radiobox */
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.radio {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
[type="radio"] {
|
||||
position: fixed;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
height: 34px;
|
||||
min-width: 42px;
|
||||
line-height: 34px;
|
||||
text-align: center;
|
||||
padding: 0 13px;
|
||||
background-color: #f3f3f3;
|
||||
box-shadow: inset 2px 1px 2px 1px rgba(50, 50, 50, 0.05);
|
||||
color: var(--color-font);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:checked + label {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-lightwhite);
|
||||
box-shadow: inset -2px -1px 2px 1px rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
:focus + label {
|
||||
border-color: #3273dc;
|
||||
box-shadow: 0 0 0.125em 0 rgba(50,115,220,0.8);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
[disabled] + label {
|
||||
pointer-events: none;
|
||||
border: none;
|
||||
opacity: 0.6;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.radio:first-child {
|
||||
label {
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.radio:last-child {
|
||||
label {
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.radio + .radio {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
218
frontend/src/utils/mass-input/mass-input.js
Normal file
218
frontend/src/utils/mass-input/mass-input.js
Normal file
@ -0,0 +1,218 @@
|
||||
/**
|
||||
*
|
||||
* 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_SUBMIT_BUTTON_CLASS = 'massinput__submit-button';
|
||||
var MASS_INPUT_INITIALIZED_CLASS = 'mass-input--initialized';
|
||||
|
||||
var massInputUtil = function(element, app) {
|
||||
var massInputId;
|
||||
var massInputFormSubmitHandler;
|
||||
var massInputForm;
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Mass Input utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(MASS_INPUT_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
massInputId = element.dataset.massInputIdent || '_';
|
||||
massInputForm = element.closest('form');
|
||||
|
||||
if (!massInputForm) {
|
||||
throw new Error('Mass Input utility cannot be setup without being wrapped in a <form>!');
|
||||
}
|
||||
|
||||
massInputFormSubmitHandler = makeSubmitHandler();
|
||||
|
||||
// setup submit buttons inside this massinput so browser
|
||||
// uses correct submit button for form submission.
|
||||
var buttons = getMassInputSubmitButtons();
|
||||
buttons.forEach(function(button) {
|
||||
setupSubmitButton(button);
|
||||
});
|
||||
|
||||
massInputForm.addEventListener('submit', massInputFormSubmitHandler);
|
||||
massInputForm.addEventListener('keypress', keypressHandler);
|
||||
|
||||
// mark initialized
|
||||
element.classList.add(MASS_INPUT_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: MASS_INPUT_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {
|
||||
reset();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeSubmitHandler() {
|
||||
var method = massInputForm.getAttribute('method') || 'POST';
|
||||
var url = massInputForm.getAttribute('action') || window.location.href;
|
||||
var enctype = massInputForm.getAttribute('enctype') || 'application/json';
|
||||
|
||||
var requestFn;
|
||||
if (app.httpClient[method.toLowerCase()]) {
|
||||
requestFn = app.httpClient[method.toLowerCase()].bind(app.httpClient);
|
||||
}
|
||||
|
||||
return function(event) {
|
||||
var activeElement;
|
||||
|
||||
// check if event occured from either a mass input add/delete button or
|
||||
// from inside one of massinput's inputs (i.e. a child is focused/active)
|
||||
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('.' + MASS_INPUT_SUBMIT_BUTTON_CLASS);
|
||||
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) {
|
||||
var headers = {'Mass-Input-Shortcircuit': massInputId};
|
||||
|
||||
if (enctype !== 'multipart/form-data')
|
||||
headers['Content-Type'] = enctype;
|
||||
|
||||
requestFn({
|
||||
url: url,
|
||||
headers: headers,
|
||||
body: requestBody,
|
||||
}).then(function(response) {
|
||||
return app.htmlHelpers.parseResponse(response);
|
||||
}).then(function(response) {
|
||||
processResponse(response.element);
|
||||
if (isAddCell) {
|
||||
reFocusAddCell();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function keypressHandler(event) {
|
||||
if (event.keyCode !== 13) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (massInputFormSubmitHandler) {
|
||||
return massInputFormSubmitHandler(event);
|
||||
}
|
||||
}
|
||||
|
||||
function getMassInputSubmitButtons() {
|
||||
return Array.from(element.querySelectorAll('button[type="submit"][name][value], .' + MASS_INPUT_SUBMIT_BUTTON_CLASS));
|
||||
}
|
||||
|
||||
function setupSubmitButton(button) {
|
||||
button.setAttribute('type', 'button');
|
||||
button.classList.add(MASS_INPUT_SUBMIT_BUTTON_CLASS);
|
||||
button.addEventListener('click', massInputFormSubmitHandler);
|
||||
}
|
||||
|
||||
function resetSubmitButton(button) {
|
||||
button.setAttribute('type', 'submit');
|
||||
button.classList.remove(MASS_INPUT_SUBMIT_BUTTON_CLASS);
|
||||
button.removeEventListener('click', massInputFormSubmitHandler);
|
||||
}
|
||||
|
||||
function processResponse(responseElement) {
|
||||
element.innerHTML = "";
|
||||
element.appendChild(responseElement);
|
||||
|
||||
reset();
|
||||
|
||||
app.utilRegistry.setupAll(element);
|
||||
}
|
||||
|
||||
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) {
|
||||
// Clearing of add-inputs is done in the backend
|
||||
addCellInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
element.classList.remove(MASS_INPUT_INITIALIZED_CLASS);
|
||||
massInputForm.removeEventListener('submit', massInputFormSubmitHandler);
|
||||
massInputForm.removeEventListener('keypress', keypressHandler);
|
||||
|
||||
var buttons = getMassInputSubmitButtons();
|
||||
buttons.forEach(function(button) {
|
||||
resetSubmitButton(button);
|
||||
});
|
||||
}
|
||||
|
||||
return init();
|
||||
};
|
||||
|
||||
export default {
|
||||
name: MASS_INPUT_UTIL_NAME,
|
||||
selector: MASS_INPUT_UTIL_SELECTOR,
|
||||
setup: massInputUtil,
|
||||
};
|
||||
203
frontend/src/utils/modal/modal.js
Normal file
203
frontend/src/utils/modal/modal.js
Normal file
@ -0,0 +1,203 @@
|
||||
import './modal.scss';
|
||||
|
||||
/**
|
||||
*
|
||||
* Modal Utility
|
||||
*
|
||||
* Attribute: uw-modal
|
||||
*
|
||||
* Params:
|
||||
* data-modal-trigger: string
|
||||
* Selector for the element that toggles the modal.
|
||||
* If trigger element has "href" attribute the modal will be dynamically loaded from the referenced page
|
||||
* data-modal-closeable: boolean property
|
||||
* If the param is present the modal will have a close-icon and can also be closed by clicking anywhere on the overlay
|
||||
*
|
||||
* Example usage:
|
||||
* <div uw-modal data-modal-trigger='#trigger' data-modal-closeable>This is the modal content
|
||||
* <div id='trigger'>Click me to open the modal
|
||||
*/
|
||||
|
||||
var MODAL_UTIL_NAME = 'modal';
|
||||
var MODAL_UTIL_SELECTOR = '[uw-modal]';
|
||||
|
||||
var MODAL_HEADERS = {
|
||||
'Is-Modal': 'True',
|
||||
};
|
||||
|
||||
var MODAL_INITIALIZED_CLASS = 'modal--initialized';
|
||||
var MODAL_CLASS = 'modal';
|
||||
var MODAL_OPEN_CLASS = 'modal--open';
|
||||
var MODAL_TRIGGER_CLASS = 'modal__trigger';
|
||||
var MODAL_CONTENT_CLASS = 'modal__content';
|
||||
var MODAL_OVERLAY_CLASS = 'modal__overlay';
|
||||
var MODAL_OVERLAY_OPEN_CLASS = 'modal__overlay--open';
|
||||
var MODAL_CLOSER_CLASS = 'modal__closer';
|
||||
|
||||
var MAIN_CONTENT_CLASS = 'main__content-body';
|
||||
|
||||
// one singleton wrapper to keep all the modals to avoid CSS bug
|
||||
// with blurry text due to `transform: translate(-50%, -50%)`
|
||||
// will be created (and reused) for the first modal that gets initialized
|
||||
var MODALS_WRAPPER_CLASS = 'modals-wrapper';
|
||||
var MODALS_WRAPPER_SELECTOR = '.' + MODALS_WRAPPER_CLASS;
|
||||
var MODALS_WRAPPER_OPEN_CLASS = 'modals-wrapper--open';
|
||||
|
||||
var modalUtil = function(element, app) {
|
||||
|
||||
var modalsWrapper;
|
||||
var modalOverlay;
|
||||
var modalUrl;
|
||||
|
||||
function _init() {
|
||||
if (!element) {
|
||||
throw new Error('Modal utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(MODAL_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ensureModalWrapper();
|
||||
|
||||
// param modalTrigger
|
||||
if (!element.dataset.modalTrigger) {
|
||||
throw new Error('Modal utility cannot be setup without a trigger element!');
|
||||
} else {
|
||||
setupTrigger();
|
||||
}
|
||||
|
||||
// param modalCloseable
|
||||
if (element.dataset.modalCloseable !== undefined) {
|
||||
setupCloser();
|
||||
}
|
||||
|
||||
// mark as initialized and add modal class for styling
|
||||
element.classList.add(MODAL_INITIALIZED_CLASS, MODAL_CLASS);
|
||||
|
||||
return {
|
||||
name: MODAL_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
function ensureModalWrapper() {
|
||||
modalsWrapper = document.querySelector(MODALS_WRAPPER_SELECTOR);
|
||||
if (!modalsWrapper) {
|
||||
// create modal wrapper
|
||||
modalsWrapper = document.createElement('div');
|
||||
modalsWrapper.classList.add(MODALS_WRAPPER_CLASS);
|
||||
document.body.appendChild(modalsWrapper);
|
||||
}
|
||||
|
||||
modalOverlay = modalsWrapper.querySelector('.' + MODAL_OVERLAY_CLASS);
|
||||
if (!modalOverlay) {
|
||||
// create modal overlay
|
||||
modalOverlay = document.createElement('div');
|
||||
modalOverlay.classList.add(MODAL_OVERLAY_CLASS);
|
||||
modalsWrapper.appendChild(modalOverlay);
|
||||
}
|
||||
}
|
||||
|
||||
function setupTrigger() {
|
||||
var triggerSelector = element.dataset.modalTrigger;
|
||||
if (!triggerSelector.startsWith('#')) {
|
||||
triggerSelector = '#' + triggerSelector;
|
||||
}
|
||||
var triggerElement = document.querySelector(triggerSelector);
|
||||
|
||||
if (!triggerElement) {
|
||||
throw new Error('Trigger element for Modal not found: "' + triggerSelector + '"');
|
||||
}
|
||||
|
||||
triggerElement.classList.add(MODAL_TRIGGER_CLASS);
|
||||
triggerElement.addEventListener('click', onTriggerClicked, false);
|
||||
modalUrl = triggerElement.getAttribute('href');
|
||||
}
|
||||
|
||||
function setupCloser() {
|
||||
var closerElement = document.createElement('div');
|
||||
element.insertBefore(closerElement, null);
|
||||
closerElement.classList.add(MODAL_CLOSER_CLASS);
|
||||
closerElement.addEventListener('click', onCloseClicked, false);
|
||||
modalOverlay.addEventListener('click', onCloseClicked, false);
|
||||
}
|
||||
|
||||
function onTriggerClicked(event) {
|
||||
event.preventDefault();
|
||||
open();
|
||||
}
|
||||
|
||||
function onCloseClicked(event) {
|
||||
event.preventDefault();
|
||||
close();
|
||||
}
|
||||
|
||||
function onKeyUp(event) {
|
||||
if (event.key === 'Escape') {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
function open() {
|
||||
element.classList.add(MODAL_OPEN_CLASS);
|
||||
modalOverlay.classList.add(MODAL_OVERLAY_OPEN_CLASS);
|
||||
modalsWrapper.classList.add(MODALS_WRAPPER_OPEN_CLASS);
|
||||
modalsWrapper.appendChild(element);
|
||||
|
||||
if (modalUrl) {
|
||||
fillModal(modalUrl);
|
||||
}
|
||||
|
||||
document.addEventListener('keyup', onKeyUp);
|
||||
}
|
||||
|
||||
function close() {
|
||||
modalOverlay.classList.remove(MODAL_OVERLAY_OPEN_CLASS);
|
||||
element.classList.remove(MODAL_OPEN_CLASS);
|
||||
modalsWrapper.classList.remove(MODALS_WRAPPER_OPEN_CLASS);
|
||||
|
||||
document.removeEventListener('keyup', onKeyUp);
|
||||
};
|
||||
|
||||
function fillModal(url) {
|
||||
app.httpClient.get({
|
||||
url: url,
|
||||
headers: MODAL_HEADERS,
|
||||
}).then(function(response) {
|
||||
return app.htmlHelpers.parseResponse(response);
|
||||
}).then(function(response) {
|
||||
processResponse(response.element);
|
||||
});
|
||||
}
|
||||
|
||||
function processResponse(responseElement) {
|
||||
var modalContent = document.createElement('div');
|
||||
modalContent.classList.add(MODAL_CONTENT_CLASS);
|
||||
|
||||
var contentBody = responseElement.querySelector('.' + MAIN_CONTENT_CLASS);
|
||||
|
||||
if (contentBody) {
|
||||
modalContent.innerHTML = contentBody.innerHTML;
|
||||
}
|
||||
|
||||
var previousModalContent = element.querySelector('.' + MODAL_CONTENT_CLASS);
|
||||
if (previousModalContent) {
|
||||
previousModalContent.remove();
|
||||
}
|
||||
|
||||
element.insertBefore(modalContent, null);
|
||||
|
||||
// setup any newly arrived utils
|
||||
app.utilRegistry.setupAll(element);
|
||||
}
|
||||
|
||||
return _init();
|
||||
};
|
||||
|
||||
export default {
|
||||
name: MODAL_UTIL_NAME,
|
||||
selector: MODAL_UTIL_SELECTOR,
|
||||
setup: modalUtil,
|
||||
};
|
||||
115
frontend/src/utils/modal/modal.scss
Normal file
115
frontend/src/utils/modal/modal.scss
Normal file
@ -0,0 +1,115 @@
|
||||
.modals-wrapper {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.modals-wrapper--open {
|
||||
z-index: 200;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: relative;
|
||||
display: none;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
min-width: 60vw;
|
||||
max-width: 70vw;
|
||||
min-height: 100px;
|
||||
max-height: calc(100vh - 30px);
|
||||
border-radius: 2px;
|
||||
z-index: -1;
|
||||
color: var(--color-font);
|
||||
padding: 0 40px;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
|
||||
&.modal--open {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
z-index: 200;
|
||||
transition:
|
||||
opacity .2s .1s ease-in-out,
|
||||
transform .3s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.modal {
|
||||
min-width: 80vw;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.modal {
|
||||
min-width: 90vw;
|
||||
}
|
||||
}
|
||||
@media (max-width: 425px) {
|
||||
.modal {
|
||||
min-width: calc(100vw - 20px);
|
||||
}
|
||||
}
|
||||
|
||||
.modal__overlay {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
z-index: -1;
|
||||
transition: all .2s ease;
|
||||
display: none;
|
||||
|
||||
&.modal__overlay--open {
|
||||
display: block;
|
||||
z-index: 199;
|
||||
opacity: 1;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.modal__trigger {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal__trigger-label {
|
||||
font-style: italic;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.modal__closer {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-color: var(--color-darker);
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
z-index: 20;
|
||||
|
||||
&::before {
|
||||
content: '\f00d';
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.modal__content {
|
||||
margin: 20px 0;
|
||||
width: 100%;
|
||||
}
|
||||
109
frontend/src/utils/show-hide/show-hide.js
Normal file
109
frontend/src/utils/show-hide/show-hide.js
Normal file
@ -0,0 +1,109 @@
|
||||
import './show-hide.scss';
|
||||
|
||||
var SHOW_HIDE_UTIL_NAME = 'showHide';
|
||||
var SHOW_HIDE_UTIL_SELECTOR = '[uw-show-hide]';
|
||||
|
||||
var SHOW_HIDE_LOCAL_STORAGE_KEY = 'SHOW_HIDE';
|
||||
var SHOW_HIDE_INITIALIZED_CLASS = 'show-hide--initialized';
|
||||
var SHOW_HIDE_COLLAPSED_CLASS = 'show-hide--collapsed';
|
||||
var SHOW_HIDE_TOGGLE_CLASS = 'show-hide__toggle';
|
||||
var SHOW_HIDE_TOGGLE_RIGHT_CLASS = 'show-hide__toggle--right';
|
||||
|
||||
/**
|
||||
*
|
||||
* ShowHide Utility
|
||||
*
|
||||
* Attribute: uw-show-hide
|
||||
*
|
||||
* Params: (all optional)
|
||||
* data-show-hide-id: string
|
||||
* If this param is given the state of the utility will be persisted in the clients local storage.
|
||||
* data-show-hide-collapsed: boolean property
|
||||
* If this param is present the ShowHide utility will be collapsed. This value will be overruled by any value stored in the LocalStorage.
|
||||
* data-show-hide-align: 'right'
|
||||
* Where to put the arrow that marks the element as a ShowHide toggle. Left of toggle by default.
|
||||
*
|
||||
* Example usage:
|
||||
* <div>
|
||||
* <div uw-show-hide>Click me
|
||||
* <div>This will be toggled
|
||||
* <div>This will be toggled as well
|
||||
*/
|
||||
var showHideUtil = function(element) {
|
||||
|
||||
var showHideId;
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('ShowHide utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(SHOW_HIDE_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// register click listener
|
||||
addClickListener();
|
||||
|
||||
// param showHideId
|
||||
if (element.dataset.showHideId) {
|
||||
showHideId = element.dataset.showHideId;
|
||||
}
|
||||
|
||||
// param showHideCollapsed
|
||||
var collapsed = false;
|
||||
if (element.dataset.showHideCollapsed !== undefined) {
|
||||
collapsed = true;
|
||||
}
|
||||
if (showHideId) {
|
||||
var localStorageCollapsed = getLocalStorage()[showHideId];
|
||||
if (typeof localStorageCollapsed !== 'undefined') {
|
||||
collapsed = localStorageCollapsed;
|
||||
}
|
||||
}
|
||||
element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS, collapsed);
|
||||
|
||||
// param showHideAlign
|
||||
var alignment = element.dataset.showHideAlign;
|
||||
if (alignment === 'right') {
|
||||
element.classList.add(SHOW_HIDE_TOGGLE_RIGHT_CLASS);
|
||||
}
|
||||
|
||||
// mark as initialized
|
||||
element.classList.add(SHOW_HIDE_INITIALIZED_CLASS, SHOW_HIDE_TOGGLE_CLASS);
|
||||
|
||||
return {
|
||||
name: SHOW_HIDE_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
function addClickListener() {
|
||||
element.addEventListener('click', function clickListener() {
|
||||
var newState = element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS);
|
||||
|
||||
if (showHideId) {
|
||||
setLocalStorage(showHideId, newState);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setLocalStorage(id, state) {
|
||||
var lsData = getLocalStorage();
|
||||
lsData[id] = state;
|
||||
window.localStorage.setItem(SHOW_HIDE_LOCAL_STORAGE_KEY, JSON.stringify(lsData));
|
||||
}
|
||||
|
||||
function getLocalStorage() {
|
||||
return JSON.parse(window.localStorage.getItem(SHOW_HIDE_LOCAL_STORAGE_KEY)) || {};
|
||||
}
|
||||
|
||||
return init();
|
||||
};
|
||||
|
||||
export default {
|
||||
name: SHOW_HIDE_UTIL_NAME,
|
||||
selector: SHOW_HIDE_UTIL_SELECTOR,
|
||||
setup: showHideUtil,
|
||||
};
|
||||
54
frontend/src/utils/show-hide/show-hide.scss
Normal file
54
frontend/src/utils/show-hide/show-hide.scss
Normal file
@ -0,0 +1,54 @@
|
||||
$show-hide-toggle-size: 6px;
|
||||
|
||||
.show-hide__toggle {
|
||||
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-grey-lighter);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.show-hide__toggle::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: $show-hide-toggle-size;
|
||||
height: $show-hide-toggle-size;
|
||||
left: -15px;
|
||||
top: 50%;
|
||||
color: var(--color-primary);
|
||||
border-right: 2px solid currentColor;
|
||||
border-top: 2px solid currentColor;
|
||||
transition: transform .2s ease;
|
||||
transform: translateY(-50%) rotate(-45deg);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
left: auto;
|
||||
right: 20px;
|
||||
color: var(--color-font);
|
||||
}
|
||||
}
|
||||
|
||||
.show-hide__toggle--right::before {
|
||||
left: auto;
|
||||
right: 20px;
|
||||
color: var(--color-font);
|
||||
}
|
||||
|
||||
.show-hide--collapsed {
|
||||
|
||||
.show-hide__toggle::before {
|
||||
transform: translateY(-50%) rotate(135deg);
|
||||
}
|
||||
|
||||
:not(.show-hide__toggle) {
|
||||
display: block;
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-height: 0;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
||||
92
frontend/src/utils/tabber/tabber.js
Normal file
92
frontend/src/utils/tabber/tabber.js
Normal file
@ -0,0 +1,92 @@
|
||||
import './tabber.scss';
|
||||
|
||||
(function($) {
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
'use strict';
|
||||
|
||||
// define plugin
|
||||
$.fn.tabgroup = function() {
|
||||
|
||||
var $this = $(this);
|
||||
var $openers = $('<div class="tab-group-openers"></div>');
|
||||
$this.prepend($openers);
|
||||
|
||||
var openedByDefault = $this.data('tab-open') || 0;
|
||||
var tabs = [];
|
||||
var currentTab = {};
|
||||
|
||||
$this.find('.tab').each(function(i, t) {
|
||||
var tab = $(t);
|
||||
tab.data('tab-index', i);
|
||||
var tabName = tab.data('tab-name') || 'Tab '+i;
|
||||
var tabFile = tab.data('tab-file') || false;
|
||||
var $opener = makeOpener(tabName, i);
|
||||
$openers.append($opener);
|
||||
if (tab.find('.tab-title')) {
|
||||
tab.find('.tab-title').remove();
|
||||
}
|
||||
tab.hide();
|
||||
var loaded = false;
|
||||
tabs.push({index: i, name: tabName, file: tabFile, dom: tab, opener: $opener, loaded: loaded });
|
||||
});
|
||||
|
||||
$this.on('click', 'a[href^="#"]', function(event) {
|
||||
var $target = $(event.currentTarget);
|
||||
var tab = getTabByName($target.attr('href').replace('#', ''));
|
||||
if ( tab ) {
|
||||
showTab(tab.index);
|
||||
}
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
function getTabByName(name) {
|
||||
var it = -1;
|
||||
$.each(tabs, function(i, t) {
|
||||
if ( t.name.toLowerCase() === name.toLowerCase() ) {
|
||||
it = i;
|
||||
}
|
||||
});
|
||||
if ( it >= 0 ) {
|
||||
return tabs[it];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function makeOpener(tabName, i) {
|
||||
return $('<span class="tab-opener">'+tabName+'</span>').
|
||||
on('click', function() {
|
||||
showTab(i);
|
||||
});
|
||||
}
|
||||
|
||||
function showTab(i) {
|
||||
tabs.forEach(function(t) {
|
||||
t.dom.hide();
|
||||
t.opener.removeClass('tab-visible');
|
||||
});
|
||||
currentTab = tabs[i];
|
||||
if ( !currentTab.loaded && currentTab.file ){
|
||||
$.get(currentTab.file, function(res) {
|
||||
currentTab.dom.html(res);
|
||||
currentTab.loaded = true;
|
||||
});
|
||||
}
|
||||
currentTab.opener.addClass('tab-visible');
|
||||
currentTab.dom.show();
|
||||
}
|
||||
|
||||
showTab(openedByDefault);
|
||||
currentTab = tabs[openedByDefault];
|
||||
|
||||
};
|
||||
|
||||
// apply plugin to all available tab-groups if on wide screen
|
||||
if (window.innerWidth > 768) {
|
||||
$('.tab-group').each(function(i, t) {
|
||||
$(t).tabgroup();
|
||||
});
|
||||
}
|
||||
});
|
||||
})($);
|
||||
39
frontend/src/utils/tabber/tabber.scss
Normal file
39
frontend/src/utils/tabber/tabber.scss
Normal file
@ -0,0 +1,39 @@
|
||||
.tab-group {
|
||||
border-top: 2px solid #dcdcdc;
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
.tab-group-openers {
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
line-height: 40px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.tab-opener {
|
||||
display: inline-block;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 0 13px;
|
||||
margin: 0 2px;
|
||||
background-color: var(--color-dark);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
transition: all .1s ease;
|
||||
border-bottom: 5px solid rgba(100, 100, 100, 0.2);
|
||||
}
|
||||
.tab-opener:not(.tab-visible):hover {
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
color: rgb(52, 48, 58);
|
||||
border-bottom-color: grey;
|
||||
}
|
||||
|
||||
.tab-opener.tab-visible {
|
||||
background-color: transparent;
|
||||
color: rgb(52, 48, 58);
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
7
frontend/src/utils/tooltips/tooltips.js
Normal file
7
frontend/src/utils/tooltips/tooltips.js
Normal file
@ -0,0 +1,7 @@
|
||||
import './tooltips.scss';
|
||||
|
||||
export default {
|
||||
name: 'tooltips',
|
||||
selector: '[not-something-that-would-be-found]',
|
||||
setup: () => {},
|
||||
};
|
||||
88
frontend/src/utils/tooltips/tooltips.scss
Normal file
88
frontend/src/utils/tooltips/tooltips.scss
Normal file
@ -0,0 +1,88 @@
|
||||
.tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 1.5rem;
|
||||
vertical-align: -0.375rem;
|
||||
|
||||
&:hover .tooltip__content {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip__handle {
|
||||
background-color: var(--color-dark);
|
||||
border-radius: 50%;
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
line-height: 1.5rem;
|
||||
font-size: 1.2rem;
|
||||
color: white;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
margin: 0 10px;
|
||||
cursor: default;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '\f128';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
font-family: "Font Awesome 5 Free";
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
&.tooltip__handle--danger::before {
|
||||
content: '\f12a';
|
||||
}
|
||||
|
||||
&.tooltip__handle--danger {
|
||||
background-color: var(--color-warning);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-light);
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip__content {
|
||||
position: absolute;
|
||||
display: none;
|
||||
top: -10px;
|
||||
transform: translateY(-100%);
|
||||
right: 3px;
|
||||
width: 275px;
|
||||
z-index: 10;
|
||||
background-color: #fafafa;
|
||||
border-radius: 4px;
|
||||
padding: 13px 17px;
|
||||
box-shadow: 0 0 20px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: #fafafa;
|
||||
transform: rotate(45deg);
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
.tooltip {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
|
||||
.tooltip__content {
|
||||
left: 3px;
|
||||
right: 3px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
frontend/src/utils/utils.js
Normal file
38
frontend/src/utils/utils.js
Normal file
@ -0,0 +1,38 @@
|
||||
import alerts from './alerts/alerts';
|
||||
import asidenav from './asidenav/asidenav';
|
||||
import asyncForm from './async-form/async-form';
|
||||
import asyncTable from './async-table/async-table';
|
||||
import checkAll from './check-all/check-all';
|
||||
import massInput from './mass-input/mass-input';
|
||||
import { fileInput, checkbox } from './inputs/inputs';
|
||||
import modal from './modal/modal';
|
||||
import showHide from './show-hide/show-hide';
|
||||
import {
|
||||
interactiveFieldset,
|
||||
navigateAwayPrompt,
|
||||
autoSubmitButton,
|
||||
autoSubmitInput,
|
||||
formErrorRemover,
|
||||
datepicker,
|
||||
} from './form/form';
|
||||
import tooltips from './tooltips/tooltips';
|
||||
|
||||
export default [
|
||||
alerts,
|
||||
asidenav,
|
||||
asyncForm,
|
||||
asyncTable,
|
||||
checkAll,
|
||||
massInput,
|
||||
fileInput,
|
||||
checkbox,
|
||||
modal,
|
||||
showHide,
|
||||
interactiveFieldset,
|
||||
navigateAwayPrompt,
|
||||
autoSubmitButton,
|
||||
autoSubmitInput,
|
||||
formErrorRemover,
|
||||
datepicker,
|
||||
tooltips,
|
||||
];
|
||||
740
frontend/vendor/flatpickr.css
vendored
Normal file
740
frontend/vendor/flatpickr.css
vendored
Normal file
@ -0,0 +1,740 @@
|
||||
.flatpickr-calendar {
|
||||
background: transparent;
|
||||
opacity: 0;
|
||||
display: none;
|
||||
text-align: center;
|
||||
visibility: hidden;
|
||||
padding: 0;
|
||||
-webkit-animation: none;
|
||||
animation: none;
|
||||
direction: ltr;
|
||||
border: 0;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
border-radius: 5px;
|
||||
position: absolute;
|
||||
width: 307.875px;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-ms-touch-action: manipulation;
|
||||
touch-action: manipulation;
|
||||
background: #fff;
|
||||
-webkit-box-shadow: 1px 0 0 #e6e6e6, -1px 0 0 #e6e6e6, 0 1px 0 #e6e6e6, 0 -1px 0 #e6e6e6, 0 3px 13px rgba(0,0,0,0.08);
|
||||
box-shadow: 1px 0 0 #e6e6e6, -1px 0 0 #e6e6e6, 0 1px 0 #e6e6e6, 0 -1px 0 #e6e6e6, 0 3px 13px rgba(0,0,0,0.08);
|
||||
}
|
||||
.flatpickr-calendar.open,
|
||||
.flatpickr-calendar.inline {
|
||||
opacity: 1;
|
||||
max-height: 640px;
|
||||
visibility: visible;
|
||||
}
|
||||
.flatpickr-calendar.open {
|
||||
display: inline-block;
|
||||
z-index: 99999;
|
||||
}
|
||||
.flatpickr-calendar.animate.open {
|
||||
-webkit-animation: fpFadeInDown 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
animation: fpFadeInDown 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
.flatpickr-calendar.inline {
|
||||
display: block;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
.flatpickr-calendar.static {
|
||||
position: absolute;
|
||||
top: calc(100% + 2px);
|
||||
}
|
||||
.flatpickr-calendar.static.open {
|
||||
z-index: 999;
|
||||
display: block;
|
||||
}
|
||||
.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+1) .flatpickr-day.inRange:nth-child(7n+7) {
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+2) .flatpickr-day.inRange:nth-child(7n+1) {
|
||||
-webkit-box-shadow: -2px 0 0 #e6e6e6, 5px 0 0 #e6e6e6;
|
||||
box-shadow: -2px 0 0 #e6e6e6, 5px 0 0 #e6e6e6;
|
||||
}
|
||||
.flatpickr-calendar .hasWeeks .dayContainer,
|
||||
.flatpickr-calendar .hasTime .dayContainer {
|
||||
border-bottom: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.flatpickr-calendar .hasWeeks .dayContainer {
|
||||
border-left: 0;
|
||||
}
|
||||
.flatpickr-calendar.showTimeInput.hasTime .flatpickr-time {
|
||||
height: 40px;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
.flatpickr-calendar.noCalendar.hasTime .flatpickr-time {
|
||||
height: auto;
|
||||
}
|
||||
.flatpickr-calendar:before,
|
||||
.flatpickr-calendar:after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
border: solid transparent;
|
||||
content: '';
|
||||
height: 0;
|
||||
width: 0;
|
||||
left: 22px;
|
||||
}
|
||||
.flatpickr-calendar.rightMost:before,
|
||||
.flatpickr-calendar.rightMost:after {
|
||||
left: auto;
|
||||
right: 22px;
|
||||
}
|
||||
.flatpickr-calendar:before {
|
||||
border-width: 5px;
|
||||
margin: 0 -5px;
|
||||
}
|
||||
.flatpickr-calendar:after {
|
||||
border-width: 4px;
|
||||
margin: 0 -4px;
|
||||
}
|
||||
.flatpickr-calendar.arrowTop:before,
|
||||
.flatpickr-calendar.arrowTop:after {
|
||||
bottom: 100%;
|
||||
}
|
||||
.flatpickr-calendar.arrowTop:before {
|
||||
border-bottom-color: #e6e6e6;
|
||||
}
|
||||
.flatpickr-calendar.arrowTop:after {
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.flatpickr-calendar.arrowBottom:before,
|
||||
.flatpickr-calendar.arrowBottom:after {
|
||||
top: 100%;
|
||||
}
|
||||
.flatpickr-calendar.arrowBottom:before {
|
||||
border-top-color: #e6e6e6;
|
||||
}
|
||||
.flatpickr-calendar.arrowBottom:after {
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.flatpickr-calendar:focus {
|
||||
outline: 0;
|
||||
}
|
||||
.flatpickr-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.flatpickr-months {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
.flatpickr-months .flatpickr-month {
|
||||
background: transparent;
|
||||
color: rgba(0,0,0,0.9);
|
||||
fill: rgba(0,0,0,0.9);
|
||||
height: 28px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
.flatpickr-months .flatpickr-prev-month,
|
||||
.flatpickr-months .flatpickr-next-month {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
line-height: 16px;
|
||||
height: 28px;
|
||||
padding: 10px;
|
||||
z-index: 3;
|
||||
}
|
||||
.flatpickr-months .flatpickr-prev-month.disabled,
|
||||
.flatpickr-months .flatpickr-next-month.disabled {
|
||||
display: none;
|
||||
}
|
||||
.flatpickr-months .flatpickr-prev-month i,
|
||||
.flatpickr-months .flatpickr-next-month i {
|
||||
position: relative;
|
||||
}
|
||||
.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,
|
||||
.flatpickr-months .flatpickr-next-month.flatpickr-prev-month {
|
||||
/*
|
||||
/*rtl:begin:ignore*/
|
||||
/*
|
||||
*/
|
||||
left: 0;
|
||||
/*
|
||||
/*rtl:end:ignore*/
|
||||
/*
|
||||
*/
|
||||
}
|
||||
/*
|
||||
/*rtl:begin:ignore*/
|
||||
/*
|
||||
/*rtl:end:ignore*/
|
||||
.flatpickr-months .flatpickr-prev-month.flatpickr-next-month,
|
||||
.flatpickr-months .flatpickr-next-month.flatpickr-next-month {
|
||||
/*
|
||||
/*rtl:begin:ignore*/
|
||||
/*
|
||||
*/
|
||||
right: 0;
|
||||
/*
|
||||
/*rtl:end:ignore*/
|
||||
/*
|
||||
*/
|
||||
}
|
||||
/*
|
||||
/*rtl:begin:ignore*/
|
||||
/*
|
||||
/*rtl:end:ignore*/
|
||||
.flatpickr-months .flatpickr-prev-month:hover,
|
||||
.flatpickr-months .flatpickr-next-month:hover {
|
||||
color: #959ea9;
|
||||
}
|
||||
.flatpickr-months .flatpickr-prev-month:hover svg,
|
||||
.flatpickr-months .flatpickr-next-month:hover svg {
|
||||
fill: #f64747;
|
||||
}
|
||||
.flatpickr-months .flatpickr-prev-month svg,
|
||||
.flatpickr-months .flatpickr-next-month svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.flatpickr-months .flatpickr-prev-month svg path,
|
||||
.flatpickr-months .flatpickr-next-month svg path {
|
||||
-webkit-transition: fill 0.1s;
|
||||
transition: fill 0.1s;
|
||||
fill: inherit;
|
||||
}
|
||||
.numInputWrapper {
|
||||
position: relative;
|
||||
height: auto;
|
||||
}
|
||||
.numInputWrapper input,
|
||||
.numInputWrapper span {
|
||||
display: inline-block;
|
||||
}
|
||||
.numInputWrapper input {
|
||||
width: 100%;
|
||||
min-width: auto !important;
|
||||
}
|
||||
.numInputWrapper input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
.numInputWrapper span {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 14px;
|
||||
padding: 0 4px 0 2px;
|
||||
height: 50%;
|
||||
line-height: 50%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid rgba(57,57,57,0.15);
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.numInputWrapper span:hover {
|
||||
background: rgba(0,0,0,0.1);
|
||||
}
|
||||
.numInputWrapper span:active {
|
||||
background: rgba(0,0,0,0.2);
|
||||
}
|
||||
.numInputWrapper span:after {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
}
|
||||
.numInputWrapper span.arrowUp {
|
||||
top: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
.numInputWrapper span.arrowUp:after {
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 4px solid rgba(57,57,57,0.6);
|
||||
top: 26%;
|
||||
}
|
||||
.numInputWrapper span.arrowDown {
|
||||
top: 50%;
|
||||
}
|
||||
.numInputWrapper span.arrowDown:after {
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 4px solid rgba(57,57,57,0.6);
|
||||
top: 40%;
|
||||
}
|
||||
.numInputWrapper span svg {
|
||||
width: inherit;
|
||||
height: auto;
|
||||
}
|
||||
.numInputWrapper span svg path {
|
||||
fill: rgba(0,0,0,0.5);
|
||||
}
|
||||
.numInputWrapper:hover {
|
||||
background: rgba(0,0,0,0.05);
|
||||
}
|
||||
.numInputWrapper:hover span {
|
||||
opacity: 1;
|
||||
}
|
||||
.flatpickr-current-month {
|
||||
font-size: 135%;
|
||||
line-height: inherit;
|
||||
font-weight: 300;
|
||||
color: inherit;
|
||||
position: absolute;
|
||||
width: 75%;
|
||||
left: 12.5%;
|
||||
padding: 6.16px 0 0 0;
|
||||
line-height: 1;
|
||||
height: 28px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
-webkit-transform: translate3d(0px, 0px, 0px);
|
||||
transform: translate3d(0px, 0px, 0px);
|
||||
}
|
||||
.flatpickr-current-month span.cur-month {
|
||||
font-family: inherit;
|
||||
font-weight: 700;
|
||||
color: inherit;
|
||||
display: inline-block;
|
||||
margin-left: 0.5ch;
|
||||
padding: 0;
|
||||
}
|
||||
.flatpickr-current-month span.cur-month:hover {
|
||||
background: rgba(0,0,0,0.05);
|
||||
}
|
||||
.flatpickr-current-month .numInputWrapper {
|
||||
width: 6ch;
|
||||
width: 7ch\0;
|
||||
display: inline-block;
|
||||
}
|
||||
.flatpickr-current-month .numInputWrapper span.arrowUp:after {
|
||||
border-bottom-color: rgba(0,0,0,0.9);
|
||||
}
|
||||
.flatpickr-current-month .numInputWrapper span.arrowDown:after {
|
||||
border-top-color: rgba(0,0,0,0.9);
|
||||
}
|
||||
.flatpickr-current-month input.cur-year {
|
||||
background: transparent;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
color: inherit;
|
||||
cursor: text;
|
||||
padding: 0 0 0 0.5ch;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: 300;
|
||||
line-height: inherit;
|
||||
height: auto;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
vertical-align: initial;
|
||||
}
|
||||
.flatpickr-current-month input.cur-year:focus {
|
||||
outline: 0;
|
||||
}
|
||||
.flatpickr-current-month input.cur-year[disabled],
|
||||
.flatpickr-current-month input.cur-year[disabled]:hover {
|
||||
font-size: 100%;
|
||||
color: rgba(0,0,0,0.5);
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
.flatpickr-weekdays {
|
||||
background: transparent;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
}
|
||||
.flatpickr-weekdays .flatpickr-weekdaycontainer {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
span.flatpickr-weekday {
|
||||
cursor: default;
|
||||
font-size: 90%;
|
||||
background: transparent;
|
||||
color: rgba(0,0,0,0.54);
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
display: block;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
font-weight: bolder;
|
||||
}
|
||||
.dayContainer,
|
||||
.flatpickr-weeks {
|
||||
padding: 1px 0 0 0;
|
||||
}
|
||||
.flatpickr-days {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: start;
|
||||
-webkit-align-items: flex-start;
|
||||
-ms-flex-align: start;
|
||||
align-items: flex-start;
|
||||
width: 307.875px;
|
||||
}
|
||||
.flatpickr-days:focus {
|
||||
outline: 0;
|
||||
}
|
||||
.dayContainer {
|
||||
padding: 0;
|
||||
outline: 0;
|
||||
text-align: left;
|
||||
width: 307.875px;
|
||||
min-width: 307.875px;
|
||||
max-width: 307.875px;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
display: -ms-flexbox;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
-ms-flex-wrap: wrap;
|
||||
-ms-flex-pack: justify;
|
||||
-webkit-justify-content: space-around;
|
||||
justify-content: space-around;
|
||||
-webkit-transform: translate3d(0px, 0px, 0px);
|
||||
transform: translate3d(0px, 0px, 0px);
|
||||
opacity: 1;
|
||||
}
|
||||
.dayContainer + .dayContainer {
|
||||
-webkit-box-shadow: -1px 0 0 #e6e6e6;
|
||||
box-shadow: -1px 0 0 #e6e6e6;
|
||||
}
|
||||
.flatpickr-day {
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 150px;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
color: #393939;
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
width: 14.2857143%;
|
||||
-webkit-flex-basis: 14.2857143%;
|
||||
-ms-flex-preferred-size: 14.2857143%;
|
||||
flex-basis: 14.2857143%;
|
||||
max-width: 39px;
|
||||
height: 39px;
|
||||
line-height: 39px;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
.flatpickr-day.inRange,
|
||||
.flatpickr-day.prevMonthDay.inRange,
|
||||
.flatpickr-day.nextMonthDay.inRange,
|
||||
.flatpickr-day.today.inRange,
|
||||
.flatpickr-day.prevMonthDay.today.inRange,
|
||||
.flatpickr-day.nextMonthDay.today.inRange,
|
||||
.flatpickr-day:hover,
|
||||
.flatpickr-day.prevMonthDay:hover,
|
||||
.flatpickr-day.nextMonthDay:hover,
|
||||
.flatpickr-day:focus,
|
||||
.flatpickr-day.prevMonthDay:focus,
|
||||
.flatpickr-day.nextMonthDay:focus {
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
background: #e6e6e6;
|
||||
border-color: #e6e6e6;
|
||||
}
|
||||
.flatpickr-day.today {
|
||||
border-color: #959ea9;
|
||||
}
|
||||
.flatpickr-day.today:hover,
|
||||
.flatpickr-day.today:focus {
|
||||
border-color: #959ea9;
|
||||
background: #959ea9;
|
||||
color: #fff;
|
||||
}
|
||||
.flatpickr-day.selected,
|
||||
.flatpickr-day.startRange,
|
||||
.flatpickr-day.endRange,
|
||||
.flatpickr-day.selected.inRange,
|
||||
.flatpickr-day.startRange.inRange,
|
||||
.flatpickr-day.endRange.inRange,
|
||||
.flatpickr-day.selected:focus,
|
||||
.flatpickr-day.startRange:focus,
|
||||
.flatpickr-day.endRange:focus,
|
||||
.flatpickr-day.selected:hover,
|
||||
.flatpickr-day.startRange:hover,
|
||||
.flatpickr-day.endRange:hover,
|
||||
.flatpickr-day.selected.prevMonthDay,
|
||||
.flatpickr-day.startRange.prevMonthDay,
|
||||
.flatpickr-day.endRange.prevMonthDay,
|
||||
.flatpickr-day.selected.nextMonthDay,
|
||||
.flatpickr-day.startRange.nextMonthDay,
|
||||
.flatpickr-day.endRange.nextMonthDay {
|
||||
background: #569ff7;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
color: #fff;
|
||||
border-color: #569ff7;
|
||||
}
|
||||
.flatpickr-day.selected.startRange,
|
||||
.flatpickr-day.startRange.startRange,
|
||||
.flatpickr-day.endRange.startRange {
|
||||
border-radius: 50px 0 0 50px;
|
||||
}
|
||||
.flatpickr-day.selected.endRange,
|
||||
.flatpickr-day.startRange.endRange,
|
||||
.flatpickr-day.endRange.endRange {
|
||||
border-radius: 0 50px 50px 0;
|
||||
}
|
||||
.flatpickr-day.selected.startRange + .endRange,
|
||||
.flatpickr-day.startRange.startRange + .endRange,
|
||||
.flatpickr-day.endRange.startRange + .endRange {
|
||||
-webkit-box-shadow: -10px 0 0 #569ff7;
|
||||
box-shadow: -10px 0 0 #569ff7;
|
||||
}
|
||||
.flatpickr-day.selected.startRange.endRange,
|
||||
.flatpickr-day.startRange.startRange.endRange,
|
||||
.flatpickr-day.endRange.startRange.endRange {
|
||||
border-radius: 50px;
|
||||
}
|
||||
.flatpickr-day.inRange {
|
||||
border-radius: 0;
|
||||
-webkit-box-shadow: -5px 0 0 #e6e6e6, 5px 0 0 #e6e6e6;
|
||||
box-shadow: -5px 0 0 #e6e6e6, 5px 0 0 #e6e6e6;
|
||||
}
|
||||
.flatpickr-day.disabled,
|
||||
.flatpickr-day.disabled:hover,
|
||||
.flatpickr-day.prevMonthDay,
|
||||
.flatpickr-day.nextMonthDay,
|
||||
.flatpickr-day.notAllowed,
|
||||
.flatpickr-day.notAllowed.prevMonthDay,
|
||||
.flatpickr-day.notAllowed.nextMonthDay {
|
||||
color: rgba(57,57,57,0.3);
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
.flatpickr-day.disabled,
|
||||
.flatpickr-day.disabled:hover {
|
||||
cursor: not-allowed;
|
||||
color: rgba(57,57,57,0.1);
|
||||
}
|
||||
.flatpickr-day.week.selected {
|
||||
border-radius: 0;
|
||||
-webkit-box-shadow: -5px 0 0 #569ff7, 5px 0 0 #569ff7;
|
||||
box-shadow: -5px 0 0 #569ff7, 5px 0 0 #569ff7;
|
||||
}
|
||||
.flatpickr-day.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
.rangeMode .flatpickr-day {
|
||||
margin-top: 1px;
|
||||
}
|
||||
.flatpickr-weekwrapper {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
.flatpickr-weekwrapper .flatpickr-weeks {
|
||||
padding: 0 12px;
|
||||
-webkit-box-shadow: 1px 0 0 #e6e6e6;
|
||||
box-shadow: 1px 0 0 #e6e6e6;
|
||||
}
|
||||
.flatpickr-weekwrapper .flatpickr-weekday {
|
||||
float: none;
|
||||
width: 100%;
|
||||
line-height: 28px;
|
||||
}
|
||||
.flatpickr-weekwrapper span.flatpickr-day,
|
||||
.flatpickr-weekwrapper span.flatpickr-day:hover {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
color: rgba(57,57,57,0.3);
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
border: none;
|
||||
}
|
||||
.flatpickr-innerContainer {
|
||||
display: block;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
.flatpickr-rContainer {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.flatpickr-time {
|
||||
text-align: center;
|
||||
outline: 0;
|
||||
display: block;
|
||||
height: 0;
|
||||
line-height: 40px;
|
||||
max-height: 40px;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
.flatpickr-time:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
.flatpickr-time .numInputWrapper {
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
width: 40%;
|
||||
height: 40px;
|
||||
float: left;
|
||||
}
|
||||
.flatpickr-time .numInputWrapper span.arrowUp:after {
|
||||
border-bottom-color: #393939;
|
||||
}
|
||||
.flatpickr-time .numInputWrapper span.arrowDown:after {
|
||||
border-top-color: #393939;
|
||||
}
|
||||
.flatpickr-time.hasSeconds .numInputWrapper {
|
||||
width: 26%;
|
||||
}
|
||||
.flatpickr-time.time24hr .numInputWrapper {
|
||||
width: 49%;
|
||||
}
|
||||
.flatpickr-time input {
|
||||
background: transparent;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: inherit;
|
||||
line-height: inherit;
|
||||
cursor: pointer;
|
||||
color: #393939;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.flatpickr-time input.flatpickr-hour {
|
||||
font-weight: bold;
|
||||
}
|
||||
.flatpickr-time input.flatpickr-minute,
|
||||
.flatpickr-time input.flatpickr-second {
|
||||
font-weight: 400;
|
||||
}
|
||||
.flatpickr-time input:focus {
|
||||
outline: 0;
|
||||
border: 0;
|
||||
}
|
||||
.flatpickr-time .flatpickr-time-separator,
|
||||
.flatpickr-time .flatpickr-am-pm {
|
||||
height: inherit;
|
||||
display: inline-block;
|
||||
float: left;
|
||||
line-height: inherit;
|
||||
color: #393939;
|
||||
font-weight: bold;
|
||||
width: 2%;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-align-self: center;
|
||||
-ms-flex-item-align: center;
|
||||
align-self: center;
|
||||
}
|
||||
.flatpickr-time .flatpickr-am-pm {
|
||||
outline: 0;
|
||||
width: 18%;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
}
|
||||
.flatpickr-time .flatpickr-am-pm:hover,
|
||||
.flatpickr-time .flatpickr-am-pm:focus {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.flatpickr-input[readonly] {
|
||||
cursor: pointer;
|
||||
min-width: auto;
|
||||
}
|
||||
@-webkit-keyframes fpFadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
-webkit-transform: translate3d(0, -20px, 0);
|
||||
transform: translate3d(0, -20px, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
@keyframes fpFadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
-webkit-transform: translate3d(0, -20px, 0);
|
||||
transform: translate3d(0, -20px, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
2
frontend/vendor/flatpickr.js
vendored
Normal file
2
frontend/vendor/flatpickr.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
frontend/vendor/fontawesome.css
vendored
Normal file
5
frontend/vendor/fontawesome.css
vendored
Normal file
File diff suppressed because one or more lines are too long
5
frontend/vendor/main.js
vendored
Normal file
5
frontend/vendor/main.js
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
import './flatpickr';
|
||||
import './zepto';
|
||||
|
||||
import './fontawesome.css';
|
||||
import './flatpickr.css';
|
||||
2
frontend/vendor/zepto.js
vendored
Normal file
2
frontend/vendor/zepto.js
vendored
Normal file
File diff suppressed because one or more lines are too long
129
static/fonts/fonts.css
Normal file
129
static/fonts/fonts.css
Normal file
@ -0,0 +1,129 @@
|
||||
/*!
|
||||
* Font Awesome Free 5.1.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
*/
|
||||
@font-face{
|
||||
font-family:"Font Awesome 5 Free";
|
||||
font-style:normal;
|
||||
font-weight:900;
|
||||
src:url(../fonts/fontawesome/fa-solid-900.eot);
|
||||
src:url(../fonts/fontawesome/fa-solid-900.eot?#iefix) format("embedded-opentype"),
|
||||
url(../fonts/fontawesome/fa-solid-900.woff2) format("woff2"),
|
||||
url(../fonts/fontawesome/fa-solid-900.woff) format("woff"),
|
||||
url(../fonts/fontawesome/fa-solid-900.ttf) format("truetype"),
|
||||
url(../fonts/fontawesome/fa-solid-900.svg#fontawesome) format("svg");
|
||||
}
|
||||
.fa,.fas{
|
||||
font-family:"Font Awesome 5 Free";
|
||||
font-weight:900;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src:
|
||||
local('Source Sans Pro Light'),
|
||||
local('SourceSansPro-Light'),
|
||||
/* from https://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlxdu.woff2 */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_300.woff2') format('woff2'),
|
||||
/* from https://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlxdo.woff */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_300.woff') format('woff'),
|
||||
/* from https://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlxdq.eot */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_300.eot?#iefix') format('embedded-opentype'),
|
||||
/* from https://fonts.gstatic.com/l/font?kit=6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlxdp&skey=e2b26fc7e9e1ade8&v=v11#SourceSansPro */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_300.svg#SourceSansPro') format('svg'),
|
||||
/* from https://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlxdr.ttf */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_300.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src:
|
||||
local('Source Sans Pro Regular'),
|
||||
local('SourceSansPro-Regular'),
|
||||
/* from https://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2 */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_400.woff2') format('woff2'),
|
||||
/* from https://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7j.woff */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_400.woff') format('woff'),
|
||||
/* from https://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7h.eot */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_400.eot?#iefix') format('embedded-opentype'),
|
||||
/* from https://fonts.gstatic.com/l/font?kit=6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7i&skey=1e026b1c27170b9b&v=v11#SourceSansPro */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_400.svg#SourceSansPro') format('svg'),
|
||||
/* from https://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7g.ttf */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_400.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src:
|
||||
local('Source Sans Pro SemiBold'),
|
||||
local('SourceSansPro-SemiBold'),
|
||||
/* from https://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlxdu.woff2 */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_600.woff2') format('woff2'),
|
||||
/* from https://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlxdo.woff */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_600.woff') format('woff'),
|
||||
/* from https://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlxdq.eot */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_600.eot?#iefix') format('embedded-opentype'),
|
||||
/* from https://fonts.gstatic.com/l/font?kit=6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlxdp&skey=227a890402fab339&v=v11#SourceSansPro */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_600.svg#SourceSansPro') format('svg'),
|
||||
/* from https://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlxdr.ttf */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_600.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src:
|
||||
local('Source Sans Pro Black'),
|
||||
local('SourceSansPro-Black'),
|
||||
/* from https://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3iu4nwlxdu.woff2 */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_900.woff2') format('woff2'),
|
||||
/* from https://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3iu4nwlxdo.woff */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_900.woff') format('woff'),
|
||||
/* from https://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3iu4nwlxdq.eot */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_900.eot?#iefix') format('embedded-opentype'),
|
||||
/* from https://fonts.gstatic.com/l/font?kit=6xKydSBYKcSV-LCoeQqfX1RYOo3iu4nwlxdp&skey=cf4dfcb31d72fb0a&v=v11#SourceSansPro */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_900.svg#SourceSansPro') format('svg'),
|
||||
/* from https://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3iu4nwlxdr.ttf */
|
||||
url('../fonts/sourcesanspro/Source_Sans_Pro_900.ttf') format('truetype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src:
|
||||
local('Roboto Light'),
|
||||
local('Roboto-Light'),
|
||||
/* from https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fBBc4.woff2 */
|
||||
url('../fonts/roboto/Roboto_300.woff2') format('woff2'),
|
||||
/* from https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fBBc-.woff */
|
||||
url('../fonts/roboto/Roboto_300.woff') format('woff'),
|
||||
/* from https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fBBc8.eot */
|
||||
url('../fonts/roboto/Roboto_300.eot?#iefix') format('embedded-opentype'),
|
||||
/* from https://fonts.gstatic.com/l/font?kit=KFOlCnqEu92Fr1MmSU5fBBc_&skey=11ce8ad5f54705ca&v=v18#Roboto */
|
||||
url('../fonts/roboto/Roboto_300.svg#Roboto') format('svg'),
|
||||
/* from https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fBBc9.ttf */
|
||||
url('../fonts/roboto/Roboto_300.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src:
|
||||
local('Roboto'),
|
||||
local('Roboto-Regular'),
|
||||
/* from https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu4mxK.woff2 */
|
||||
url('../fonts/roboto/Roboto_400.woff2') format('woff2'),
|
||||
/* from https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu4mxM.woff */
|
||||
url('../fonts/roboto/Roboto_400.woff') format('woff'),
|
||||
/* from https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu4mxO.eot */
|
||||
url('../fonts/roboto/Roboto_400.eot?#iefix') format('embedded-opentype'),
|
||||
/* from https://fonts.gstatic.com/l/font?kit=KFOmCnqEu92Fr1Mu4mxN&skey=a0a0114a1dcab3ac&v=v18#Roboto */
|
||||
url('../fonts/roboto/Roboto_400.svg#Roboto') format('svg'),
|
||||
/* from https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu4mxP.ttf */
|
||||
url('../fonts/roboto/Roboto_400.ttf') format('truetype');
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user