Ckeditor 5 image upload by default outputs <img src="url">, while I want to integrate UI kit images with output like <img uk-img data-src="url" data-srcset="sourcet">.
I am having trouble getting src and srcset from the uploaded image in downcastCustomImg() function, because viewElement is an EmptyElement class and getIdentity() method returns an empty img without attributes.
My idea is to convert src to data-src and srcset to data-srceset, also add empty uk-img.
How do I achieve that?
P.s. using vue.js 3 with cli
I am trying to use the official guide. my code is as follows:
export default class CustomFigureAttributes {
/**
* Plugin's constructor - receives an editor instance on creation.
*/
constructor( editor ) {
// Save reference to the editor.
this.editor = editor;
}
/**
* Sets the conversion up and extends the table & image features schema.
*
* Schema extending must be done in the "afterInit()" call because plugins define their schema in "init()".
*/
afterInit() {
const editor = this.editor;
editor.model.schema.extend('image', {
allowAttributes: ['data-src', 'data-srcset', 'uk-img']
});
editor.conversion.for( 'upcast' ).add( upcastCustomClasses( 'figure' ), { priority: 'low' } );
editor.conversion.for( 'downcast' ).add( downcastCustomImg( 'img', 'image' ), { priority: 'low' } );
// Define custom attributes that should be preserved.
setupCustomAttributeConversion( 'img', 'image', 'uk-img', editor );
setupCustomAttributeConversion( 'img', 'image', 'src', editor );
setupCustomAttributeConversion( 'img', 'image', 'srcset', editor );
}
}
function downcastCustomImg( viewElementName, modelElementName ) {
return dispatcher => dispatcher.on( `insert:${ modelElementName }`, ( evt, data, conversionApi ) => {
if ( conversionApi.consumable.consume( data.item, 'insert' ) ) {
return;
}
const modelElement = data.item;
const viewFigure = conversionApi.mapper.toViewElement( modelElement );
const viewElement = findViewChild( viewFigure, viewElementName, conversionApi );
if ( !viewElement ) {
return;
}
const src = viewElement.getAttribute( 'src' ) || [];
// CANNOT GET SRC & SRCSET from viewElement, because it is an EmptyElement !!!
conversionApi.writer.setAttribute( 'uk-img', '', viewElement );
conversionApi.writer.setAttribute( 'data-src', src, viewElement );
// conversionApi.writer.setAttribute( 'data-srcset', srcSet, viewElement );
} );
}
/**
* Helper method that searches for a given view element in all children of the model element.
*
* #param {module:engine/view/item~Item} viewElement
* #param {String} viewElementName
* #param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
* #return {module:engine/view/item~Item}
*/
function findViewChild( viewElement, viewElementName, conversionApi ) {
const viewChildren = Array.from( conversionApi.writer.createRangeIn( viewElement ).getItems() );
return viewChildren.find( item => item.is( 'element', viewElementName ) );
}
/**
* Sets up a conversion for a custom attribute on the view elements contained inside a <figure>.
*
* This method:
* - Adds proper schema rules.
* - Adds an upcast converter.
* - Adds a downcast converter.
*/
function setupCustomAttributeConversion( viewElementName, modelElementName, viewAttribute, editor ) {
// Extends the schema to store an attribute in the model.
const modelAttribute = `${ viewAttribute }`;
editor.model.schema.extend( modelElementName, { allowAttributes: [ modelAttribute ] } );
editor.conversion.for( 'upcast' ).add( upcastAttribute( viewElementName, viewAttribute, modelAttribute ) );
editor.conversion.for( 'downcast' ).add( downcastAttribute( modelElementName, viewElementName, viewAttribute, modelAttribute ) );
}
/**
* Returns the custom attribute upcast converter.
*/
function upcastAttribute( viewElementName, viewAttribute, modelAttribute ) {
return dispatcher => dispatcher.on( `element:${ viewElementName }`, ( evt, data, conversionApi ) => {
const viewItem = data.viewItem;
const modelRange = data.modelRange;
const modelElement = modelRange && modelRange.start.nodeAfter;
if ( !modelElement ) {
return;
}
conversionApi.writer.setAttribute( modelAttribute, viewItem.getAttribute( viewAttribute ), modelElement );
} );
}
/**
* Returns the custom attribute downcast converter.
*/
function downcastAttribute( modelElementName, viewElementName, viewAttribute, modelAttribute ) {
return dispatcher => dispatcher.on( `insert:${ modelElementName }`, ( evt, data, conversionApi ) => {
const modelElement = data.item;
const viewFigure = conversionApi.mapper.toViewElement( modelElement );
const viewElement = findViewChild( viewFigure, viewElementName, conversionApi );
if ( !viewElement ) {
return;
}
conversionApi.writer.setAttribute( viewAttribute, modelElement.getAttribute( modelAttribute ), viewElement );
} );
}
Related
I could not use the extension of level information(Autodesk.AEC.LevelsExtension) directly and I tried to use a workaround which is described below link.
https://forge.autodesk.com/blog/add-data-visualization-heatmaps-rooms-non-revit-model-part-i-nwc
However, it did not work for me. I tried it as in the below code. But, when i tried to print length of the dbIds it returns zero. So, I could not fill the levels. What can be the problem and why the layer searching is not worked?
async function findLevels(model) {
return new Promise((resolve, reject) => {
viewer.model.search(
'Layer',
(dbIds) => {
const levels = [];
const tree = viewer.model.getData().instanceTree;
console.log('Db Ids: ' + dbIds.length);
for( let i=0; i<dbIds.length; i++ ) {
const dbId = dbIds[i];
const name = tree.getNodeName( dbId );
if( name.includes( '<No level>' ) ) continue;
levelsGeo.push({
guid: dbId,
name,
dbId,
extension: {
buildingStory: true,
structure: false,
computationHeight: 0,
groundPlane: false,
hasAssociatedViewPlans: false,
}
});
}
resolve(levelsGeo);
},
reject,
['Icon']
);
});
}
here is my injected js:
window.ReactNativeWebView.postMessage(JSON.stringify({
external_url_open: null,
height: Math.max(document.body.offsetHeight, document.body.scrollHeight, document.documentElement.clientHeight)
}));
(function(){
var attachEvent = function(elem, event, callback)
{
...
}
var all_links = document.querySelectorAll('a[href]');
if ( all_links ) {
for ( var i in all_links ) {
if ( all_links.hasOwnProperty(i) ) {
attachEvent(all_links[i], 'onclick', function(e){
if ( new RegExp( '^https?:\/\/www.dropbox\.com.*', 'gi' ).test( this.href ) ) {
// handle external URL
e.preventDefault();
window.ReactNativeWebView.postMessage(JSON.stringify({
external_url_open: this.href,
}));
}
else { window.location.href = this.href; }
});
}
}
}
})();
I want to receive the pages height on react native side with this code:
<WebView
...
javascriptEnabled={true}
onMessage={(event) => {
// retrieve event data
var data = event.nativeEvent.data; // maybe parse stringified JSON
try {
data = JSON.parse(data)
} catch ( e ) { }
// check if this message concerns us
if ( 'object' == typeof data && data.external_url_open ) {
if (data.external_url_open != null){
// proceed with URL open request
Linking.openURL(data.external_url_open);
}
else {
height = data.height;
console.log(data.page_height);
}
}
}}
injectedJavaScript={jsCode}
...
/>
the link handling part works but the height receive part doesn't and I dont see anything in console when refreshing the page. Can anyone help?
I just needed to relocate this else to the outer if:
else {
height = data.height;
console.log(data.page_height);
}
This is the first time that I cannot find an answer and have to write here. I am trying a three.js project with Vue.js. I have this error:
Failed to compile.
./node_modules/three-gltf-loader/index.js
Module not found: Error: Can't resolve 'three' in 'C:\Users\Skunk\Documents\dolfin\dolfin\node_modules\three-gltf-loader'
My code:
import * as THREE from 'three-js';
import GLTFLoader from 'three-gltf-loader';
export default {
name: 'HelloWorld',
props: {
msg: String
},
mounted(){
let scene = new THREE.Scene( );
let camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerWidth / window.innerHeight, 0.1, 1000);
let renderer = new THREE.WebGLRenderer( );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
window.addEventListener( 'resize', function( )
{
let width = window.innerWidth;
let height = window.innerHeight;
renderer.setSize( width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix( );
} );
const loader = new GLTFLoader();
// Load a glTF resource
loader.load(
// resource URL
'models/dol.gltf',
// called when the resource is loaded
function ( gltf ) {
scene.add( gltf.scene );
gltf.animations; // Array<THREE.AnimationClip>
gltf.scene; // THREE.Group
gltf.scenes; // Array<THREE.Group>
gltf.cameras; // Array<THREE.Camera>
gltf.asset; // Object
},
// called while loading is progressing
function ( xhr ) {
console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );
}
);
camera.position.z = 3;
//let ambientLight = new THREE.AmbientLight( 0xFFFFFF, 0.8);
// scene.add( ambientLight );
// game logic
let update = function ( )
{};
// draw scene
let render = function( )
{
renderer.render( scene, camera );
};
// run game loop
let GameLoop = function( )
{
requestAnimationFrame( GameLoop );
update( );
render( );
};
GameLoop( );
}
}
Am I using pieces of code that are not compatible?
For starters, you should not be using the "three-js" node module. This is a really outdated version of Three that got stuck on r79 and hasn't been updated in 4 years. Instead, you should be using the official "three" node module, which is the legitimate library, and is currently on r124.
Second, just import the GLTF loader from the "three/examples" folder as demonstrated in the GLTF examples, instead of installing a whole new module.
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
const loader = new GLTFLoader();
I'm using default example of NormalPeoplePicker from https://developer.microsoft.com/en-us/fluentui#/controls/web/peoplepicker#IPeoplePickerProps.
When the dropdown displays it cuts off longer items (example: 'Anny Lundqvist, Junior Manager of Soft..'). How do I make it wider, so that the full item's text displays?
import * as React from 'react';
import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
import { IPersonaProps } from 'office-ui-fabric-react/lib/Persona';
import { IBasePickerSuggestionsProps, NormalPeoplePicker, ValidationState } from 'office-ui-fabric-react/lib/Pickers';
import { people, mru } from '#uifabric/example-data';
const suggestionProps: IBasePickerSuggestionsProps = {
suggestionsHeaderText: 'Suggested People',
mostRecentlyUsedHeaderText: 'Suggested Contacts',
noResultsFoundText: 'No results found',
loadingText: 'Loading',
showRemoveButtons: true,
suggestionsAvailableAlertText: 'People Picker Suggestions available',
suggestionsContainerAriaLabel: 'Suggested contacts',
};
const checkboxStyles = {
root: {
marginTop: 10,
},
};
export const PeoplePickerNormalExample: React.FunctionComponent = () => {
const [delayResults, setDelayResults] = React.useState(false);
const [isPickerDisabled, setIsPickerDisabled] = React.useState(false);
const [mostRecentlyUsed, setMostRecentlyUsed] = React.useState<IPersonaProps[]>(mru);
const [peopleList, setPeopleList] = React.useState<IPersonaProps[]>(people);
const picker = React.useRef(null);
const onFilterChanged = (
filterText: string,
currentPersonas: IPersonaProps[],
limitResults?: number,
): IPersonaProps[] | Promise<IPersonaProps[]> => {
if (filterText) {
let filteredPersonas: IPersonaProps[] = filterPersonasByText(filterText);
filteredPersonas = removeDuplicates(filteredPersonas, currentPersonas);
filteredPersonas = limitResults ? filteredPersonas.slice(0, limitResults) : filteredPersonas;
return filterPromise(filteredPersonas);
} else {
return [];
}
};
const filterPersonasByText = (filterText: string): IPersonaProps[] => {
return peopleList.filter(item => doesTextStartWith(item.text as string, filterText));
};
const filterPromise = (personasToReturn: IPersonaProps[]): IPersonaProps[] | Promise<IPersonaProps[]> => {
if (delayResults) {
return convertResultsToPromise(personasToReturn);
} else {
return personasToReturn;
}
};
const returnMostRecentlyUsed = (currentPersonas: IPersonaProps[]): IPersonaProps[] | Promise<IPersonaProps[]> => {
return filterPromise(removeDuplicates(mostRecentlyUsed, currentPersonas));
};
const onRemoveSuggestion = (item: IPersonaProps): void => {
const indexPeopleList: number = peopleList.indexOf(item);
const indexMostRecentlyUsed: number = mostRecentlyUsed.indexOf(item);
if (indexPeopleList >= 0) {
const newPeople: IPersonaProps[] = peopleList
.slice(0, indexPeopleList)
.concat(peopleList.slice(indexPeopleList + 1));
setPeopleList(newPeople);
}
if (indexMostRecentlyUsed >= 0) {
const newSuggestedPeople: IPersonaProps[] = mostRecentlyUsed
.slice(0, indexMostRecentlyUsed)
.concat(mostRecentlyUsed.slice(indexMostRecentlyUsed + 1));
setMostRecentlyUsed(newSuggestedPeople);
}
};
const onDisabledButtonClick = (): void => {
setIsPickerDisabled(!isPickerDisabled);
};
const onToggleDelayResultsChange = (): void => {
setDelayResults(!delayResults);
};
return (
<div>
<NormalPeoplePicker
// eslint-disable-next-line react/jsx-no-bind
onResolveSuggestions={onFilterChanged}
// eslint-disable-next-line react/jsx-no-bind
onEmptyInputFocus={returnMostRecentlyUsed}
getTextFromItem={getTextFromItem}
pickerSuggestionsProps={suggestionProps}
className={'ms-PeoplePicker'}
key={'normal'}
// eslint-disable-next-line react/jsx-no-bind
onRemoveSuggestion={onRemoveSuggestion}
onValidateInput={validateInput}
removeButtonAriaLabel={'Remove'}
inputProps={{
onBlur: (ev: React.FocusEvent<HTMLInputElement>) => console.log('onBlur called'),
onFocus: (ev: React.FocusEvent<HTMLInputElement>) => console.log('onFocus called'),
'aria-label': 'People Picker',
}}
componentRef={picker}
onInputChange={onInputChange}
resolveDelay={300}
disabled={isPickerDisabled}
/>
<Checkbox
label="Disable People Picker"
checked={isPickerDisabled}
// eslint-disable-next-line react/jsx-no-bind
onChange={onDisabledButtonClick}
styles={checkboxStyles}
/>
<Checkbox
label="Delay Suggestion Results"
defaultChecked={delayResults}
// eslint-disable-next-line react/jsx-no-bind
onChange={onToggleDelayResultsChange}
styles={checkboxStyles}
/>
</div>
);
};
function doesTextStartWith(text: string, filterText: string): boolean {
return text.toLowerCase().indexOf(filterText.toLowerCase()) === 0;
}
function removeDuplicates(personas: IPersonaProps[], possibleDupes: IPersonaProps[]) {
return personas.filter(persona => !listContainsPersona(persona, possibleDupes));
}
function listContainsPersona(persona: IPersonaProps, personas: IPersonaProps[]) {
if (!personas || !personas.length || personas.length === 0) {
return false;
}
return personas.filter(item => item.text === persona.text).length > 0;
}
function convertResultsToPromise(results: IPersonaProps[]): Promise<IPersonaProps[]> {
return new Promise<IPersonaProps[]>((resolve, reject) => setTimeout(() => resolve(results), 2000));
}
function getTextFromItem(persona: IPersonaProps): string {
return persona.text as string;
}
function validateInput(input: string): ValidationState {
if (input.indexOf('#') !== -1) {
return ValidationState.valid;
} else if (input.length > 1) {
return ValidationState.warning;
} else {
return ValidationState.invalid;
}
}
/**
* Takes in the picker input and modifies it in whichever way
* the caller wants, i.e. parsing entries copied from Outlook (sample
* input: "Aaron Reid <aaron>").
*
* #param input The text entered into the picker.
*/
function onInputChange(input: string): string {
const outlookRegEx = /<.*>/g;
const emailAddress = outlookRegEx.exec(input);
if (emailAddress && emailAddress[0]) {
return emailAddress[0].substring(1, emailAddress[0].length - 1);
}
return input;
}
Component which renders suggestion list have fixed width of 180px. Take a look at PeoplePickerItemSuggestion.styles.ts.
What you can do is to modify this class .ms-PeoplePicker-Persona:
.ms-PeoplePicker-Persona {
width: 260px; // Or what ever you want
}
UPDATE - Solution from comments
Change width trough styles property of PeoplePickerItemSuggestion Component
const onRenderSuggestionsItem = (personaProps, suggestionsProps) => (
<PeoplePickerItemSuggestion
personaProps={personaProps}
suggestionsProps={suggestionsProps}
styles={{ personaWrapper: { width: '100%' }}}
/>
);
<NormalPeoplePicker
onRenderSuggestionsItem={onRenderSuggestionsItem}
pickerCalloutProps={{ calloutWidth: 500 }}
...restProps
/>
Working Codepen example
For more information how to customize components read Component Styling.
I have create a custom build of CKEditor5 and created a plugin BorderColor. The Plugin isn't working good. The problem is that the model is changing, but the view isn't changed.
border-color.js
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
import BorderColorEditing from './border-color-editing';
import BorderColorUi from './border-color-ui';
export default class BorderColor extends Plugin {
/**
* #inheritDoc
*/
static get requires() {
return [ BorderColorEditing, BorderColorUi ];
}
/**
* #inheritDoc
*/
static get pluginName() {
return 'BorderColor';
}
}
border-color-ui.js
import ColorUI from '#ckeditor/ckeditor5-font/src/ui/colorui';
export default class BorderColorUi extends ColorUI {
/**
* #inheritDoc
*/
constructor( editor ) {
const t = editor.locale.t;
super( editor, {
commandName: 'borderColor',
componentName: 'borderColor',
dropdownLabel: t( 'Rahmenfarbe' )
} );
}
/**
* #inheritDoc
*/
static get pluginName() {
return 'BorderColorUI';
}
}
border-color-editing.js
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
import BorderColorCommand from './border-color-command';
import { isDefault, isSupported, supportedOptions } from './utils';
export default class BorderColorEditing extends Plugin {
/**
* #inheritDoc
*/
constructor( editor ) {
super( editor );
editor.config.define( 'borderColor', supportedOptions );
}
/**
* #inheritDoc
*/
init() {
const editor = this.editor;
const schema = editor.model.schema;
// Filter out unsupported options.
const enabledOptions = editor.config.get( 'borderColor.colors' );
// Allow alignment attribute on all blocks.
schema.extend( '$block', { allowAttributes: 'borderColor' } );
editor.model.schema.setAttributeProperties( 'borderColor', { isFormatting: true } );
editor.model.schema.addAttributeCheck( ( context, attributeName ) => {
if ( context.endsWith( 'table' ) || context.endsWith( 'tableRow' ) || context.endsWith( 'tableCell' )) {
return true;
}
} );
const definition = _buildDefinition( enabledOptions.filter( option => !isDefault( option ) ) );
editor.conversion.attributeToAttribute( definition );
editor.commands.add( 'borderColor', new BorderColorCommand( editor ) );
}
}
// Utility function responsible for building converter definition.
// #private
function _buildDefinition( options ) {
const definition = {
model: {
key: 'borderColor',
values: options.slice()
},
view: {}
};
for ( const option of options ) {
const _def = { key: 'style', value: {} };
_def.value[ 'border-color' ] = option.color;
definition.view[ option ] = _def;
}
return definition;
}
border-color-command.js
import Command from '#ckeditor/ckeditor5-core/src/command';
import first from '#ckeditor/ckeditor5-utils/src/first';
import { isDefault } from './utils';
const BORDER_COLOR = 'borderColor';
export default class BorderColorCommand extends Command {
/**
* #inheritDoc
*/
refresh() {
let childBlocks;
if ( this.editor.model.document.selection.getSelectedElement() != null )
childBlocks = this.getSelectedBlocks( this.editor.model.document, 'all-tablerow' );
const firstBlock = this.editor.model.document.selection.getSelectedElement() != null
? this.editor.model.document.selection.getSelectedElement()
: first( this.editor.model.document.selection.getSelectedBlocks() );
// As first check whether to enable or disable the command as the value will always be false if the command cannot be enabled.
this.isEnabled = !!firstBlock && this._canBeAligned( firstBlock );
/**
* A value of the current block's alignment.
*
* #observable
* #readonly
* #member {String} #value
*/
if ( this.isEnabled && firstBlock.hasAttribute( BORDER_COLOR ) )
this.value = firstBlock.getAttribute( BORDER_COLOR );
else if ( this.isEnabled && childBlocks && childBlocks[ 0 ].hasAttribute( BORDER_COLOR ) )
this.value = childBlocks[ 0 ].getAttribute( BORDER_COLOR );
else
this.value = '';
}
/**
* Executes the command. Applies the alignment `value` to the selected blocks.
* If no `value` is passed, the `value` is the default one or it is equal to the currently selected block's alignment attribute,
* the command will remove the attribute from the selected blocks.
*
* #param {Object} [options] Options for the executed command.
* #param {String} [options.value] The value to apply.
* #fires execute
*/
execute( options = {} ) {
const editor = this.editor;
const model = editor.model;
const doc = model.document;
const value = options.value;
model.change( writer => {
let childBlocks;
if ( this.editor.model.document.selection.getSelectedElement() != null )
childBlocks = this.getSelectedBlocks( this.editor.model.document, 'all-tablerow' );
const firstBlock = this.editor.model.document.selection.getSelectedElement() != null
? this.editor.model.document.selection.getSelectedElement()
: first( this.editor.model.document.selection.getSelectedBlocks() );
let currentAlignment;
if ( firstBlock.hasAttribute( BORDER_COLOR ) )
currentAlignment = firstBlock.getAttribute( BORDER_COLOR );
else if ( childBlocks && childBlocks[ 0 ].hasAttribute( BORDER_COLOR ) )
currentAlignment = childBlocks[ 0 ].getAttribute( BORDER_COLOR );
// Remove alignment attribute if current alignment is:
// - default (should not be stored in model as it will bloat model data)
// - equal to currently set
// - or no value is passed - denotes default alignment.
const removeAlignment = isDefault( value ) || currentAlignment === value || !value;
console.log( 'childBlocks / firstBlock', childBlocks, firstBlock, currentAlignment);
const blocks = childBlocks ? Array.from( childBlocks ) : [];
blocks.push( firstBlock );
if ( removeAlignment ) {
removeAlignmentFromSelection( blocks, writer );
} else {
setAlignmentOnSelection( blocks, writer, value );
}
} );
}
getSelectedBlocks( doc, value ) {
let blocks;
if ( doc.selection.getSelectedElement() == null )
blocks = Array.from( doc.selection.getSelectedBlocks() ).filter( block => this._canBeAligned( block ) )
else {
blocks = [ doc.selection.getSelectedElement() ];
if ( value.indexOf( 'tablerow' ) > -1 ) {
var children = doc.selection.getSelectedElement().getChildren();
console.log( 'Selected Element Children', children );
var out = [];
let _next = children.next();
while ( !_next.done ) {
console.log( _next );
if ( _next.value.getChildren ) {
const children2 = _next.value.getChildren();
let _next2 = children2.next();
while ( !_next2.done ) {
console.log( _next2 );
out[ out.length ] = _next2.value;
_next2 = children2.next();
}
}
else {
out[ out.length ] = _next.value;
}
_next = children.next();
}
blocks = out;
}
}
return blocks;
}
/**
* Checks whether a block can have alignment set.
*
* #private
* #param {module:engine/model/element~Element} block The block to be checked.
* #returns {Boolean}
*/
_canBeAligned( block ) {
return this.editor.model.schema.checkAttribute( block, BORDER_COLOR );
}
}
// Removes the alignment attribute from blocks.
// #private
function removeAlignmentFromSelection( blocks, writer ) {
for ( const block of blocks ) {
writer.removeAttribute( BORDER_COLOR, block );
}
}
// Sets the alignment attribute on blocks.
// #private
function setAlignmentOnSelection( blocks, writer, border ) {
for ( const block of blocks ) {
writer.setAttribute( BORDER_COLOR, border, block );
}
}
The code can also be seen at Github: CKEditor Custom Build Github.
I have found a solution.
I edited border-color-editing.js:
I replaced following code:
editor.conversion.attributeToAttribute( definition );
with:
editor.conversion.for( 'downcast' ).attributeToAttribute( {
model: 'borderColor',
view: function( option ) {
const allSelectedBlocks = getAllSelectedBlocks( this.editor );
const borderAttribute = allSelectedBlocks[ 0 ].getAttribute( BORDER );
if ( !borderAttribute || !option )
return { key: 'style', value: '' };
const _value = {};
const styleOption = borderAttribute.replace( '-tablerow', '' );
_value[ 'border' + ( styleOption == 'all' ? '' : '-' + styleOption ) + '-color' ] = option ? option : 'black';
return { key: 'style', value: _value };
}.bind( this )
} );