123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895/// FOURJS_START_COPYRIGHT(D,2017)
/// Property of Four Js*
/// (c) Copyright Four Js 2017, 2022. All Rights Reserved.
/// * Trademark of Four Js Development Tools Europe Ltd
/// in the United States and elsewhere
///
/// This file can be modified by licensees according to the
/// product manual.
/// FOURJS_END_COPYRIGHT
"use strict";
modulum('FieldWidgetBase', ['TextWidgetBase', 'WidgetFactory'],
function(context, cls) {
/**
* Base class for genero formfield widgets
* @class FieldWidgetBase
* @memberOf classes
* @publicdoc Widgets
* @extends classes.TextWidgetBase
*/
cls.FieldWidgetBase = context.oo.Class(cls.TextWidgetBase, function($super) {
return /** @lends classes.FieldWidgetBase.prototype */ {
__name: "FieldWidgetBase",
/**
* Flag for augmentedFace
* @type {boolean}
*/
__virtual: true,
/**
* List of values through time
* @type {Array}
*/
_valueStack: null,
/**
* Flag to know if the placeholder contain the real one (it could be the comment)
* @type {boolean}
*/
_isFakePlaceholder: true,
/**
* true if widget has pending changes
* @type {boolean}
*/
_editing: false,
/**
* true if widget is readOnly and can't be edited nor focused
* @type {boolean}
* */
_isReadOnly: false,
/**
* the input element
* @protected
* @type {HTMLElement}
*/
_inputElement: null,
/**
* Indicates if key event handlers are bound
*/
_keyEventsBound: false,
/***
* Time of the last widget modification
* @type {number}
*/
_editingTime: 0,
/**
* Position of the current value in the stack
* @type {Number}
*/
_valueStackCursor: -1,
/**
* Exact value from VM
* @type {*}
*/
_vmValue: null,
/**
* Old value, used by typeahead
* @type {?string}
*/
_oldValue: null,
/**
* true if widget should not be editable but navigation is possible
* @type {boolean}
*/
_notEditable: false,
/**
* true if widget requires a value
* @type {boolean}
*/
_required: false,
/**
* true if widget is set as not Null
* @type {boolean}
*/
_notNull: false,
/**
* List of possible values for the widget
* @type {?Array}
*/
_include: null,
/**
* Flag to check if the mouse button is currently pressed
* @type {boolean}
*/
_isMousePressed: false,
/**
* input element state
* @type {cls.InputTextState}
*/
_elementState: null,
/**
* Scroll attribute value
* @type {boolean}
*/
_scroll: null,
/**
* true if must ignore the scroll attribute (equivalent to scroll = 0)
* @type {boolean}
*/
_dataTypeWithNoScroll: false,
/**
* Maximum number of characters allowed. By default -1 indicates no limit.
* @type {number}
*/
_maxLength: -1,
/**
* widget VM width
* @type {number}
*/
_vmWidth: 0,
/**
* true if we are between a key down and key yp event
* @type {boolean}
*/
_processingKeyEvent: null,
/**
* true if a picture is define on this field
* @type {boolean}
*/
_pictureDefined: null,
/**
* @constructs
* @inheritDoc
*/
constructor: function(opts) {
$super.constructor.call(this, opts);
this.setEnabled(false, true);
},
/**
* @inheritDoc
*/
_initElement: function() {
$super._initElement.call(this);
this._valueStack = [];
if (window.isMobile()) {
var inputElement = this._element.getElementsByTagName('input')[0];
if (inputElement) {
// Track the focus and mouse down/up events on mobile devices to handle the virtual keybord's TAB key
inputElement.on('focus.FieldWidgetBase', this._onMobileFocus.bind(this));
inputElement.on('mousedown.FieldWidgetBase', this._onMobileMouseDown.bind(this));
inputElement.on('mouseup.FieldWidgetBase', this._onMobileMouseUp.bind(this));
}
}
this._elementState = new cls.InputTextState();
this._processingKeyEvent = false;
this._pictureDefined = false;
},
/**
* @inheritDoc
*/
destroy: function() {
$super.destroy.call(this);
this._valueStack = null;
this._oldValue = null;
this.unbindKeyEvents();
this._keyEventsBound = false;
if (this._inputElement && this.isNotEditable()) {
this._inputElement.off('drop.FieldWidgetBase_notEditable');
}
if (this._inputElement && window.isMobile()) {
this._inputElement.off('focus.FieldWidgetBase');
this._inputElement.off('mousedown.FieldWidgetBase');
this._inputElement.off('mouseup.FieldWidgetBase');
}
this._inputElement = null;
this._elementState = null;
},
/**
* @inheritDoc
*/
_afterInitElement: function() {
$super._afterInitElement.call(this);
this.bindKeyEvents();
this._keyEventsBound = true;
},
/**
* Get the input part of the widget
* @return {HTMLElement} the input part of the widget
* @publicdoc
*/
getInputElement: function() {
return this._inputElement;
},
/**
* Check if the widget has an input element
* @return {boolean} true if widget has an input element
* @publicdoc
*/
hasInputElement: function() {
return Boolean(this.getInputElement());
},
/**
* Bind all keys events of the widget (done when the widget becomes active)
* @protected
*/
bindKeyEvents: function() {},
/**
* Unbind all keys events of the widget (done when the widget becomes inactive or in typeahead)
* @protected
*/
unbindKeyEvents: function() {},
/**
* Set the value of widget
* @param {string|number} value - sets the value to display
* @param {boolean} [fromVM] - true if value comes from the VM
* @publicdoc
*/
setValue: function(value, fromVM) {
if (this.hasCursors() && !fromVM) { // only widgets with cursors manage undo/redo
this._valueStack.push(value);
this._valueStackCursor++;
}
if (fromVM) {
this._vmValue = value;
if (this.getValue() !== value) {
this._valueStack = [value];
this._valueStackCursor = 0;
} else {
this._valueStack.push(value);
this._valueStackCursor++;
}
this._oldValue = value;
}
if (this._valueStack.length > 30) {
this._valueStack.shift();
this._valueStackCursor--;
}
},
/**
* Internal setValue to change value without any event emited
* @param {string} value - the value
* @private
*/
_setValue: function(value) {
if (this._inputElement) {
this._inputElement.value = value;
}
},
/**
* Handle input event to manage
* - set editing
* - shift attribute (textTransform !== none)
* @private
*/
_onInput: function() {
if (this.isNotEditable()) {
// If not editable, rollback to old value (the initial one)
this._inputElement.value = this._oldValue;
} else {
this._editingTime = Date.now();
this.setEditing(this.isEditing() || this.getValue() !== this._oldValue);
if (this.isEditing() && this._textTransform !== 'none' && this.hasInputElement()) {
var start = this._inputElement.selectionStart;
var end = this._inputElement.selectionEnd;
this._inputElement.value = this.getValue();
this._inputElement.setCursorPosition(start, end);
}
}
},
/**
* Handle drop event
* @param evt
* @private
*/
_onDrop: function(evt) {
if (this.isNotEditable()) {
evt.preventCancelableDefault();
}
},
/**
* NotEditable allows cursor moving, but not a value change
* @param {boolean} notEditable - true to set the edit part as read-only
*/
setNotEditable: function(notEditable) {
this._notEditable = notEditable;
if (this._inputElement) {
if (notEditable) {
this._inputElement.on('drop.FieldWidgetBase_notEditable', this._onDrop.bind(this));
} else {
this._inputElement.off('drop.FieldWidgetBase_notEditable');
}
}
},
/**
* NotEditable allows cursor moving, but not a value change
* @return {boolean} true if the edit part is not editable
*/
isNotEditable: function() {
return this._notEditable;
},
/**
* Set the widget validation to 'required'
* @param {boolean} required - true if a value is required
*/
setRequired: function(required) {
this._required = required;
this.toggleClass("gbc_Required", required);
},
/**
* Verify if the widget value is required
* @return {boolean} true if a value is required
*/
isRequired: function() {
return this._required;
},
/**
* Verify if the placeholder is the real one
* @return {boolean} true if it is a fake placeholder
*/
isFakePlaceholder: function() {
return this._isFakePlaceholder;
},
/**
* Set the widget validation to noNull
* @param {boolean} notNull - false if the widget value can be null, true otherwise
*/
setNotNull: function(notNull) {
this._notNull = notNull;
this.toggleClass("gbc_NotNull", notNull);
},
/**
* Verify if the widget can be null
* @return {boolean} false if the widget value can be null, true otherwise
*/
isNotNull: function() {
return this._notNull;
},
/**
* Get the list of allowed values defined by INCLUDE list
* @param {Array|null} include - list of allowed values or null if not defined
*/
setAllowedValues: function(include) {
this._include = include;
},
/**
* Get the list of allowed values defined by INCLUDE list
* @return {Array|null} list of allowed values or null if not defined
*/
getAllowedValues: function() {
return this._include;
},
/**
* Prevent value change but allow navigation
* @param {Event} evt the browser event
* @param {string} keyString the string representation of the key sequence
* @private
*/
_preventEditAllowNavigation: function(evt, keyString) {
var prevent = ["ctrl+x", "ctrl+v", "meta+x", "meta+v"].contains(keyString); // CTRL+X & CTRL+V forbidden
prevent = prevent || (["tab", "home", "end", "left", "right", "up", "down", "shift+left", "shift+right", "ctrl+c",
"ctrl+a",
"meta+c", "meta+a"
].contains(
keyString) === false);
if (prevent) {
evt.preventCancelableDefault();
this.flash();
}
},
/**
* Get the value of the widget
* @returns {?string|number} the value
* @publicdoc
*/
getValue: function() {
return null;
},
/**
* @inheritDoc
*/
getClipboardValue: function() {
return this.getValue();
},
/**
* Define the widget as readonly or not
* @param {boolean} readonly - true to set the widget as readonly without possibility of edition, false otherwise
* @publicdoc
*/
setReadOnly: function(readonly) {
this._isReadOnly = readonly;
},
/**
* Check if the widget is readonly or not
* @returns {boolean} true if the widget is readonly, false otherwise
* @publicdoc
*/
isReadOnly: function() {
return this._isReadOnly;
},
/**
* @returns {number} time of the last widget modification
*/
getEditingTime: function() {
return this._editingTime;
},
/**
* Check if widget is currently editing
* @return {boolean}
*/
isEditing: function() {
return this._editing;
},
/**
* Flag or unflag widget as having value pending changes
* @param editing {boolean} the new editing state
* @publicdoc
*/
setEditing: function(editing) {
this._editing = editing;
if (this.getElement()) {
this.getElement().toggleClass("editing", Boolean(editing));
}
},
/**
* Returns if the widget is focusable
* @return {boolean} State of focusable
* @publicdoc
*/
isFocusable: function() {
return this.hasInputElement() || $super.isFocusable.call(this);
},
/**
* Tests if the widget has really the DOM focus (check document.activeElement)
* @returns {boolean} true if the widget has the DOM focus
* @publicdoc
*/
hasDOMFocus: function() {
return (this.hasInputElement() && this.getInputElement() === document.activeElement) ||
$super.hasDOMFocus.call(this);
},
/**
* Defines the enabled status of the widget
* @param {boolean} enabled true if the widget allows user interaction, false otherwise.
* @publicdoc
*/
setEnabled: function(enabled, noSelectionUpdate) {
if (this._enabled !== Boolean(enabled)) {
this._enabled = Boolean(enabled);
this.domAttributesMutator(function(noSelectionUpdate) {
if (this._enabled) {
this.removeClass("disabled");
if (this.hasInputElement() && !this.isReadOnly()) {
this.getInputElement().removeAttribute("readonly");
}
} else {
this.addClass("disabled");
if (!noSelectionUpdate) {
if (this.hasCursors()) {
this.setCursors(0);
this.afterDomMutator(function() {
var selection = window.getSelection();
if (selection) {
var hasTextSelection = selection.focusNode === this._element;
if (hasTextSelection) {
selection.removeAllRanges();
}
}
}.bind(this));
}
}
if (this.hasInputElement()) {
this.getInputElement().setAttribute("readonly", "readonly");
}
}
}.bind(this, noSelectionUpdate));
}
// bind/unbind keys events
if (enabled && (this.isNotEditable && !this.isNotEditable() || this.isReadOnly && !this.isReadOnly())) {
if (!this._keyEventsBound) {
this._keyEventsBound = true;
this.bindKeyEvents();
}
} else {
if (this._keyEventsBound) {
this._keyEventsBound = false;
this.unbindKeyEvents();
}
}
},
/**
* @inheritDoc
*/
loseVMFocus: function(vmNewFocusedWidget = null) {
$super.loseVMFocus.call(this, vmNewFocusedWidget);
this.setEditing(false);
},
/**
* @inheritDoc
*/
loseFocus: function() {
$super.loseFocus.call(this);
this.setEditing(false);
},
/**
* @inheritDoc
*/
managePriorityKeyDown: function(keyString, domKeyEvent, repeat) {
var keyProcessed = false;
if (this.isEnabled() && this.hasCursors()) {
if (keyString === "home") {
this.setCursors(0);
keyProcessed = true;
} else if (keyString === "end") {
this.setCursors(this.getValue() && this.getValue().toString().length || 0);
keyProcessed = true;
}
}
if (keyProcessed) {
return true;
} else {
return $super.managePriorityKeyDown.call(this, keyString, domKeyEvent, repeat);
}
},
/**
* @inheritDoc
*/
manageKeyDown: function(keyString, domKeyEvent, repeat) {
var keyProcessed = false;
this._processingKeyEvent = true;
if (this.isEnabled() && !this.isReadOnly()) {
if (keyString === "ctrl+z" || keyString === 'meta+z') {
this.undo();
keyProcessed = true;
} else if (keyString === "ctrl+shift+z" || keyString === 'meta+shift+z') {
this.redo();
keyProcessed = true;
}
if (this.isNotEditable()) {
this._preventEditAllowNavigation(domKeyEvent, keyString);
}
}
if (keyProcessed) {
return true;
} else {
return $super.manageKeyDown.call(this, keyString, domKeyEvent, repeat);
}
},
/**
* @inheritDoc
*/
manageKeyUp: function(keyString, domKeyEvent) {
$super.manageKeyUp.call(this, keyString, domKeyEvent);
this._processingKeyEvent = false;
},
/**
* Cancel the last value
*/
undo: function() {
if (this.hasCursors()) { // only widgets with cursors manage undo/redo
var cursors = this.getCursors();
var prevValue = this.getValue();
//go back but store the current as last known value
if (this._valueStackCursor === this._valueStack.length - 1 && this._valueStack[this._valueStack.length - 1] !==
this.getValue()) {
this.setValue(this.getValue());
}
this._valueStackCursor--;
this._valueStackCursor = this._valueStackCursor < 0 ? 0 : this._valueStackCursor;
this.afterDomMutator(function() {
var val = this._valueStack[this._valueStackCursor];
if (typeof val === "string" && this.hasInputElement()) {
this._setValue(val);
var diff = prevValue.length - val.length;
this.setCursors(cursors.start - diff);
}
}.bind(this));
}
},
/**
* Restore the last value
*/
redo: function() {
if (this.hasCursors()) { // only widgets with cursors manage undo/redo
var cursors = this.getCursors();
var prevValue = this.getValue();
if (this._valueStackCursor < this._valueStack.length - 1) {
this._valueStackCursor++;
}
this.afterDomMutator(function() {
var val = this._valueStack[this._valueStackCursor];
if (typeof val === "string" && this.hasInputElement()) {
this._setValue(val);
var diff = prevValue.length - val.length;
this.setCursors(cursors.start - diff);
}
}.bind(this));
}
},
/**
* @inheritDoc
*/
buildExtraContextMenuActions: function(contextMenu) {
$super.buildExtraContextMenuActions.call(this, contextMenu);
if (!this.isReadOnly() && !this.isInTable() && this.hasInputElement()) {
var selectAllAllowed = this.getValue().length > 0;
contextMenu.addAction("selectAll", i18next.t("gwc.contextMenu.selectAll"), "font:FontAwesome.ttf:f0ea", "Ctrl+A", {
clickCallback: function() {
contextMenu.hide();
this.setFocus();
this.selectAllInputText();
}.bind(this),
disabled: !selectAllAllowed
}, true);
}
},
/**
* Select all the text in the input element
* @publicdoc
*/
selectAllInputText: function() {
if (this.hasInputElement()) {
var cursor2 = this.getValue() && this.getValue().length || 0;
this._inputElement.setCursorPosition(0, cursor2);
}
},
/**
* Defines a placeholder text
* @param {string} placeholder - placeholder text
* @param {boolean} fake - true if placeholder come from another attribute
* @publicdoc
*/
setPlaceHolder: function(placeholder, fake) {
if (this.hasInputElement()) {
this._isFakePlaceholder = fake ? true : false;
if (placeholder) {
this._inputElement.setAttribute('placeholder', placeholder);
} else {
this._inputElement.removeAttribute('placeholder');
}
}
},
/**
* Method used to validate or not the value, this trigger a rollback if not valid when sending
* the value to the VM
* @return {boolean} - true if valid, false otherwise
*/
validateValue: function() {
// Implement your own method on widgets
return true;
},
/**
* This function requests the VM focus if this focus event hasn't been triggered
* by a mouse or touch event.
* This happens when the user presses the TAB key of a mobile's virtual keyboard.
* - TAB generates only a focus event
* - A tap or click generates a mousedown, focus and mouseup events
* @param {FocusEvent} event HTML focus event
* @private
*/
_onMobileFocus: function(event) {
// Skip event if the focus is given by FocusApplicationService._transferFocusToNode for this widget
if (!this._isMousePressed && cls.FocusApplicationService.getCurrentTransferFocusWidget() !== this) {
this._onRequestFocus(event);
}
},
/**
* @param {MouseEvent} event HTML mouse event
* @private
*/
_onMobileMouseDown: function(event) {
this._isMousePressed = true;
},
/**
* @param {MouseEvent} event HTML mouse event
* @private
*/
_onMobileMouseUp: function(event) {
this._isMousePressed = false;
},
/**
* Fix the char full/half char size according to the widget field width
* @param {string} text - widget value
* @param {string} newTextPart - new text part to verify
* @return {string} a valid newTextPart
*/
checkValueDisplayWidth: function(text, newTextPart) {
if (this._maxLength <= 0 && this._vmWidth <= 0) {
return newTextPart;
}
var displayWidth = this._vmWidth;
var maxLength = this.getUserInterfaceWidget().isCharLengthSemantics() ? this._maxLength : -1;
var fullText = text + newTextPart;
var textLength = Array.from(text).length;
var res = newTextPart;
var codepoints = Array.from(newTextPart);
if (fullText.displayWidth() > displayWidth || (maxLength > 0 && (textLength + codepoints.length) > maxLength)) {
do {
codepoints.pop();
res = codepoints.join('');
fullText = text + res;
} while (codepoints.length > 0 && fullText.displayWidth() > displayWidth ||
(maxLength > 0 && (textLength + codepoints.length) > maxLength));
}
return res;
},
/**
* Fix the newTextPart according to the requested byte length
* @param {string} text - old part of the widget value
* @param {string} newTextPart - new text part to verify
* @param {number} bytes - requested max bytes length
* @return {string} a valid newTextPart
*/
checkValueByteCount: function(text, newTextPart, bytes) {
var fullText = text + newTextPart;
if (fullText.length === 0 || fullText.countBytes() <= bytes) {
return newTextPart;
}
if (String.isSingleByteEncoding()) {
return newTextPart.substr(0, this._maxLength - text.length);
}
var codepoints = Array.from(newTextPart);
var textLength = text.countBytes();
var res = '';
do {
codepoints.pop();
res = codepoints.join('');
} while (codepoints.length > 0 && (textLength + res.countBytes()) > bytes);
return res;
},
/**
* @return {boolean} true if we must ignore the scroll attribute
*/
isDataTypeWithNoScroll: function() {
return this._dataTypeWithNoScroll;
},
/**
* Set to true if we must ignore the scroll attribute
* @param {boolean} checkDisplayValue
*/
setDataTypeWithNoScroll: function(checkDisplayValue) {
this._dataTypeWithNoScroll = checkDisplayValue;
},
/**
* Widget VM width
* @param {number} width
*/
setVMWidth: function(width) {
this._vmWidth = width;
},
/**
* return true if the backup value has been restored
* @return {boolean}
*/
isValueRestored: function() {
return this._elementState.isRestored();
},
/**
* Set if a picture is defined on this field
* @param {boolean} defined
*/
setPictureDefined: function(defined) {
this._pictureDefined = defined;
},
/**
* True if a picture is defined on this field
* @return {boolean}
*/
isPictureDefined: function() {
return this._pictureDefined;
},
/**
* Get the element state
* @return {cls.InputTextState}
*/
getElementState: function() {
return this._elementState;
}
};
});
});