Fabric UI Detailslist Additional Columns[React SPFx] - spfx

I'm building a SPFx Webpart with the Fabric/Fluent UI DetailsList component and I'm trying to add dynamic columns based on security groups selected from a picker (this is likely to change but will remain an array). I've found some code that does just this and have adopted it to my project but the gulp serve fails to mount the webpart in the workbench.
private _columns: IColumn[] = [
{
key: 'filepath',
name: 'File path',
onRender: item => (
// tslint:disable-next-line:jsx-no-lxambda
<Link key={item} onClick={() => this._navigate(item)}>
{item}
</Link>
),
} as IColumn,
];
private _addcolumns(_columns:IColumn[]): IColumn[] {
for (let user of this.props.people) {
_columns.push({
key: 'permissionset',
name: 'Permission',
onRender: item => (<DropPermissionItem/>)
} as IColumn,
)
}
return _columns
};
...
<DetailsList
key={this.state.key}
items={this.state.items}
columns={this._addcolumns(this._columns)}
onItemInvoked={this._navigate}
initialFocusedIndex={this.state.initialFocusedIndex}
ariaLabelForSelectionColumn="Toggle selection"
ariaLabelForSelectAllCheckbox="Toggle selection for all items"
checkButtonAriaLabel="Row checkbox"
/>
The DetailsList uses the _addColumns and passes in the _columns to append the additional columns. Is there something I'm missing or is there a better way?

I've been able to dynamically set the number of columns.
private _columns: IColumn[] = [
{
key: 'file',
name: "File",
minWidth: 60,
onRender: item => (
// tslint:disable-next-line:jsx-no-lxambda
<Link key={item} onClick={() => this._navigate(item)}>
{item}
</Link>
)
},
{
key: 'permission',
name: "permission",
minWidth: 60,
onRender: item => (<DropPermissionItem/>),
}
];
private _addcolumns(column: IColumn[]): IColumn[] {
let _loadColumn = this._loadUser();
if (_loadColumn == null){
return column;
} else
{
for (let col of _loadColumn) {
column.push({
key: 'permission',
name: 'Permission',
minWidth: 60,
onRender: item => (<DropPermissionItem/>),
}
);
}
return column;
}
}
private displayColumns: IColumn[] = this._addcolumns(this._columns);
Use the this.displayColumns inside the column property of the detail list.

Related

Register click listener on ckeditor5 dropdown items

I am currently trying to write a plugin for the CKEditor 5 to support automatic translations. I was able to find out how to write plugins and how to create dropdowns in the documentation.
But in the documentation there is no mention (or I missed it) how to be informed about a click on the values:
There is an Execute Handler for the button that opens the dropdown, but how do I register a listener for a click on one of the values?
Can I assign an id or similar to my items to recognize the click on the right element of the dropdown?
Here's the code that I was able to build based on the documentation:
class Translation extends Plugin {
init() {
this.editor.ui.componentFactory.add('translate', (locale) => {
const dropdownView = createDropdown(locale);
dropdownView.buttonView.set({
icon: languageIcon,
label: 'Translate',
tooltip: true,
});
const items = new Collection();
items.add({
id: 'en', // how to assign id ???
type: 'button',
model: new Model({
withText: true,
label: 'English'
}),
});
items.add({
id: 'es', // how to assign id ???
type: 'button',
model: new Model({
withText: true,
label: 'Spanish'
}),
});
addListToDropdown(dropdownView, items);
// callback for click on item ????
dropdownView.on('click', (event) => {
console.log('click', event);
});
return dropdownView;
});
}
}
You can use DropdownView.on() method to listen to the execute event.
And, use EventInfo.source property to get the object that is clicked and then use its property e.g. id or label to identify it.
For example:
const items = new Collection();
items.add( {
type: 'button',
model: new Model({
id: 'en', // id
withText: true,
label: 'English',
})
} );
items.add( {
type: 'button',
model: new Model({
id: 'es', // id
withText: true,
label: 'Spanish'
})
} );
addListToDropdown(dropdownView, items);
dropdownView.on('execute', (eventInfo) => {
const { id, label } = eventInfo.source;
if ( id === 'en' ) {
console.log('Object (en):', label);
} else if ( id === 'es' ) {
console.log('Object (es):', label);
}
});
Here's the complete working example with ClassicEditor (tested):
import ClassicEditor from '#ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Essentials from '#ckeditor/ckeditor5-essentials/src/essentials';
import Model from '#ckeditor/ckeditor5-ui/src/model';
import Collection from '#ckeditor/ckeditor5-utils/src/collection';
import { createDropdown, addListToDropdown } from '#ckeditor/ckeditor5-ui/src/dropdown/utils';
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
const Translate = 'translate';
class Translation extends Plugin {
init() {
console.log('Translation initialized!');
this.editor.ui.componentFactory.add(Translate, (locale) => {
const dropdownView = createDropdown(locale);
dropdownView.buttonView.set({
label: 'Translate',
withText: true,
});
const items = new Collection();
items.add( {
type: 'button',
model: new Model({
id: 'en',
withText: true,
label: 'English',
})
} );
items.add( {
type: 'button',
model: new Model({
id: 'es',
withText: true,
label: 'Spanish'
})
} );
addListToDropdown(dropdownView, items);
dropdownView.on('execute', (eventInfo) => {
const { id, label } = eventInfo.source;
if ( id === 'en' ) {
console.log('Object (en):', label);
} else if ( id === 'es' ) {
console.log('Object (es):', label);
}
});
return dropdownView;
});
}
}
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ Essentials, Translation ],
toolbar: [ Translate ]
} )
.then( editor => {
console.log( 'Editor was initialized', editor );
} )
.catch( error => {
console.error( error.stack );
} );
Console output after clicking both items:
Object (en): English
Object (es): Spanish
You can use execute. it will fire an event when the toolbar button or list item is executed.for listView It fires when a child of some ListItemView fired execute. for toolbarView It fires when one of the buttons has been executed. execute will return EventInfo object when event fires. Also, there is off() and stop() method to de-register the event listener.
Note: Only supported when dropdown has list view added using addListToDropdown or addToolbarToDropdown.
Here is the snippet, give it a try.
this.listenTo( dropdownView, 'execute', eventInfo => {
console.log(eventInfo.source);
} );
----------------------------------------------------------- OR ------------------------------------------------------------------
dropdownView.on('execute', eventInfo => {
console.log(eventInfo.source);
} );

Error when trying to update in react-redux

I am trying to update my data in redux but I get an error when I have more than one value in the state.
How I am transferring data into the AllPalletes component below:
<Route exact path='/' render ={(routeProps) => <AllPalletes data = {this.props.palleteNames} />} />
The AllPalletes component, where I am setting up the edit form:
class connectingPalletes extends Component {
render () {
console.log(this.props)
return (
<div>
<Menu inverted>
<Menu.Item header>Home</Menu.Item>
<Menu.Item as = {Link} to = '/createpalette'>Create a Palette</Menu.Item>
</Menu>
<Container>
<Card.Group itemsPerRow={4}>
{this.props.data.map((card) => {
let cardName = card.Name.replace(/\s+/g, '-').toLowerCase()
return (
<Card key = {card._id}>
<Image src = 'https://images.pexels.com/photos/1212406/pexels-photo-1212406.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500' wrapped ui={false}/>
<Card.Content>
<Grid>
<Grid.Column floated = 'left' width ={7}>
{card.edit? (
<PaletteEditForm {...card}/>
) : (
<Card.Header as = {Link} to = {`/palette/${cardName}`}>{card.Name}</Card.Header>
)}
</Grid.Column>
<Grid.Column floated = 'right' width = {5}>
<Icon name = 'pencil' />
<Icon name = 'trash' onClick = {() => this.props.dispatch(removePalette({id: card._id}))}/>
</Grid.Column>
</Grid>
</Card.Content>
</Card>
)
})}
</Card.Group>
<Divider></Divider>
<Divider hidden></Divider>
<Grid centered columns={1}>
<Button as = {Link} to = '/testing'>Go Back</Button>
</Grid>
</Container>
</div>
)
}
}
const AllPalletes = connect()(connectingPalletes)
export default AllPalletes
And here is the edit form:
class EditForm extends Component {
constructor(props) {
super(props)
this.state = {
paletteName: this.props.Name
}
}
handleChange = (e) => {
const val = e.target.value,
s_name = e.target.name
this.setState (() => {
return {
[s_name]: val,
}
})
}
handleSubmit = () => {
let updates = {Name: this.state.paletteName, edit: false}
this.props.dispatch(editPalette(this.props._id, updates))
}
render() {
console.log(this.props)
return (
<Form onSubmit = {this.handleSubmit}>
<Input type = 'text' name = 'paletteName' value = {this.state.paletteName} onChange={this.handleChange} />
</Form>
)
}
}
const PaletteEditForm = connect()(EditForm)
export default PaletteEditForm
My Reducer:
import uuid from 'uuid/v1'
const paletteDefault = [{
Name: "Material UI",
myArray: [],
_id: uuid(),
edit: false
}, {
Name: "Splash UI",
myArray: [],
_id: uuid(),
edit: true
}]
const PaletteReducers = (state = paletteDefault, action) => {
console.log(action)
switch(action.type) {
case 'ADD_PALETTE':
return [...state, action.palette]
case 'REMOVE_PALETTE':
return state.filter(x => x._id !== action.id)
case 'EDIT_PALETTE':
return state.map((palette) => {
if(palette._id === action.id) {
return {
...palette,
...action.updates
}
}
})
default:
return state
}
}
export default PaletteReducers
My Action
// EDIT_PALETTE
const editPalette = (id, updates) => ({
type: 'EDIT_PALETTE',
id,
updates
})
export {addPalette, removePalette, editPalette}
I have a feeling that the problem could be in how I have set up the reducer case.
The edit dispatch only works when I have one value in the state. Otherwise, I am getting this error:
Uncaught TypeError: Cannot read property 'Name' of undefined
at AllPalletes.js:23
Please help..
I found the error. I had not give a return value in the 'EDIT_PALETTE' case, after the if-statement. It was
case 'EDIT_PALETTE':
return state.map((palette) => {
if(palette._id === action.id) {
return {
...palette,
...action.updates
}
}
})
And instead should be:
case 'EDIT_PALETTE':
return state.map((palette) => {
if(palette._id === action.id) {
return {
...palette,
...action.updates
}
}
return palette
})

In CKEditor5, In a downcast converter, how to insert a view element at the beginning of another view element?

I want to add "decoration" (non semantic) elements into the editing view at specific positions.
I made a small change in the Block-Widget Plugin given in the CKEditor5 tutorial
https://ckeditor.com/docs/ckeditor5/latest/framework/guides/tutorials/implementing-a-block-widget.html
I added 3 lines ("const badge" etc...) into the downcast converter code:
_defineConverters() {
const conversion = this.editor.conversion;
// <simpleBox> converters
conversion.for( 'upcast' ).elementToElement( {
model: 'simpleBox',
view: {
name: 'section',
classes: 'simple-box'
}
} );
conversion.for( 'dataDowncast' ).elementToElement( {
model: 'simpleBox',
view: {
name: 'section',
classes: 'simple-box'
}
} );
conversion.for( 'editingDowncast' ).elementToElement( {
model: 'simpleBox',
view: ( modelElement, viewWriter ) => {
const section = viewWriter.createContainerElement( 'section', { class: 'simple-box' } );
const badge = viewWriter.createContainerElement( 'span', { class: 'cause-badge', style: 'font-weight: bold' } );
viewWriter.insert( viewWriter.createPositionAt( badge, 0 ), viewWriter.createText('Simple Box') );
viewWriter.insert( viewWriter.createPositionAt( section, 0), badge );
return toWidget( section, viewWriter, { label: 'simple box widget' } );
}
} );
//....
I was expecting the "Simple box" at the begining (top) of the widget, since I use createPositionAt( section, 0).
But instead, I get it at the end (bottom) of the widget (on Firefox).
screenshot on Firefox
I think that the easiest approach is to define this star rating element in the model and add proper conversion for it.
For editing pipeline you'd need and UIElement that allows to render in the editor editing area arbitrary HTML using render function. You can check some examples of using render function in custom UI elements POC.
Simple stub of such functionality below:
// Define schema element
schema.register( 'simpleBoxStars', {
allowIn: 'simpleBox',
allowAttributes: [ 'rating' ]
} );
conversion.for( 'upcast' ).elementToElement( {
model: ( viewElement, writer ) => {
return writer.createElement( 'simpleBoxStars', { rating: viewElement.getAttribute( 'data-rating' ) } );
},
view: {
name: 'span',
classes: 'simple-box-stars'
}
} );
conversion.for( 'editingDowncast' ).elementToElement( {
model: 'simpleBoxStars',
view: ( modelElement, writer ) => {
const uiSpan = writer.createUIElement( 'span', { class: 'simple-box-stars' }, function( domDocument ) {
const domElement = this.toDomElement( domDocument );
// Customize this for you needs
domElement.innerHTML = `This many stars: <span class="stars">${ modelElement.getAttribute( 'rating' ) }</span>`;
//
domElement.addEventListener( 'click', evt => {
// detect which star was clicked
editor.model.change( writer => {
// Change model elment value, ie by using .setAttribute()
} );
} );
return domElement;
} );
return uiSpan;
}
} );
// For editor.getData() just output the data normally:
conversion.for( 'dataDowncast' ).elementToElement( {
model: 'simpleBoxStars',
view: {
name: 'span',
classes: 'simple-box-stars'
}
} );

Conversion for multiple style attributes in different plugins are not working?

I have developed a few plugins for ckeditor which work by adding span tag with style attribute.
Letter Spacing
Line Height
Text Transform
They are working fine when I make changes in editor but, when I initialise the editor with <p><span style="line-height:3;letter-spacing:18px;text-transform:uppercase;font-family:Ubuntu, Arial, sans-serif;font-size:12px;">safasfasfas</span></p> only text-transform, font-family and font-size is working but rest of 2 plugins/style attributes are not working.
Is there any idea, what I am doing wrong?
Edit:
I have conversion like below
editor.conversion.for( 'downcast' )
.add( downcastAttributeToElement( {
model: {
key: 'letterSpacing',
name: '$text'
},
view: ( modelAttributeValue, viewWriter ) => {
return viewWriter.createAttributeElement( 'span', { style: 'letter-spacing:' + modelAttributeValue + 'px' } );
}
} ) );
editor.conversion.for( 'upcast' )
.add( upcastElementToAttribute( {
view: {
name: 'span'
},
model: {
key: 'letterSpacing',
value: viewElement => {
const letterSpacing = viewElement.getStyle( 'letter-spacing' );
if (letterSpacing === undefined) {
return null;
}
return letterSpacing.substr( 0, letterSpacing.length - 2 );
}
}
} ) );
I was able to fix this issue by adding below code in conversion.
styles: {
'letter-spacing': /[\S]+/
}
Now conversion becomes like
editor.conversion.for( 'downcast' )
.add( downcastAttributeToElement( {
model: {
key: 'letterSpacing',
name: '$text'
},
view: ( modelAttributeValue, viewWriter ) => {
return viewWriter.createAttributeElement( 'span', { style: 'letter-spacing:' + modelAttributeValue + 'px' } );
}
} ) );
editor.conversion.for( 'upcast' )
.add( upcastElementToAttribute( {
view: {
name: 'span',
styles: {
'letter-spacing': /[\S]+/
}
},
model: {
key: 'letterSpacing',
value: viewElement => {
const letterSpacing = viewElement.getStyle( 'letter-spacing' );
if (letterSpacing === undefined) {
return null;
}
return letterSpacing.substr( 0, letterSpacing.length - 2 );
}
}
} ) );

ckeditor with placeholder plguin enhancement double click issue

I need a placeholder/variable that takes name, defaultValue, tooltip/description. I created a plugin and it is working fine in the editor/create mode. When placeholder is created, it is adding the following tags to source
<var class="cke_placeholder" name="varName" title="varToolTip">[[varDefaultValue]]</var>
Image that depicts create & edit mode differences
When I save the html content with placehoder in db and trying to load it back to ckeditor, I am not able to get the + symbol and hence not able to launch the editor.
Here is my ckeditor/plugins/var/plguin.js
'use strict';
( function() {
CKEDITOR.plugins.add( 'var', {
requires: 'widget,dialog',
icons: 'var', // %REMOVE_LINE_CORE%
hidpi: true, // %REMOVE_LINE_CORE%
onLoad: function() {
CKEDITOR.dialog.add( 'var', this.path + 'dialogs/var.js' );
},
init: function( editor ) {
this.registerWidget( editor );
editor.ui.addButton && editor.ui.addButton( 'Var', {
label: 'Create Variable',
command: 'var',
toolbar: 'insert',
icon: 'var'
} );
},
registerWidget: function(editor){
var that = this;
// Put ur init code here.
editor.widgets.add( 'var', {
// Widget code.
dialog: 'var',
pathName: 'var',
// We need to have wrapping element, otherwise there are issues in
// add dialog.
template: '<var class="cke_placeholder">[[]]</var>',
downcast: function() {
return new CKEDITOR.htmlParser.text( '<var class="cke_placeholder" name="'+this.data.name+'" title="'+this.data.description+'">[[' + this.data.defaultValue + ']]</var>' );
},
init: function() {
this.setData( 'defaultValue', this.element.getText().slice( 2, -2 ) );
this.setData( 'name', this.element.getAttribute("name") );
this.setData( 'description', this.element.getAttribute("title") );
},
data: function() {
this.element.setText( '[[' + this.data.defaultValue + ']]' );
this.element.setAttribute('name', this.data.name );
this.element.setAttribute('title', this.data.description );
}
} );
},
afterInit: function( editor ) {
this.registerWidget( editor );
/*var placeholderReplaceRegex = /\[\[([^\[\]])+\]\]/g;
editor.dataProcessor.dataFilter.addRules( {
text: function( text, node ) {
var dtd = node.parent && CKEDITOR.dtd[ node.parent.name ];
// Skip the case when placeholder is in elements like <title> or <textarea>
// but upcast placeholder in custom elements (no DTD).
if ( dtd && !dtd.span )
return;
return text.replace( placeholderReplaceRegex, function( match ) {
// Creating widget code.
var widgetWrapper = null,
innerElement = new CKEDITOR.htmlParser.element( 'span', {
'class': 'cke_placeholder'
} );
// Adds placeholder identifier as innertext.
innerElement.add( new CKEDITOR.htmlParser.text( match ) );
widgetWrapper = editor.widgets.wrapElement( innerElement, 'placeholder' );
// Return outerhtml of widget wrapper so it will be placed
// as replacement.
return widgetWrapper.getOuterHtml();
} );
}
} );*/
}
} );
} )();
Here is my ckeditor/plugins/var/dialogs/var.js
'use strict';
CKEDITOR.dialog.add( 'var', function( editor ) {
//var lang = editor.lang.var,
//generalLabel = editor.lang.common.generalTab,
var generalLabel = 'General',
validRegex = /^[^\[\]<>]+$/,
emptyOrInvalid = ' can not be empty. It can not contain any of following characters: [, ], <, >',
invalid = ' can not contain any of following characters: [, ], <, >';
return {
title: 'Variable properties',
minWidth: 300,
minHeight: 80,
contents: [
{
id: 'info',
label: generalLabel,
title: generalLabel,
elements: [
// Dialog window UI elements.
{
id: 'name',
type: 'text',
style: 'width: 100%;',
label: 'Name',
'default': '',
required: true,
validate: CKEDITOR.dialog.validate.regex( validRegex, 'name'+emptyOrInvalid ),
setup: function( widget ) {
this.setValue( widget.data.name );
},
commit: function( widget ) {
widget.setData( 'name', this.getValue() );
}
},
{
id: 'defaultValue',
type: 'text',
style: 'width: 100%;',
label: 'Default Value',
'default': '',
required: true,
validate: CKEDITOR.dialog.validate.regex( validRegex, 'Default Value'+emptyOrInvalid ),
setup: function( widget ) {
this.setValue( widget.data.defaultValue );
},
commit: function( widget ) {
widget.setData( 'defaultValue', this.getValue() );
}
},
{
id: 'description',
type: 'text',
style: 'width: 100%;',
label: 'Description',
'default': '',
required: true,
validate: CKEDITOR.dialog.validate.regex( validRegex, 'Description'+invalid ),
setup: function( widget ) {
this.setValue( widget.data.description );
},
commit: function( widget ) {
widget.setData( 'description', this.getValue() );
}
}
]
}
]
};
} );

Resources