import $ from '$qui/lib/jquery.module.js'
import {AssertionError} from '$qui/base/errors.js'
import {Mixin} from '$qui/base/mixwith.js'
import * as Toast from '$qui/messages/toast.js'
/**
* @alias qui.views.STATE_NORMAL
*/
export const STATE_NORMAL = 'normal'
/**
* @alias qui.views.STATE_WARNING
*/
export const STATE_WARNING = 'warning'
/**
* @alias qui.views.STATE_ERROR
*/
export const STATE_ERROR = 'error'
/**
* @alias qui.views.STATE_PROGRESS
*/
export const STATE_PROGRESS = 'progress'
/** @lends qui.views.ViewMixin */
const ViewMixin = Mixin((superclass = Object) => {
/**
* A mixin to be used with classes that behave as views (have a visual HTML representation).
* @alias qui.views.ViewMixin
* @mixin
*/
class ViewMixin extends superclass {
/**
* @constructs
* @param {String} [cssClass] additional CSS classes to set to the view element
* @param {...*} args parent class parameters
*/
constructor({cssClass = null, ...args} = {}) {
super(args)
/* ViewMixin is prepared to be initialized multiple times along the inheritance path;
* this may happen when inheriting ViewMixin more than once through mixins */
if (this._html === undefined) {
this._html = null
}
if (this._state === undefined) {
this._state = STATE_NORMAL
this._warningMessage = null
this._errorMessage = null
this._progressPercent = null
}
if (!this._cssClass) {
this._cssClass = cssClass
}
else {
this._cssClass += ` ${cssClass}`
}
}
/* HTML */
/**
* Create the HTML element of this view.
* @abstract
* @returns {jQuery}
*/
makeHTML() {
return $('<div></div>')
}
/**
* Override this to further initialize the HTML element of this view.
* @param {jQuery} html the HTML element to be initialized
*/
initHTML(html) {
}
/**
* Return the HTML element of this view.
* @returns {jQuery}
*/
getHTML() {
if (this._html == null) {
this._html = this.makeHTML()
this.initHTML(this._html)
if (this._cssClass) {
this._html.addClass(this._cssClass)
}
this.init()
}
return this._html
}
/**
* Tells if the HTML element of this view has been created.
* @return {Boolean}
*/
hasHTML() {
return this._html != null
}
/**
* Initialize the view. Called after the HTML has been created and initialized.
*/
init() {
}
/* State */
/**
* Update the current state.
* @param {String} state the desired state
*/
setState(state) {
/* Make sure we have the HTML created before switching states */
this.getHTML()
if (this._state !== state) {
this.leaveState(this._state, state)
}
let oldState = this._state
this._state = state
this.enterState(oldState, state)
}
/**
* Return the current view state.
* @returns {String}
*/
getState() {
return this._state
}
/**
* Define the behavior of the view when entering states. Entering a state usually means showing a visual element
* corresponding to that state.
* @param {String} oldState
* @param {String} newState
*/
enterState(oldState, newState) {
switch (newState) {
case STATE_NORMAL:
break
case STATE_WARNING:
this.showWarning(this._warningMessage)
break
case STATE_ERROR:
this.showError(this._errorMessage)
break
case STATE_PROGRESS:
this.showProgress(this._progressPercent)
break
}
}
/**
* Define the behavior of the view when leaving states. Leaving a state usually means hiding a visual element
* corresponding to that state.
* @param {String} oldState
* @param {String} newState
*/
leaveState(oldState, newState) {
switch (oldState) {
case STATE_NORMAL:
break
case STATE_WARNING:
this._warningMessage = null
this.hideWarning()
break
case STATE_ERROR:
this._errorMessage = null
this.hideError()
break
case STATE_PROGRESS:
this._progressPercent = null
this.hideProgress()
break
}
}
/* Progress */
/**
* Put the view in the progress state or updates the progress. The view state is set to
* {@link qui.views.STATE_PROGRESS}.
*
* It is safe to call this method multiple times, updating the progress percent.
*
* @param {?Number} [percent] optional progress percent (from `0` to `100`); `null` indicates indefinite
* progress
*/
setProgress(percent = null) {
/* No duplicate setProgress() protection, since we want to be able to update the progress */
this._progressPercent = percent
this.setState(STATE_PROGRESS)
}
/**
* Put the view in the normal state, but only if the current state is {@link qui.views.STATE_PROGRESS}.
*/
clearProgress() {
if (this._state === STATE_PROGRESS) {
this.setState(STATE_NORMAL)
}
}
/**
* Return the current progress percent.
*
* An exception will be thrown if the current view state is not {@link qui.views.STATE_PROGRESS}.
*
* @returns {?Number}
*/
getProgressPercent() {
if (this._state !== STATE_PROGRESS) {
throw new AssertionError(`getProgressPercent() called in ${this._state} state`)
}
return this._progressPercent
}
/**
* Define how the view is displayed in progress state, disallowing any user interaction.
*
* {@link qui.views.ViewMixin#inProgress} returns `true` when called from this method.
*
* Does nothing by default. Override this method to implement your own progress display.
*
* @param {?Number} [percent] optional progress percent (from `0` to `100`); `null` indicates indefinite
* progress
*/
showProgress(percent) {
}
/**
* Hide a previously displayed progress, re-enabling user interaction.
*
* {@link qui.views.ViewMixin#inProgress} returns `false` when called from this method.
*
* Does nothing by default. Override this method to implement your own progress hiding.
*/
hideProgress() {
}
/**
* Tell if the view is currently in progress (its state is {@link qui.views.STATE_PROGRESS}).
* @returns {Boolean}
*/
inProgress() {
return this._state === STATE_PROGRESS
}
/* Warning */
/**
* Put the view in the warning state or updates the warning message. The view state is set to
* {@link qui.views.STATE_WARNING}.
*
* It is safe to call this method multiple times, updating the warning message.
*
* @param {?String} [message] an optional warning message
*/
setWarning(message = null) {
/* No duplicate setWarning() protection, since we want to be able to update the error */
this._warningMessage = message
this.setState(STATE_WARNING)
}
/**
* Put the view in the normal state, but only if the current state is {@link qui.views.STATE_WARNING}.
*/
clearWarning() {
if (this._state === STATE_WARNING) {
this.setState(STATE_NORMAL)
}
}
/**
* Return the warning message set with {@link qui.views.ViewMixin#setWarning} or `null` if no warning message
* was set.
*
* An exception will be thrown if the current view state is not {@link qui.views.STATE_WARNING}.
*
* @returns {?String}
*/
getWarningMessage() {
if (this._state !== STATE_WARNING) {
throw new AssertionError(`getWarningMessage() called in ${this._state} state`)
}
return this._warningMessage
}
/**
* Define how the view is displayed in warning state, showing the warning message.
*
* {@link qui.views.ViewMixin#hasWarning} returns `true` when called from this method.
*
* By default displays a warning toast message. Override this method to implement your own warning display.
*
* @param {?String} message the warning message, or `null` if no warning message available
*/
showWarning(message) {
Toast.show({message: message, type: 'error', timeout: 0})
}
/**
* Hide a previously displayed warning.
*
* {@link qui.views.ViewMixin#hasWarning} returns `false` when called from this method.
*
* By default calls {@link qui.messages.toast.hide}. Override this method to implement your own warning
* hiding.
*/
hideWarning() {
Toast.hide()
}
/**
* Tell if the view is currently in the warning state (its state is {@link qui.views.STATE_WARNING}).
* @returns {Boolean}
*/
hasWarning() {
return this._state === STATE_WARNING
}
/* Error */
/**
* Put the view in the error state or updates the error message.
*
* The view state is set to {@link qui.views.STATE_ERROR}.
*
* It is safe to call this method multiple times, updating the error message.
*
* @param {?String|Error} [message] an error message
*/
setError(message = null) {
/* No duplicate setError() protection, since we want to be able to update the error */
if (message instanceof Error) {
message = message.message
}
this._errorMessage = message
this.setState(STATE_ERROR)
}
/**
* Put the view in the normal state, but only if the current state is {@link qui.views.STATE_ERROR}.
*/
clearError() {
if (this._state === STATE_ERROR) {
this.setState(STATE_NORMAL)
}
}
/**
* Return the error message set with {@link qui.views.ViewMixin#setError} or `null` if no error message was set.
*
* An exception will be thrown if the current view state is not {@link qui.views.STATE_ERROR}.
*
* @returns {?String}
*/
getErrorMessage() {
if (this._state !== STATE_ERROR) {
throw new AssertionError(`getErrorMessage() called in ${this._state} state`)
}
return this._errorMessage
}
/**
* Define how the view is displayed in error state, showing the error message.
*
* {@link qui.views.ViewMixin#hasError} returns `true` when called from this method.
*
* By default displays a error toast message. Override this method to implement your own error display.
*
* @param {?String} message the error message, or `null` if no error message available
*/
showError(message) {
Toast.show({message: message, type: 'error', timeout: 0})
}
/**
* Hide a previously displayed error.
*
* {@link qui.views.ViewMixin#hasError} returns `false` when called from this method.
*
* By default calls {@link qui.messages.toast.hide}. Override this method to implement your own error
* hiding.
*/
hideError() {
Toast.hide()
}
/**
* Tell if the view is currently in the error state (its state is {@link qui.views.STATE_ERROR}).
* @returns {Boolean}
*/
hasError() {
return this._state === STATE_ERROR
}
/* Closing */
/**
* Tell if the view has been closed.
*
* By default returns `false`.
*
* Override this method to implement your own closed status.
*
* @returns {Boolean}
*/
isClosed() {
return false
}
/**
* Close the view.
*
* By default does nothing.
*
* Override this method to implement your own close behavior.
*/
close() {
}
}
return ViewMixin
})
export default ViewMixin