ReactJS handle tab character in textarea - html

How can I handle tab key pressed events in ReactJS so I'm able to indent text inside a textarea?
onChange event does not get fired when tab is pressed on the textarea, so I guess there might be a higher level handler I can use to detect this event.

onKeyDown={(e: React.KeyboardEvent) => {
if (e.key === 'Tab' && !e.shiftKey) {
e.preventDefault();
const value = this.textareaRef.current!.value;
const selectionStart = this.textareaRef.current!.selectionStart;
const selectionEnd = this.textareaRef.current!.selectionEnd;
this.textareaRef.current!.value =
value.substring(0, selectionStart) + ' ' + value.substring(selectionEnd);
this.textareaRef.current!.selectionStart = selectionEnd + 2 - (selectionEnd - selectionStart);
this.textareaRef.current!.selectionEnd = selectionEnd + 2 - (selectionEnd - selectionStart);
}
if (e.key === 'Tab' && e.shiftKey) {
e.preventDefault();
const value = this.textareaRef.current!.value;
const selectionStart = this.textareaRef.current!.selectionStart;
const selectionEnd = this.textareaRef.current!.selectionEnd;
const beforeStart = value
.substring(0, selectionStart)
.split('')
.reverse()
.join('');
const indexOfTab = beforeStart.indexOf(' ');
const indexOfNewline = beforeStart.indexOf('\n');
if (indexOfTab !== -1 && indexOfTab < indexOfNewline) {
this.textareaRef.current!.value =
beforeStart
.substring(indexOfTab + 2)
.split('')
.reverse()
.join('') +
beforeStart
.substring(0, indexOfTab)
.split('')
.reverse()
.join('') +
value.substring(selectionEnd);
this.textareaRef.current!.selectionStart = selectionStart - 2;
this.textareaRef.current!.selectionEnd = selectionEnd - 2;
}
}
}}

you can try onKeyDown and get the keycode for tab.
add: function(event){
console.log(event.keyCode); //press TAB and get the keyCode
},
render: function(){
return(
<div>
<input type="text" id="one" onKeyDown={this.add} />
</div>
);
}

Just in case someone wants a slightly updated and (in my opinion, enhanced) React Hooks version of vipe's solution in TypeScript:
Implementation Usage Example:
<EnhancedTextArea
ref={txtCodeInput} {/* reference used whenever required as seen below */}
className='code-input'
tabSize={2}
onTextChange={handleCodeChange} {/* Function accepting callback of type (string) -> void, called every time code is changed */}
/>
Getting Text:
const txtCodeInput = useRef<EnhancedTextAreaRefs>(null);
...
const codeContent = txtCodeInput.current?.getCodeContent();
EnhancedTextArea.tsx:
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
type EnhancedTextAreaProps = {
onTextChange?: (text: string) => void,
className?: string,
spellCheck?: boolean,
tabSize?: number,
};
export type EnhancedTextAreaRefs = {
getCodeContent: () => string;
}
const EnhancedTextArea = forwardRef<EnhancedTextAreaRefs, EnhancedTextAreaProps>(({
onTextChange = undefined,
className = undefined,
tabSize = 4,
spellCheck = false,
}: EnhancedTextAreaProps, ref) => {
const [text, setText] = useState('');
const [stateSelectionStart, setStateSelectionStart] = useState(0);
const [stateSelectionEnd, setStateSelectionEnd] = useState(0);
const txtInput = useRef<HTMLTextAreaElement>(null);
useImperativeHandle(ref, () => ({
getCodeContent: () => text,
}));
useEffect(() => {
const textArea = txtInput.current;
if (!textArea) {
return;
}
if (stateSelectionStart >= 0) {
textArea.selectionStart = stateSelectionStart;
}
if (stateSelectionEnd >= 0) {
textArea.selectionEnd = stateSelectionEnd;
}
}, [text, stateSelectionStart, stateSelectionEnd]);
async function handleCodeChange(e: React.ChangeEvent<HTMLTextAreaElement>): Promise<void> {
const text = e.target.value;
setText(text);
if (onTextChange) {
onTextChange(text);
}
}
async function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>): Promise<void> {
const textArea = e.target as HTMLTextAreaElement;
const tabString = ' '.repeat(tabSize);
const value = textArea.value;
const selectionStart = textArea.selectionStart;
const selectionEnd = textArea.selectionEnd;
if (e.key === 'Tab' && !e.shiftKey) {
e.preventDefault();
if (selectionStart !== selectionEnd) {
const slices1 = getNewLineSlices(value, selectionStart, selectionEnd);
const newValue1 = addTabs(value, slices1, tabString);
setText(newValue1);
setStateSelectionStart(selectionStart + tabSize);
setStateSelectionEnd(selectionEnd + (newValue1.length - value.length));
} else {
const newValue2 = value.substring(0, selectionStart) + tabString + value.substring(selectionEnd);
setText(newValue2);
setStateSelectionStart(selectionEnd + tabSize - (selectionEnd - selectionStart));
setStateSelectionEnd(selectionEnd + tabSize - (selectionEnd - selectionStart));
}
} else if (e.key === 'Tab' && e.shiftKey) {
e.preventDefault();
const slices2 = getNewLineSlices(value, selectionStart, selectionEnd);
const newValue3 = removeTabs(value, slices2, tabSize);
const diff = value.length - newValue3.length;
setText(newValue3);
setStateSelectionStart(Math.max(0, selectionStart - (diff ? tabSize : 0)));
setStateSelectionEnd(Math.max(0, selectionEnd - diff));
} else {
setStateSelectionStart(-1);
setStateSelectionEnd(-1);
}
}
function getNewLineSlices(value: string, selectionStart: number, selectionEnd: number): Array<string | null> {
const newLineLocations = getAllIndices(value, '\n');
const left = findRange(newLineLocations, selectionStart);
const split = value.split('\n');
const arr = [];
let count = 0;
for (let i = 0; i < split.length; i++) {
const line = split[i];
if (count > left && count <= selectionEnd) {
arr.push(line);
} else {
arr.push(null);
}
count += line.length + 1;
}
return arr;
}
function addTabs(value: string, arr: Array<string | null>, joiner: string): string {
const split = value.split('\n');
let ret = '';
for (let i = 0; i < split.length; i++) {
const val = split[i];
const newLineVal = arr[i];
if (newLineVal === val) {
ret += joiner;
}
ret += val;
if (i !== split.length - 1) {
ret += '\n';
}
}
return ret;
}
function removeTabs(value: string, arr: Array<string | null>, tabSize: number): string {
const split = value.split('\n');
let ret = '';
for (let i = 0; i < split.length; i++) {
const val = split[i];
const newLineVal = arr[i];
if (!val.startsWith(' ') || newLineVal !== val) {
ret += val;
if (i !== split.length - 1) {
ret += '\n';
}
continue;
}
let count = 1;
while (val[count] === ' ' && count < tabSize) {
count++;
}
ret += val.substring(count);
if (i !== split.length - 1) {
ret += '\n';
}
}
return ret;
}
function getAllIndices(arr: string, val: string): Array<number> {
const indices = [];
let i = -1;
while ((i = arr.indexOf(val, i + 1)) !== -1){
indices.push(i);
}
return indices;
}
function findRange(arr: Array<number>, min: number): number {
for (let i = 0; i < arr.length; i++) {
if (arr[i] >= min) {
return i === 0 ? -1 : arr[i - 1];
}
}
return arr[arr.length - 1];
}
return(
<textarea
ref={txtInput}
value={text}
onKeyDown={handleKeyDown}
onChange={handleCodeChange}
className={className}
spellCheck={spellCheck} />
);
});
EnhancedTextArea.displayName = 'EnhancedTextArea';
export default EnhancedTextArea;

My solution using useRef() for functional component :
const codeAreaRef = useRef();
const [code, setCode] = useState('');
//--------------
<textarea
name='code'
value={code}
ref={codeAreaRef}
onChange={(e) => {
setCode(e.target.value);
}}
onKeyDown={(e) => {
if (e.key == 'Tab') {
e.preventDefault();
const { selectionStart, selectionEnd } = e.target;
const newText =
code.substring(0, selectionStart) +
' ' + // Edit this for type tab you want
// Here it's 2 spaces per tab
code.substring(selectionEnd, code.length);
codeAreaRef.current.focus();
codeAreaRef.current.value = newText;
codeAreaRef.current.setSelectionRange(
selectionStart + 2,
selectionStart + 2
);
setCode(newText);
}
}}
/>

Related

input cursor is not staying where it is placed it is going again at the last of input text

here is the code:
const [expression, setExpression] = useState("");
const Remove = () => {
const input = document.getElementById("input");
if (expression.length > 0) {
const pos = input.selectionStart;
if (pos > 0) {
setExpression(expression.substring(0, pos - 1) + expression.substring(pos));
input.focus();
input.selectionStart = pos - 1;
input.selectionEnd = pos - 1;
}
}
}
return (
<>
<input id="input" value={expression} />
<button onClick={Remove}>remove</button>
</>
)
I want to remove the text where cursor is place in input box on button click but the cursor goes again and again at the end of input text after removing the number. I want it to stay where it is placed in input field and not go at the end of input box text.
You can do it in a stateful and stateless way.
Stateful
const [expression, setExpression] = useState("");
const [cursorPos, setCursorPos] = useState(0);
useEffect(() => {
const input = document.getElementById("input");
input.focus();
input.setSelectionRange(cursorPos, cursorPos);
}, [cursorPos, expression]);
const handleInput = (e) => {
setExpression(e.target.value);
const input = document.getElementById("input");
setCursorPos(input.selectionStart);
}
const Remove = () => {
const input = document.getElementById("input");
if (expression.length > 0) {
const pos = input.selectionStart;
if (pos > 0) {
setExpression(
expression.substring(0, pos - 1) + expression.substring(pos)
);
setCursorPos(pos - 1);
}
}
};
return (
<>
<input id="input" value={expression} onInput={handleInput} />
<button onClick={Remove}>remove</button>
</>
);
Stateless, using ref
const inputRef = useRef();
const Remove = () => {
const input = inputRef.current;
const value = input.value;
if (value.length > 0) {
const pos = input.selectionStart;
if (pos > 0) {
input.value = value.substring(0, pos - 1) + value.substring(pos);
input.focus();
input.selectionStart = pos - 1;
input.selectionEnd = pos - 1;
}
}
};
return (
<>
<input id="input" ref={inputRef} />
<button onClick={Remove}>remove</button>
</>
);

Using Forge Javascript-based Extension in Angular app

How do i use a Jasvscript-based Extension, for example the IconMarkupExtension from https://forge.autodesk.com/blog/placing-custom-markup-dbid in my Angular-based app.
I tried the following:
Import the Javascript file:
import IconMarkupExtension from './IconMarkupExtension';
using the extension by defining in the viewerConfig:
constructor(private router: Router, private auth: AuthService, private api: ApiService, private messageService: MessageService) {
this.viewerOptions3d = {
initializerOptions: {
env: 'AutodeskProduction',
getAccessToken: (async (onGetAccessToken) => {
const authToken: AuthToken = await this.api.get2LToken();
this.auth.currentUserValue.twolegggedToken = authToken.access_token;
onGetAccessToken(this.auth.currentUserValue.twolegggedToken, 30 * 60);
}),
api: 'derivativeV2',
},
viewerConfig: {
extensions: ['IconMarkupExtension'], // [GetParameterExtension.extensionName],
theme: 'dark-theme',
},
onViewerScriptsLoaded: this.scriptsLoaded,
onViewerInitialized: (async (args: ViewerInitializedEvent) => {
if (this.platform.currentProject.encodedmodelurn) {
args.viewerComponent.DocumentId = this.platform.currentProject.encodedmodelurn;
this.loadCustomToolbar();
// this.loadIconMarkupExtension();
}
else {
// Graphische Anpassung
$('#forge-viewer').hide();
// args.viewerComponent.viewer.uninitialize();
this.messageService.clear();
this.messageService.add({ key: 'noModel', sticky: true, severity: 'warn', summary: 'NOT AVAILABLE', detail: 'Do you want to add a Model' });
this.platform.app.openOverlay();
}
}),
// Muss true sein
showFirstViewable: true,
// Ist falsch gesetzt => GuiViewer3D => Buttons asugeblendet in CSS
headlessViewer: false,
};
}
and finally register after onViewerScriptsLoaded
public scriptsLoaded() {
// Extension.registerExtension(GetParameterExtension.extensionName, GetParameterExtension);
Extension.registerExtension('IconMarkupExtension', IconMarkupExtension);
}
The problem i'm facing that i get an error cause of
class IconMarkupExtension extends Autodesk.Viewing.Extension {
Autodesk is not defined
Thank you
The solution was to rewrite the extension, then as usual register and load the extension.
/// <reference types="forge-viewer" />
import { Extension } from '../../viewer/extensions/extension';
declare const THREE: any;
export class IconMarkupExtension extends Extension {
// Extension must have a name
public static extensionName: string = 'IconMarkupExtension';
public _group;
public _button: Autodesk.Viewing.UI.Button;
public _icons;
public _inputs;
public _enabled;
public _frags;
public onClick;
constructor(viewer, options) {
super(viewer, options);
this._group = null;
this._button = null;
this._enabled = false;
this._icons = options.icons || [];
this._inputs = options.inputs || [];
this.onClick = (id) => {
this.viewer.select(id);
this.viewer.fitToView([id], this.viewer.model, false);
};
}
public load() {
const updateIconsCallback = () => {
if (this._enabled) {
this.updateIcons();
}
};
this.viewer.addEventListener(Autodesk.Viewing.CAMERA_CHANGE_EVENT, updateIconsCallback);
this.viewer.addEventListener(Autodesk.Viewing.ISOLATE_EVENT, updateIconsCallback);
this.viewer.addEventListener(Autodesk.Viewing.HIDE_EVENT, updateIconsCallback);
this.viewer.addEventListener(Autodesk.Viewing.SHOW_EVENT, updateIconsCallback);
return true;
}
unload() {
// Clean our UI elements if we added any
if (this._group) {
this._group.removeControl(this._button);
if (this._group.getNumberOfControls() === 0) {
this.viewer.toolbar.removeControl(this._group);
}
}
return true;
}
onToolbarCreated() {
// Create a new toolbar group if it doesn't exist
this._group = this.viewer.toolbar.getControl('customExtensions');
if (!this._group) {
this._group = new Autodesk.Viewing.UI.ControlGroup('customExtensions');
this.viewer.toolbar.addControl(this._group);
}
// Add a new button to the toolbar group
this._button = new Autodesk.Viewing.UI.Button('IconExtension');
this._button.onClick = (ev) => {
this._enabled = !this._enabled;
this.showIcons(this._enabled);
this._button.setState(this._enabled ? 0 : 1);
};
// this._button.setToolTip(this.options.button.tooltip);
this._button.setToolTip('Showing Panel Information');
// #ts-ignore
this._button.container.children[0].classList.add('fas', 'fa-cogs');
this._group.addControl(this._button);
// Iterate through _inputs
this._inputs.forEach(input => {
var name = '';
if (input.objectPath.indexOf('/')) {
name = input.objectPath.split('/')[input.objectPath.split('/').length - 1];
}
else {
name = input.objectPath;
}
this.viewer.search(name, (idArray) => {
if (idArray.length === 1) {
// console.log(idArray);
this._icons.push({ dbId: idArray[0], label: name, css: 'fas fa-question-circle' });
}
else if (idArray.length !== 1) {
console.log('idArray.length !== 1 getMarkups!!');
}
}, (err) => {
console.log('Something with GETTING MARKUPS went wrong');
}, ['name']);
});
}
showIcons(show) {
console.log(this.viewer);
console.log(this._icons);
// #ts-ignore
const $viewer = $('#' + this.viewer.clientContainer.id + ' div.adsk-viewing-viewer');
// remove previous...
// #ts-ignore
$('#' + this.viewer.clientContainer.id + ' div.adsk-viewing-viewer label.markup').remove();
if (!show) return;
// do we have anything to show?
if (this._icons === undefined || this._icons === null) return;
// do we have access to the instance tree?
const tree = this.viewer.model.getInstanceTree();
if (tree === undefined) { console.log('Loading tree...'); return; }
const onClick = (e) => {
this.onClick($(e.currentTarget).data('id'));
};
this._frags = {};
for (var i = 0; i < this._icons.length; i++) {
// we need to collect all the fragIds for a given dbId
const icon = this._icons[i];
this._frags['dbId' + icon.dbId] = [];
// create the label for the dbId
const $label = $(`
<label class="markup update" data-id="${icon.dbId}">
<span class="${icon.css}"> ${icon.label || ''}</span>
</label>
`);
$label.css('display', this.viewer.isNodeVisible(icon.dbId) ? 'block' : 'none');
$label.on('click', onClick);
$viewer.append($label);
// now collect the fragIds
const _this = this;
tree.enumNodeFragments(icon.dbId, (fragId) => {
_this._frags['dbId' + icon.dbId].push(fragId);
_this.updateIcons(); // re-position of each fragId found
});
}
}
getModifiedWorldBoundingBox(dbId) {
var fragList = this.viewer.model.getFragmentList();
const nodebBox = new THREE.Box3();
// for each fragId on the list, get the bounding box
for (const fragId of this._frags['dbId' + dbId]) {
const fragbBox = new THREE.Box3();
fragList.getWorldBounds(fragId, fragbBox);
nodebBox.union(fragbBox); // create a unifed bounding box
}
return nodebBox;
}
updateIcons() {
// #ts-ignore
// const label = $('#' + this.viewer.clientContainer.id + ' div.adsk-viewing-viewer .update')[0];
// const $label = $(label);
// #ts-ignore
// #ts-ignore
$(() => {
// #ts-ignore
const labels = Array.from($('#' + this.viewer.clientContainer.id + ' div.adsk-viewing-viewer .update'));
for (const label of labels) {
const $label = $(label);
const id = $label.data('id');
// get the center of the dbId(based on its fragIds bounding boxes)
const pos = this.viewer.worldToClient(this.getModifiedWorldBoundingBox(id).center());
// position the label center to it
$label.css('left', Math.floor(pos.x - $label[0].offsetWidth / 2) + 'px');
$label.css('top', Math.floor(pos.y - $label[0].offsetHeight / 2) + 'px');
$label.css('display', this.viewer.isNodeVisible(id) ? 'block' : 'none');
}
});
// for (const label of $('#' + this.viewer.clientContainer.id + ' div.adsk-viewing-viewer .update')) {
// const $label = $(label);
// const id = $label.data('id');
// // #ts-ignore
// // const $label = $('#' + this.viewer.clientContainer.id + ' div.adsk-viewing-viewer .update')[0];
// // const id = $label.data('id');
// // get the center of the dbId (based on its fragIds bounding boxes)
// const pos = this.viewer.worldToClient(this.getModifiedWorldBoundingBox(id).center());
// // console.log(pos);
// // position the label center to it
// $label.css('left', Math.floor(pos.x - $label[0].offsetWidth / 2) + 'px');
// $label.css('top', Math.floor(pos.y - $label[0].offsetHeight / 2) + 'px');
// $label.css('display', this.viewer.isNodeVisible(id) ? 'block' : 'none');
// }
}
}
Use the Viewer's Typescript definitions - see here for install and set them up.
Here's a couple of samples as well if you are after working code:
https://github.com/dukedhx/viewer-ioniccapacitor-angular
https://github.com/dukedhx/viewer-nativescript-angular

How to call a custom directive from angular controller

I have a directive which is depending on some input. I want to call that directive from angular controller when ever that input changed. Here is my directive.
var app = angular.module('App', ['ui.bootstrap']);
app.controller('fcController', function($scope, fcService, $uibModal) {
$scope.formatType = '1';
});
app.directive('fcsaNumber', function($filter) {
var addCommasToInteger, controlKeys, hasMultipleDecimals, isNotControlKey, isNotDigit, isNumber, makeIsValid, makeMaxDecimals, makeMaxDigits, makeMaxNumber, makeMinNumber;
isNumber = function(val) {
return !isNaN(parseFloat(val)) && isFinite(val);
};
isNotDigit = function(which) {
return which < 45 || which > 57 || which === 47;
};
controlKeys = [0, 8, 13];
isNotControlKey = function(which) {
return controlKeys.indexOf(which) === -1;
};
hasMultipleDecimals = function(val) {
return (val != null) && val.toString().split('.').length > 2;
};
makeMaxDecimals = function(maxDecimals) {
var regexString, validRegex;
if (maxDecimals > 0) {
regexString = "^-?\\d*\\.?\\d{0," + maxDecimals + "}$";
} else {
regexString = "^-?\\d*$";
}
validRegex = new RegExp(regexString);
return function(val) {
return validRegex.test(val);
};
};
makeMaxNumber = function(maxNumber) {
return function(val, number) {
return number <= maxNumber;
};
};
makeMinNumber = function(minNumber) {
return function(val, number) {
return number >= minNumber;
};
};
makeMaxDigits = function(maxDigits) {
var validRegex;
validRegex = new RegExp("^-?\\d{0," + maxDigits + "}(\\.\\d*)?$");
return function(val) {
return validRegex.test(val);
};
};
makeIsValid = function(options) {
var validations;
validations = [];
if (options.maxDecimals != null) {
validations.push(makeMaxDecimals(options.maxDecimals));
}
if (options.max != null) {
validations.push(makeMaxNumber(options.max));
}
if (options.min != null) {
validations.push(makeMinNumber(options.min));
}
if (options.maxDigits != null) {
validations.push(makeMaxDigits(options.maxDigits));
}
return function(val) {
var i, number, _i, _ref;
if (!isNumber(val)) {
return false;
}
if (hasMultipleDecimals(val)) {
return false;
}
number = Number(val);
for (i = _i = 0, _ref = validations.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
if (!validations[i](val, number)) {
return false;
}
}
return true;
};
};
addCommasToInteger = function(val) {
var commas, decimals, wholeNumbers;
decimals = val.indexOf('.') == -1 ? '.00' : val.replace(/^\d+(?=\.)/, '');
wholeNumbers = val.replace(/(\.\d+)$/, '');
commas = wholeNumbers.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
return "" + commas + decimals.substring(0, 3);
};
return {
restrict: 'A',
require: 'ngModel',
scope: {
options: '#fcsaNumber',
},
link: function(scope, elem, attrs, ngModelCtrl) {
var isValid, options;
options = {};
if (scope.options != null) {
options = scope.$eval(scope.options);
}
isValid = makeIsValid(options);
ngModelCtrl.$parsers.unshift(function(viewVal) {
var noCommasVal;
noCommasVal = viewVal.replace(/,/g, '');
if (isValid(noCommasVal) || !noCommasVal) {
ngModelCtrl.$setValidity('fcsaNumber', true);
return noCommasVal;
} else {
ngModelCtrl.$setValidity('fcsaNumber', false);
return void 0;
}
});
ngModelCtrl.$formatters.push(function(val) {
if ((options.nullDisplay != null) && (!val || val === '')) {
return options.nullDisplay;
}
if ((val == null) || !isValid(val)) {
return val;
}
ngModelCtrl.$setValidity('fcsaNumber', true);
val = addCommasToInteger(val.toString());
if (options.key == 1) {
options.prepend = 'S/.';
}
if (options.key == 2) {
options.prepend = '$';
}
if (options.prepend != null) {
val = "" + options.prepend + val;
}
if (options.append != null) {
val = "" + val + options.append;
}
return val;
});
elem.on('blur', function() {
var formatter, viewValue, _i, _len, _ref;
viewValue = ngModelCtrl.$modelValue;
if ((viewValue == null) || !isValid(viewValue)) {
return;
}
_ref = ngModelCtrl.$formatters;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
formatter = _ref[_i];
viewValue = formatter(viewValue);
}
ngModelCtrl.$viewValue = viewValue;
return ngModelCtrl.$render();
});
elem.on('focus', function() {
var val;
val = elem.val();
if (options.prepend != null) {
val = val.replace(options.prepend, '');
}
if (options.append != null) {
val = val.replace(options.append, '');
}
elem.val(val.replace(/,/g, ''));
return elem[0].select();
});
if (options.preventInvalidInput === true) {
return elem.on('keypress', function(e) {
if (isNotDigit(e.which && isNotControlKey(e.which))) {
return e.preventDefault();
}
});
}
}
};
});
HTML:
<input type ="text" ng-model ="currency" fcsa-number="{key : {{formatType}}}">
Here $scope.formatType = '1'; is the input. If this formatType changed then That directive need to call. One friend suggested me this
link: function(scope,elem,attr,ctrl){
scope.$watch('fcsaNumber', function(){
// do your stuff
});
but it is not calling my directive manually.
even I have tried with
.controller("ctrl", function($scope) {
$scope.$watch('formatType', function() {
$scope.$broadcast("call_dir")
})
})
return {
restrict: 'A',
require: 'ngModel',
scope: {
options: '#fcsaNumber',
},
link: function(scope, elem, attrs, ngModelCtrl) {
var isValid, options;
options = {};
scope.$on('call_dir', function(ev) {
//your code
})
};
You need to watch the value that is going to change in your controller. In your case it is formatType.
You should try to use this expression in your isolated scope:
scope: {
options: '=fcsaNumber',
}
And if you want the event broadcasting to work, i.e. the $scope.$bradcast approach, then make sure your directive is a child of the controller scope in the scope hierarchy. Else a easier way (but not always recommended) would be to broadcast your event from $rootScope.
So something like this should also work (with your event based solution):
.controller("ctrl", function($scope, $rootScope) {
$scope.$watch('formatType', function() {
$rootScope.$broadcast("call_dir")
})
})

How to convert formatted text to html tag in Google-apps-script?

I want to convert a formatted text in a cell to html but I don't know how to read the format of the text.
Let's say that I have the following text in a cell:
A text with a bold word.
And I would like to convert it in an other cell to:
A text with a <b>bold</b> word.
How can I do that?
I didn't find anything useful in the Spreadsheet API to read format info...Does anyone have any tips?
Thanks.
EDIT: I've created a feature request. Please vote to have the feature.
My Google Script
/**
* Rich Text to HTML.
* #param {string} qRange Input text.
* #returns {string} Text as HTML.
* #customfunction
*/
function RICHTEXT_TO_HTML(qRange) {
var indexBool = false;
var indexItalic = false;
var indexUnderline = false;
var indexStrikethrough = false;
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var range = sheet.getRange(qRange);
var cell = range;
var cellValue = cell.getRichTextValue();
var txt = String(cell.getDisplayValue());
var styles = cell.getTextStyles();
var result = '';
for (var i = 0; i < txt.length; i++) {
var style = cellValue.getTextStyle(i, i + 1);
if (!indexStrikethrough && style.isStrikethrough()) {
indexStrikethrough = true;
result += '<strike>';
} else if (indexStrikethrough && !style.isStrikethrough()) {
indexStrikethrough = false;
result += '</strike>';
}
if (!indexUnderline && style.isUnderline()) {
indexUnderline = true;
result += '<u>';
} else if (indexUnderline && !style.isUnderline()) {
indexUnderline = false;
result += '</u>';
}
if (!indexBool && style.isBold()) {
indexBool = true;
result += '<b>';
} else if (indexBool && !style.isBold()) {
indexBool = false;
result += '</b>';
}
if (!indexItalic && style.isItalic()) {
indexItalic = true;
result += '<i>';
} else if (indexItalic && !style.isItalic()) {
indexItalic = false;
result += '</i>';
}
result += txt[i];
}
if (indexStrikethrough) {
result += '</strike>';
}
if (indexUnderline) {
result += '</u>';
}
if (indexBool) {
result += '</b>';
}
if (indexItalic) {
result += '</i>';
}
return result;
}
Usage
A1 = "My formatted example!!!"
A2 = =RICHTEXT_TO_HTML("A1")
A2 Result = My <i>formatted</i> <b>example</b>!!!
Working example
https://docs.google.com/spreadsheets/d/1mVvE8AdXYKSnaSIfRBrjfOeXxmTkVZovhguMZ3sc47M/edit?usp=sharing
I went ahead and updated this idea to better reflect how we do this is 2021.
/**
* #Author: Emma Sargent
* Rich Text to HTML.
* #param {Range} Google Sheets Range object
* #returns {string} Text as HTML.
* #customfunction
*/
function richTextToHtml(range) {
const runs = range.getRichTextValue().getRuns();
const formattedRuns = runs.map((run) => {
const attr = {
style: '',
};
const text = run.getText();
const link = run.getLinkUrl();
let parentTag = 'span';
if (link) {
parentTag = 'a';
attr.href = link;
}
const style = run.getTextStyle();
const styles = {
'font-family': `'${style.getFontFamily()}'`,
'font-size': `${style.getFontSize()}px`,
color: style.getForegroundColor(),
};
attr.style = Object.entries(styles)
.map(([key, val]) => `${key}: ${val}`)
.join('; ');
let tags = [];
if (style.isBold()) {
tags.push('b');
}
if (style.isItalic()) {
tags.push('i');
}
if (style.isUnderline()) {
tags.push('u');
}
if (style.isStrikethrough()) {
tags.push('strike');
}
const headTags = tags.length ? `<${tags.join('><')}>` : '';
const closeTags = tags.length ? `</${tags.join('></')}>` : '';
const attrStr = Object.entries(attr)
.map(([key, val]) => `${key}="${val}"`)
.join(' ');
const mainTag = `<${parentTag} ${attrStr}>${headTags}${text}${closeTags}</${parentTag}>`;
const lineBreakFormattedStr = mainTag.replace(/[\r\n]/g, '<br>');
return lineBreakFormattedStr;
});
return formattedRuns.join('');
}
And because someone emailed me and asked, here's a version that can be run as a custom function from the formula bar.
Spreadsheet formula: =richTextToHtml("YourSheetName!A1NotationRange")
function richTextToHtml(rangeStr) {
let [sheetName, rangeName] = rangeStr.split("!");
sheetName = sheetName.replaceAll("'",'');
const range = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName).getRange(rangeName);
const runs = range.getRichTextValue().getRuns();
const formattedRuns = runs.map((run) => {
const attr = {
style: '',
};
const text = run.getText();
const link = run.getLinkUrl();
let parentTag = 'span';
if (link) {
parentTag = 'a';
attr.href = link;
}
const style = run.getTextStyle();
const styles = {
'font-family': `'${style.getFontFamily()}'`,
'font-size': `${style.getFontSize()}px`,
color: style.getForegroundColor(),
};
attr.style = Object.entries(styles)
.map(([key, val]) => `${key}: ${val}`)
.join('; ');
let tags = [];
if (style.isBold()) {
tags.push('b');
}
if (style.isItalic()) {
tags.push('i');
}
if (style.isUnderline()) {
tags.push('u');
}
if (style.isStrikethrough()) {
tags.push('strike');
}
const headTags = tags.length ? `<${tags.join('><')}>` : '';
const closeTags = tags.length ? `</${tags.join('></')}>` : '';
const attrStr = Object.entries(attr)
.map(([key, val]) => `${key}="${val}"`)
.join(' ');
const mainTag = `<${parentTag} ${attrStr}>${headTags}${text}${closeTags}</${parentTag}>`;
const lineBreakFormattedStr = mainTag.replace(/[\r\n]/g, '<br>');
return lineBreakFormattedStr;
});
return formattedRuns.join('');
}
Demo sheet here: https://docs.google.com/spreadsheets/d/1X8I_lRXwoUXWRKb2hZPztDIPRs2nmXGtOuWmnrKWjAg/edit?usp=sharing
Thank you Eduardo Cuomo for the answer. I wanted to add the code to get down to the bottom line with a little fix.
/**
* #Author: Eduardo Cuomo
* Rich Text to HTML.
* #param {string} qRange Input text.
* #returns {string} Text as HTML.
* #customfunction
*/
function RICHTEXT_TO_HTML(qRange) {
var indexBool = false;
var indexItalic = false;
var indexUnderline = false;
var indexStrikethrough = false;
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var range = sheet.getRange(qRange);
var cell = range;
var cellValue = cell.getRichTextValue();
var txt = String(cell.getDisplayValue());
var styles = cell.getTextStyles();
var result = '';
for (var i = 0; i < txt.length; i++) {
if(txt[i]=="\n"){
result += '<br>';
}else{
var style = cellValue.getTextStyle(i, i + 1);
if (!indexStrikethrough && style.isStrikethrough()) {
indexStrikethrough = true;
result += '<strike>';
} else if (indexStrikethrough && !style.isStrikethrough()) {
indexStrikethrough = false;
result += '</strike>';
}
if (!indexUnderline && style.isUnderline()) {
indexUnderline = true;
result += '<u>';
} else if (indexUnderline && !style.isUnderline()) {
indexUnderline = false;
result += '</u>';
}
if (!indexBool && style.isBold()) {
indexBool = true;
result += '<b>';
} else if (indexBool && !style.isBold()) {
indexBool = false;
result += '</b>';
}
if (!indexItalic && style.isItalic()) {
indexItalic = true;
result += '<i>';
} else if (indexItalic && !style.isItalic()) {
indexItalic = false;
result += '</i>';
}
result += txt[i];
}
}
if (indexStrikethrough) {
result += '</strike>';
}
if (indexUnderline) {
result += '</u>';
}
if (indexBool) {
result += '</b>';
}
if (indexItalic) {
result += '</i>';
}
return result;
}
Here's a version that creates more compact HTML (avoiding lots of spans setting styles):
function richTextToHtml(range) {
let lastFont = null, lastSize = null, lastColor = null, lastLink = null;
let parentClose = '';
return range.getRichTextValue()
.getRuns()
.map(run => {
const text = run.getText().replace(/[\r\n]/g, '<br>');
const style = run.getTextStyle();
const font = style.getFontFamily();
const size = style.getFontSize();
const color = style.getForegroundColor();
const link = run.getLinkUrl();
let mainTag = '';
if (font !== lastFont || size !== lastSize || color !== lastColor || link !== lastLink) {
mainTag += parentClose;
mainTag += link ? `<a href="${link}"` : '<span';
mainTag +=
` style="font-family:'${font}';font-size:${size}px;color:${color}">`;
parentClose = link ? '</a>' : '</span>';
lastFont = font;
lastSize = size;
lastColor = color;
lastLink = link;
}
let closeTags = '';
Object.entries({isBold: 'b', isItalic: 'i', isUnderline: 'u', isStrikethrough: 'strike'})
.filter(([getter, tag]) => style[getter]())
.forEach(([getter, tag]) => {
mainTag += `<${tag}>`;
closeTags = `</${tag}>${closeTags}`;
});
return `${mainTag}${text}${closeTags}`;
})
.join('') + parentClose;
}
You need to use getFontWeight properties of the Range to read these formating values. Example given here: GAS-check-cell-format
Check Rucent88's answer.

Select2 and wbraganca/yii2-dynamicform not working together in Yii2

I'm trying to use Select2 in wbraganca/yii2-dynamicform. The first row is working fine but when I'm clicking on the plus button, the select2 fild keeps on spinning.
Screenshot - . I've tried - https://github.com/wbraganca/yii2-dynamicform/issues/76 and https://github.com/TimNZ/yii2-dynamicform/blob/master/src/assets/yii2-dynamic-form.js it but seems not to be working.
My code in vendor/wbragance/yii2-dynamicform/asset/yii2-dynamic-form.js looks like -
// "kartik-v/yii2-widget-depdrop"
var $hasDepdrop = $(widgetOptionsRoot.widgetItem).find('[data-krajee-depdrop]');
if ($hasDepdrop.length > 0) {
$hasDepdrop.each(function() {
if ($(this).data('select2') === undefined) {
$(this).removeData().off();
$(this).unbind();
_restoreKrajeeDepdrop($(this));
}
});
}
// "kartik-v/yii2-widget-select2"
var $hasSelect2 = $(widgetOptionsRoot.widgetItem).find('[data-krajee-select2]');
if ($hasSelect2.length > 0) {
$hasSelect2.each(function() {
var id = $(this).attr('id');
var configSelect2 = eval($(this).attr('data-krajee-select2'));
if ($(this).data('select2')) {
$(this).select2('destroy');
}
var configDepdrop = $(this).data('depdrop');
if (configDepdrop) {
configDepdrop = $.extend(true, {}, configDepdrop);
$(this).removeData().off();
$(this).unbind();
_restoreKrajeeDepdrop($(this));
}
var s2LoadingFunc = typeof initSelect2Loading != 'undefined' ? initSelect2Loading : initS2Loading;
var s2OpenFunc = typeof initSelect2DropStyle != 'undefined' ? initSelect2Loading : initS2Loading;
$.when($('#' + id).select2(configSelect2)).done(s2LoadingFunc(id, '.select2-container--krajee'));
var kvClose = 'kv_close_' + id.replace(/\-/g, '_');
$('#' + id).on('select2:opening', function(ev) {
s2OpenFunc(id, kvClose, ev);
});
$('#' + id).on('select2:unselect', function() {
window[kvClose] = true;
});
if (configDepdrop) {
var loadingText = (configDepdrop.loadingText) ? configDepdrop.loadingText : 'Loading ...';
initDepdropS2(id, loadingText);
}
});
}
};
Update -
I've changed the yii2-dynamic-form.js as following
// "kartik-v/yii2-widget-depdrop"
var $hasDepdrop = $(widgetOptionsRoot.widgetItem).find('[data-krajee-depdrop]');
if ($hasDepdrop.length > 0) {
$hasDepdrop.each(function() {
if ($(this).data('select2') === undefined) {
$(this).removeData().off();
$(this).unbind();
_restoreKrajeeDepdrop($(this));
}
});
}
// "kartik-v/yii2-widget-select2"
var $hasSelect2 = $(widgetOptionsRoot.widgetItem).find('[data-krajee-select2]');
if ($hasSelect2.length > 0) {
$hasSelect2.each(function() {
var id = $(this).attr('id');
var configSelect2 = eval($(this).attr('data-krajee-select2'));
if ($(this).data('select2')) {
$(this).select2('destroy');
}
var configDepdrop = $(this).data('depdrop');
if (configDepdrop) {
configDepdrop = $.extend(true, {}, configDepdrop);
$(this).removeData().off();
$(this).unbind();
_restoreKrajeeDepdrop($(this));
}
var s2LoadingFunc = typeof initSelect2Loading != 'undefined' ? initSelect2Loading : initS2Loading;
var s2OpenFunc = typeof initSelect2DropStyle != 'undefined' ? initSelect2Loading : initS2Loading;
$.when($('#' + id).select2(configSelect2)).done(initS2Loading(id, '.select2-container--krajee'));
var kvClose = 'kv_close_' + id.replace(/\-/g, '_');
$('#' + id).on('select2:opening', function(ev) {
//initSelect2DropStyle(id, kvClose, ev);
s2OpenFunc(id, kvClose, ev);
});
$('#' + id).on('select2:unselect', function() {
window[kvClose] = true;
});
if (configDepdrop) {
var loadingText = (configDepdrop.loadingText) ? configDepdrop.loadingText : 'Loading ...';
initDepdropS2(id, loadingText);
}
});
}
Now the there's no error message but I get the following screen -
I'm using Kartik Depdrop and Select2. Only in the first row both seems to be working. In the later only select2 is working with a spinning sign on top of it.
Here's my solution:
I've modified the dynamic-form.js as follows:
/**
* yii2-dynamic-form
*
* A jQuery plugin to clone form elements in a nested manner, maintaining accessibility.
*
* #author Wanderson Bragança <wanderson.wbc#gmail.com>
*/
(function ($) {
var pluginName = 'yiiDynamicForm';
var regexID = /^(.+?)([-\d-]{1,})(.+)$/i;
var regexName = /(^.+?)([\[\d{1,}\]]{1,})(\[.+\]$)/i;
$.fn.yiiDynamicForm = function (method) {
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method) {
return methods.init.apply(this, arguments);
} else {
$.error('Method ' + method + ' does not exist on jQuery.yiiDynamicForm');
return false;
}
};
var events = {
beforeInsert: 'beforeInsert',
afterInsert: 'afterInsert',
beforeDelete: 'beforeDelete',
afterDelete: 'afterDelete',
limitReached: 'limitReached'
};
var methods = {
init: function (widgetOptions) {
return this.each(function () {
widgetOptions.template = _parseTemplate(widgetOptions);
});
},
addItem: function (widgetOptions, e, $elem) {
_addItem(widgetOptions, e, $elem);
},
deleteItem: function (widgetOptions, e, $elem) {
_deleteItem(widgetOptions, e, $elem);
},
updateContainer: function () {
var widgetOptions = eval($(this).attr('data-dynamicform'));
_updateAttributes(widgetOptions);
_restoreSpecialJs(widgetOptions);
_fixFormValidaton(widgetOptions);
}
};
var _parseTemplate = function(widgetOptions) {
var $template = $(widgetOptions.template);
$template.find('div[data-dynamicform]').each(function(){
var widgetOptions = eval($(this).attr('data-dynamicform'));
if ($(widgetOptions.widgetItem).length > 1) {
var item = $(this).find(widgetOptions.widgetItem).first()[0].outerHTML;
$(this).find(widgetOptions.widgetBody).html(item);
}
});
$template.find('input, textarea, select').each(function() {
$(this).val('');
});
$template.find('input[type="checkbox"], input[type="radio"]').each(function() {
var inputName = $(this).attr('name');
var $inputHidden = $template.find('input[type="hidden"][name="' + inputName + '"]').first();
if ($inputHidden) {
$(this).val(1);
$inputHidden.val(0);
}
});
return $template;
};
var _getWidgetOptionsRoot = function(widgetOptions) {
return eval($(widgetOptions.widgetBody).parents('div[data-dynamicform]').last().attr('data-dynamicform'));
};
var _getLevel = function($elem) {
var level = $elem.parents('div[data-dynamicform]').length;
level = (level < 0) ? 0 : level;
return level;
};
var _count = function($elem, widgetOptions) {
return $elem.closest('.' + widgetOptions.widgetContainer).find(widgetOptions.widgetItem).length;
};
var _createIdentifiers = function(level) {
return new Array(level + 2).join('0').split('');
};
var _addItem = function(widgetOptions, e, $elem) {
var count = _count($elem, widgetOptions);
if (count < widgetOptions.limit) {
$toclone = widgetOptions.template;
$newclone = $toclone.clone(false, false);
if (widgetOptions.insertPosition === 'top') {
$elem.closest('.' + widgetOptions.widgetContainer).find(widgetOptions.widgetBody).prepend($newclone);
} else {
$elem.closest('.' + widgetOptions.widgetContainer).find(widgetOptions.widgetBody).append($newclone);
}
_updateAttributes(widgetOptions);
_restoreSpecialJs(widgetOptions);
_fixFormValidaton(widgetOptions);
$elem.closest('.' + widgetOptions.widgetContainer).triggerHandler(events.afterInsert, $newclone);
} else {
// trigger a custom event for hooking
$elem.closest('.' + widgetOptions.widgetContainer).triggerHandler(events.limitReached, widgetOptions.limit);
}
};
var _removeValidations = function($elem, widgetOptions, count) {
if (count > 1) {
$elem.find('div[data-dynamicform]').each(function() {
var currentWidgetOptions = eval($(this).attr('data-dynamicform'));
var level = _getLevel($(this));
var identifiers = _createIdentifiers(level);
var numItems = $(this).find(currentWidgetOptions.widgetItem).length;
for (var i = 1; i <= numItems -1; i++) {
var aux = identifiers;
aux[level] = i;
currentWidgetOptions.fields.forEach(function(input) {
var id = input.id.replace("{}", aux.join('-'));
if ($("#" + currentWidgetOptions.formId).yiiActiveForm("find", id) !== "undefined") {
$("#" + currentWidgetOptions.formId).yiiActiveForm("remove", id);
}
});
}
});
var level = _getLevel($elem.closest('.' + widgetOptions.widgetContainer));
var widgetOptionsRoot = _getWidgetOptionsRoot(widgetOptions);
var identifiers = _createIdentifiers(level);
identifiers[0] = $(widgetOptionsRoot.widgetItem).length - 1;
identifiers[level] = count - 1;
widgetOptions.fields.forEach(function(input) {
var id = input.id.replace("{}", identifiers.join('-'));
if ($("#" + widgetOptions.formId).yiiActiveForm("find", id) !== "undefined") {
$("#" + widgetOptions.formId).yiiActiveForm("remove", id);
}
});
}
};
var _deleteItem = function(widgetOptions, e, $elem) {
var count = _count($elem, widgetOptions);
if (count > widgetOptions.min) {
$todelete = $elem.closest(widgetOptions.widgetItem);
// trigger a custom event for hooking
var eventResult = $('.' + widgetOptions.widgetContainer).triggerHandler(events.beforeDelete, $todelete);
if (eventResult !== false) {
_removeValidations($todelete, widgetOptions, count);
$todelete.remove();
_updateAttributes(widgetOptions);
_restoreSpecialJs(widgetOptions);
_fixFormValidaton(widgetOptions);
$('.' + widgetOptions.widgetContainer).triggerHandler(events.afterDelete);
}
}
};
var _updateAttrID = function($elem, index) {
var widgetOptions = eval($elem.closest('div[data-dynamicform]').attr('data-dynamicform'));
var id = $elem.attr('id');
var newID = id;
if (id !== undefined) {
var matches = id.match(regexID);
if (matches && matches.length === 4) {
matches[2] = matches[2].substring(1, matches[2].length - 1);
var identifiers = matches[2].split('-');
identifiers[0] = index;
if (identifiers.length > 1) {
var widgetsOptions = [];
$elem.parents('div[data-dynamicform]').each(function(i){
widgetsOptions[i] = eval($(this).attr('data-dynamicform'));
});
widgetsOptions = widgetsOptions.reverse();
for (var i = identifiers.length - 1; i >= 1; i--) {
identifiers[i] = $elem.closest(widgetsOptions[i].widgetItem).index();
}
}
newID = matches[1] + '-' + identifiers.join('-') + '-' + matches[3];
$elem.attr('id', newID);
} else {
newID = id + index;
$elem.attr('id', newID);
}
}
if (id !== newID) {
$elem.closest(widgetOptions.widgetItem).find('.field-' + id).each(function() {
$(this).removeClass('field-' + id).addClass('field-' + newID);
});
// update "for" attribute
$elem.closest(widgetOptions.widgetItem).find("label[for='" + id + "']").attr('for',newID);
}
return newID;
};
var _updateAttrName = function($elem, index) {
var name = $elem.attr('name');
if (name !== undefined) {
var matches = name.match(regexName);
if (matches && matches.length === 4) {
matches[2] = matches[2].replace(/\]\[/g, "-").replace(/\]|\[/g, '');
var identifiers = matches[2].split('-');
identifiers[0] = index;
if (identifiers.length > 1) {
var widgetsOptions = [];
$elem.parents('div[data-dynamicform]').each(function(i){
widgetsOptions[i] = eval($(this).attr('data-dynamicform'));
});
widgetsOptions = widgetsOptions.reverse();
for (var i = identifiers.length - 1; i >= 1; i--) {
identifiers[i] = $elem.closest(widgetsOptions[i].widgetItem).index();
}
}
name = matches[1] + '[' + identifiers.join('][') + ']' + matches[3];
$elem.attr('name', name);
}
}
return name;
};
var _updateAttributes = function(widgetOptions) {
var widgetOptionsRoot = _getWidgetOptionsRoot(widgetOptions);
$(widgetOptionsRoot.widgetItem).each(function(index) {
var $item = $(this);
$(this).find('*').each(function() {
// update "id" attribute
_updateAttrID($(this), index);
// update "name" attribute
_updateAttrName($(this), index);
});
});
};
var _fixFormValidatonInput = function(widgetOptions, attribute, id, name) {
if (attribute !== undefined) {
attribute = $.extend(true, {}, attribute);
attribute.id = id;
attribute.container = ".field-" + id;
attribute.input = "#" + id;
attribute.name = name;
attribute.value = $("#" + id).val();
attribute.status = 0;
if ($("#" + widgetOptions.formId).yiiActiveForm("find", id) !== "undefined") {
$("#" + widgetOptions.formId).yiiActiveForm("remove", id);
}
$("#" + widgetOptions.formId).yiiActiveForm("add", attribute);
}
};
var _fixFormValidaton = function(widgetOptions) {
var widgetOptionsRoot = _getWidgetOptionsRoot(widgetOptions);
$(widgetOptionsRoot.widgetBody).find('input, textarea, select').each(function() {
var id = $(this).attr('id');
var name = $(this).attr('name');
if (id !== undefined && name !== undefined) {
currentWidgetOptions = eval($(this).closest('div[data-dynamicform]').attr('data-dynamicform'));
var matches = id.match(regexID);
if (matches && matches.length === 4) {
matches[2] = matches[2].substring(1, matches[2].length - 1);
var level = _getLevel($(this));
var identifiers = _createIdentifiers(level -1);
var baseID = matches[1] + '-' + identifiers.join('-') + '-' + matches[3];
var attribute = $("#" + currentWidgetOptions.formId).yiiActiveForm("find", baseID);
_fixFormValidatonInput(currentWidgetOptions, attribute, id, name);
}
}
});
};
var _restoreSpecialJs = function(widgetOptions) {
var widgetOptionsRoot = _getWidgetOptionsRoot(widgetOptions);
// "kartik-v/yii2-widget-datepicker"
var $hasDatepicker = $(widgetOptionsRoot.widgetItem).find('[data-krajee-datepicker]');
if ($hasDatepicker.length > 0) {
$hasDatepicker.each(function() {
$(this).parent().removeData().datepicker('remove');
$(this).parent().datepicker(eval($(this).attr('data-krajee-datepicker')));
});
}
// "kartik-v/yii2-widget-timepicker"
var $hasTimepicker = $(widgetOptionsRoot.widgetItem).find('[data-krajee-timepicker]');
if ($hasTimepicker.length > 0) {
$hasTimepicker.each(function() {
$(this).removeData().off();
$(this).parent().find('.bootstrap-timepicker-widget').remove();
$(this).unbind();
$(this).timepicker(eval($(this).attr('data-krajee-timepicker')));
});
}
// "kartik-v/yii2-money"
var $hasMaskmoney = $(widgetOptionsRoot.widgetItem).find('[data-krajee-maskMoney]');
if ($hasMaskmoney.length > 0) {
$hasMaskmoney.each(function() {
$(this).parent().find('input').removeData().off();
var id = '#' + $(this).attr('id');
var displayID = id + '-disp';
$(displayID).maskMoney('destroy');
$(displayID).maskMoney(eval($(this).attr('data-krajee-maskMoney')));
$(displayID).maskMoney('mask', parseFloat($(id).val()));
$(displayID).on('change', function () {
var numDecimal = $(displayID).maskMoney('unmasked')[0];
$(id).val(numDecimal);
$(id).trigger('change');
});
});
}
// "kartik-v/yii2-widget-fileinput"
var $hasFileinput = $(widgetOptionsRoot.widgetItem).find('[data-krajee-fileinput]');
if ($hasFileinput.length > 0) {
$hasFileinput.each(function() {
$(this).fileinput(eval($(this).attr('data-krajee-fileinput')));
});
}
// "kartik-v/yii2-widget-touchspin"
var $hasTouchSpin = $(widgetOptionsRoot.widgetItem).find('[data-krajee-TouchSpin]');
if ($hasTouchSpin.length > 0) {
$hasTouchSpin.each(function() {
$(this).TouchSpin('destroy');
$(this).TouchSpin(eval($(this).attr('data-krajee-TouchSpin')));
});
}
// "kartik-v/yii2-widget-colorinput"
var $hasSpectrum = $(widgetOptionsRoot.widgetItem).find('[data-krajee-spectrum]');
if ($hasSpectrum.length > 0) {
$hasSpectrum.each(function() {
var id = '#' + $(this).attr('id');
var sourceID = id + '-source';
$(sourceID).spectrum('destroy');
$(sourceID).unbind();
$(id).unbind();
var configSpectrum = eval($(this).attr('data-krajee-spectrum'));
configSpectrum.change = function (color) {
jQuery(id).val(color.toString());
};
$(sourceID).attr('name', $(sourceID).attr('id'));
$(sourceID).spectrum(configSpectrum);
$(sourceID).spectrum('set', jQuery(id).val());
$(id).on('change', function(){
$(sourceID).spectrum('set', jQuery(id).val());
});
});
}
var _restoreKrajeeDepdrop = function($elem) {
var configDepdrop = $.extend(true, {}, eval($elem.attr('data-krajee-depdrop')));
var inputID = $elem.attr('id');
var matchID = inputID.match(regexID);
if (matchID && matchID.length === 4) {
for (index = 0; index < configDepdrop.depends.length; ++index) {
var match = configDepdrop.depends[index].match(regexID);
if (match && match.length === 4) {
configDepdrop.depends[index] = match[1] + matchID[2] + match[3];
}
}
}
$elem.depdrop(configDepdrop);
};
// "kartik-v/yii2-widget-depdrop"
var _restoreKrajeeDepdrop = function($elem) {
var configDepdrop = $.extend(true, {}, eval($elem.attr('data-krajee-depdrop')));
var inputID = $elem.attr('id');
var matchID = inputID.match(regexID);
if (matchID && matchID.length === 4) {
for (index = 0; index < configDepdrop.depends.length; ++index) {
var match = configDepdrop.depends[index].match(regexID);
if (match && match.length === 4) {
configDepdrop.depends[index] = match[1] + matchID[2] + match[3];
}
}
}
$elem.depdrop(configDepdrop);
};
var $hasDepdrop = $(widgetOptionsRoot.widgetItem).find('[data-krajee-depdrop]');
if ($hasDepdrop.length > 0) {
$hasDepdrop.each(function() {
if ($(this).data('select2') === undefined) {
$(this).removeData().off();
$(this).unbind();
_restoreKrajeeDepdrop($(this));
}
var configDepdrop = eval($(this).attr('data-krajee-depdrop'));
$(this).depdrop(configDepdrop);
});
}
// "kartik-v/yii2-widget-select2"
var $hasSelect2 = $(widgetOptionsRoot.widgetItem).find('[data-krajee-select2]');
if ($hasSelect2.length > 0) {
$hasSelect2.each(function() {
var id = $(this).attr('id');
var configSelect2 = eval($(this).attr('data-krajee-select2'));
$.when($('#' + id).select2(configSelect2)).done(initS2Loading(id));
$('#' + id).on('select2-open', function() {
initSelect2DropStyle(id)
});
if ($(this).attr('data-krajee-depdrop')) {
$(this).on('depdrop.beforeChange', function(e,i,v) {
var configDepdrop = eval($(this).attr('data-krajee-depdrop'));
var loadingText = (configDepdrop.loadingText)? configDepdrop.loadingText : 'Loading ...';
$('#' + id).select2('data', {text: loadingText});
});
$(this).on('depdrop.change', function(e,i,v,c) {
$('#' + id).select2('val', $('#' + id).val());
});
}
});
}
};
})(window.jQuery);
It's working well on my end.
I've found a solution to remove the spinning icon on the select2. I've removed the 'id' from select2 widget in my _form and that resolved this issue.
Instead of doing changes in vendor's code there is new repo with issue fixes.
Link => https://packagist.org/packages/vivekmarakana/yii2-dynamicform
Either run
php composer.phar require --prefer-dist vivekmarakana/yii2-dynamicform "*"
or add
"vivekmarakana/yii2-dynamicform": "*"
to the require section of your composer.json file.