Draft-JS - Entity component doesn't re-render when data changes - draftjs

I would like to have my text editor display how many times a content block has been selected and executed though a key command.
I'm doing this by applying an entity to the selected block with a evaluatedTimes property.
The data is changed correctly but the entity component doesn't re-render until I insert new characters in the block of text.
The decorator strategy of the entity doesn't get called, so the only way out I found is to update the editor state with a new decorator instance. This way the entity components get re-rendered but the solution fills a bit hacky.
dependencies:
"draft-js": "^0.11.3",
"draft-js-plugins-editor": "^3.0.0",
"draftjs-utils": "^0.10.2"
code:
// plugin.js
import { EditorState } from "draft-js";
import { getSelectionText, getSelectionEntity } from "draftjs-utils";
import { EvaluatedSpan } from "./components";
import { findEvaluatedEntities, createEvaluatedEntity } from "./entities";
export function createCodeEvaluationPlugin({ onEvaluate = () => {} }) {
return {
decorators: [
{
strategy: findEvaluatedEntities,
component: EvaluatedSpan
}
],
keyBindingFn: e => {
// CMD + ENTER
if (e.metaKey && e.keyCode === 13) {
return "evaluate";
}
},
handleKeyCommand: (command, editorState, _, { setEditorState }) => {
if (command === "evaluate") {
const selectionState = editorState.getSelection();
const contentState = editorState.getCurrentContent();
const entityKey = getSelectionEntity(editorState);
// If selection contains an entity:
if (!entityKey) {
// Create new entity and update editor.
setEditorState(createEvaluatedEntity(editorState, selectionState));
} else {
// Modify entity data.
const entity = contentState.getEntity(entityKey);
const nextContentState = contentState.mergeEntityData(entityKey, {
evaluatedTimes: entity.data.evaluatedTimes + 1
});
// Update editor.
setEditorState(
EditorState.push(editorState, nextContentState, "change-block-data")
);
}
// Pass text to callback handle
const selectionText = getSelectionText(editorState);
onEvaluate(selectionText);
return "handled";
}
return "not-handled";
}
};
}

According to this (https://github.com/facebook/draft-js/issues/1702) you could also update the selection state, which may be sufficient for you.

Works for me:
function forceRerender(editorState: EditorState): EditorState {
const selectionState = editorState.getSelection();
return EditorState.forceSelection(editorState, selectionState);
}

Related

AG Grid: Framework component is missing the method getValue() Vue3 Composition API with expose

I'm currently using ag-grid to render data and it works fine untill I try to edit cells using my custom cellEditorFramework component:
export default defineComponent({
name: 'LinesViewVersionEditor',
props: ['params'],
setup(props, { expose }) {
const value = ref(props.params.value)
const versionOptions = ref([])
const changedValue = ref(false)
const client = new Client({ baseURL: settings.ClientBaseUrl })
const getValue = function () {
console.log('getValue')
return value.value
}
const updateValue = function (value: { version: number; entitySlug: string; entityVersionPk: number }) {
props.params.api.stopEditing()
changedValue.value = true
}
versionOptions.value = [
{
value: value.value,
label: value.value?.version.toString()
}
]
...some code here
expose({
value,
getValue
})
return () => (
<Select
showArrow={false}
class={'ant-select-custom'}
value={value.value?.version}
options={versionOptions.value}
onChange={ value => { updateValue(value) } }
onClick={ async () => {
versionOptions.value = await getChildVersions(
client,
...args
)
}}
/>
)
}
})
As you can see I'm returning some TSX, so I'm forced to use Vue3 { expose } to return method to the parent component with agGrid table. And it has no access to exposed method & value. I tried to make different method in "methods" property of class component options and it worked as supposed. In ag-grid docs written that I can simply return getValue in setup() function but it doesn't work for me for no visible reason. Thank you in advance for help.

How can I send and receive a signal when a file loads in my GJS app?

I have an app that needs to open a file and update UI elements accordingly. I can select and open the file (and log the file contents), but I can't tell the UI elements to update.
I have tried read that I can create and add signals to just about any object, but I need to emit a signal from a function in an imported library.
I'm trying to do something like this:
(in my function that has read a file from disk)
try {
Signals.addSignalMethods(this);
this.emit('update_ui', true);
} catch(e) {
print(e);
}
(and in the main app class)
Signals.addSignalMethods(this);
this.connect('update_ui',() => {
try {
print('>>> updating UI');
this.ui.updateUI();
} catch (e) {
print(e);
}
});
I don't get any errors when I run the app, but the update function is never called.
How can I get the signal to go through?
Here's the code from the main.js file that should catch the signal :
#!/usr/bin/gjs
Gio = imports.gi.Gio;
GLib = imports.gi.GLib;
Gtk = imports.gi.Gtk;
Lang = imports.lang;
Webkit = imports.gi.WebKit2;
Signals = imports.signals;
GObject = imports.gi.GObject;
Pango = imports.gi.Pango;
//
// add app folder to path
//
function getAppFileInfo() {
let stack = (new Error()).stack,
stackLine = stack.split('\n')[1],
coincidence, path, file;
if (!stackLine) throw new Error('Could not find current file (1)');
coincidence = new RegExp('#(.+):\\d+').exec(stackLine);
if (!coincidence) throw new Error('Could not find current file (2)');
path = coincidence[1];
file = Gio.File.new_for_path(path);
return [file.get_path(), file.get_parent().get_path(), file.get_basename()];
}
const path = getAppFileInfo()[1];
imports.searchPath.push(path);
const myApp = new Lang.Class({
Name: 'My Application',
// Create the application itself
_init: function() {
this.application = new Gtk.Application();
// Connect 'activate' and 'startup' signals to the callback functions
this.application.connect('activate', Lang.bind(this, this._onActivate));
this.application.connect('startup', Lang.bind(this, this._onStartup));
},
// Callback function for 'activate' signal presents windows when active
_onActivate: function() {
this._window.present();
},
// Callback function for 'startup' signal builds the UI
_onStartup: function() {
this._buildUI();
},
// Build the application's UI
_buildUI: function() {
// Create the application window
this._window = new Gtk.ApplicationWindow({
application: this.application,
title: "My App",
default_height: 200,
default_width: 400,
window_position: Gtk.WindowPosition.CENTER
});
//
// menu bar
//
const Menubar = imports.lib.menubar;
this._window.set_titlebar(Menubar.getHeader());
Signals.addSignalMethods(this);
this.connect('update_ui', () => {
try {
print('>>> updating UI');
//this.ui.updateUI();
} catch (e) {
print(e);
}
});
// Vbox to hold the switcher and stack.
this._Vbox = new Gtk.VBox({
spacing: 6
});
this._Hbox = new Gtk.HBox({
spacing: 6,
homogeneous: true
});
// const UI = imports.UI.UI;
// this.ui = new UI.UIstack();
// this.ui._buildStack();
// this._Hbox.pack_start(this.ui._stack_switcher, true, true, 0);
this._Vbox.pack_start(this._Hbox, false, false, 0);
// this._Vbox.pack_start(this.ui._Stack, true, true, 0);
// Show the vbox widget
this._window.add(this._Vbox);
// Show the window and all child widgets
this._window.show_all();
},
});
// Run the application
const app = new myApp();
app.application.run(ARGV);
and here's the header bar file that emits the signal:
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;
const File = imports.lib.file;
const PopWidget = function(properties) {
let label = new Gtk.Label({
label: properties.label
});
let image = new Gtk.Image({
icon_name: 'pan-down-symbolic',
icon_size: Gtk.IconSize.SMALL_TOOLBAR
});
let widget = new Gtk.Grid();
widget.attach(label, 0, 0, 1, 1);
widget.attach(image, 1, 0, 1, 1);
this.pop = new Gtk.Popover();
this.button = new Gtk.ToggleButton();
this.button.add(widget);
this.button.connect('clicked', () => {
if (this.button.get_active()) {
this.pop.show_all();
}
});
this.pop.connect('closed', () => {
if (this.button.get_active()) {
this.button.set_active(false);
}
});
this.pop.set_relative_to(this.button);
this.pop.set_size_request(-1, -1);
this.pop.set_border_width(8);
this.pop.add(properties.widget);
};
const getHeader = function() {
let headerBar, headerStart, imageNew, buttonNew, popMenu, imageMenu, buttonMenu;
headerBar = new Gtk.HeaderBar();
headerBar.set_title("My App");
headerBar.set_subtitle("Some subtitle text here");
headerBar.set_show_close_button(true);
headerStart = new Gtk.Grid({
column_spacing: headerBar.spacing
});
// this.widgetOpen = new PopWidget({ label: "Open", widget: this.getPopOpen() });
imageNew = new Gtk.Image({
icon_name: 'document-open-symbolic',
icon_size: Gtk.IconSize.SMALL_TOOLBAR
});
buttonNew = new Gtk.Button({
image: imageNew
});
buttonNew.connect('clicked', () => {
const opener = new Gtk.FileChooserDialog({
title: 'Select a file'
});
opener.set_action(Gtk.FileChooserAction.OPEN);
opener.add_button('open', Gtk.ResponseType.ACCEPT);
opener.add_button('cancel', Gtk.ResponseType.CANCEL);
const res = opener.run();
if (res == Gtk.ResponseType.ACCEPT) {
const filename = opener.get_filename();
print(filename);
const fileData = File.open(filename);
print(JSON.stringify(fileData, null, 2));
File.unRoll(fileData);
//
// SHOULD SEND A SIGNAL
//
try {
Signals.addSignalMethods(this);
this.emit('update_ui', true);
} catch (e) {
print(e);
}
// this._window.ui.updateUI();
}
opener.destroy();
});
// headerStart.attach(this.widgetOpen.button, 0, 0, 1, 1);
headerStart.attach(buttonNew, 1, 0, 1, 1);
headerBar.pack_start(headerStart);
popMenu = new Gtk.Popover();
imageMenu = new Gtk.Image({
icon_name: 'document-save-symbolic',
icon_size: Gtk.IconSize.SMALL_TOOLBAR
});
buttonMenu = new Gtk.MenuButton({
image: imageMenu
});
buttonMenu.set_popover(popMenu);
popMenu.set_size_request(-1, -1);
buttonMenu.set_menu_model(this.getMenu());
headerBar.pack_end(buttonMenu);
return headerBar;
};
const getPopOpen = function() {
/* Widget popover */
let widget = new Gtk.Grid(),
label = new Gtk.Label({
label: "Label 1"
}),
button = new Gtk.Button({
label: "Other Documents ..."
});
button.connect('clicked', () => {
this.widgetOpen.pop.hide();
this.printText('Open other documents');
});
button.set_size_request(200, -1);
widget.attach(label, 0, 0, 1, 1);
widget.attach(button, 0, 1, 1, 1);
widget.set_halign(Gtk.Align.CENTER);
return widget;
};
const getMenu = function() {
/* GMenu popover */
let menu, section, submenu;
menu = new Gio.Menu();
section = new Gio.Menu();
section.append("Save As...", 'app.saveAs');
section.append("Save All", 'app.saveAll');
menu.append_section(null, section);
section = new Gio.Menu();
submenu = new Gio.Menu();
section.append_submenu('View', submenu);
submenu.append("View something", 'app.toggle');
submenu = new Gio.Menu();
section.append_submenu('Select', submenu);
submenu.append("Selection 1", 'app.select::one');
submenu.append("Selection 2", 'app.select::two');
submenu.append("Selection 3", 'app.select::thr');
menu.append_section(null, section);
section = new Gio.Menu();
section.append("Close All", 'app.close1');
section.append("Close", 'app.close2');
menu.append_section(null, section);
// Set menu actions
let actionSaveAs = new Gio.SimpleAction({
name: 'saveAs'
});
actionSaveAs.connect('activate', () => {
const saver = new Gtk.FileChooserDialog({
title: 'Select a destination'
});
saver.set_action(Gtk.FileChooserAction.SAVE);
saver.add_button('save', Gtk.ResponseType.ACCEPT);
saver.add_button('cancel', Gtk.ResponseType.CANCEL);
const res = saver.run();
if (res == Gtk.ResponseType.ACCEPT) {
const filename = saver.get_filename();
print(filename);
const data = File.rollUp();
File.save(filename, data);
// let data = JSON.stringify(<FILE DATA>, null, '\t');
// GLib.file_set_contents(filename, data);
}
saver.destroy();
});
APP.add_action(actionSaveAs);
let actionSaveAll = new Gio.SimpleAction({
name: 'saveAll'
});
actionSaveAll.connect('activate', () => {
Gtk.FileChooserAction.OPEN
});
APP.add_action(actionSaveAll);
let actionClose1 = new Gio.SimpleAction({
name: 'close1'
});
actionClose1.connect('activate', () => {
this.printText('Action close all');
});
APP.add_action(actionClose1);
let actionClose2 = new Gio.SimpleAction({
name: 'close2'
});
actionClose2.connect('activate', () => {
this.printText('Action close');
});
APP.add_action(actionClose2);
let actionToggle = new Gio.SimpleAction({
name: 'toggle',
state: new GLib.Variant('b', true)
});
actionToggle.connect('activate', (action) => {
let state = action.get_state().get_boolean();
if (state) {
action.set_state(new GLib.Variant('b', false));
} else {
action.set_state(new GLib.Variant('b', true));
}
this.printText('View ' + state);
});
APP.add_action(actionToggle);
let variant = new GLib.Variant('s', 'one');
let actionSelect = new Gio.SimpleAction({
name: 'select',
state: variant,
parameter_type: variant.get_type()
});
actionSelect.connect('activate', (action, parameter) => {
let str = parameter.get_string()[0];
if (str === 'one') {
action.set_state(new GLib.Variant('s', 'one'));
}
if (str === 'two') {
action.set_state(new GLib.Variant('s', 'two'));
}
if (str === 'thr') {
action.set_state(new GLib.Variant('s', 'thr'));
}
this.printText('Selection ' + str);
});
APP.add_action(actionSelect);
return menu;
};
it's not a class, just a static library... it's imported by the main.js file, so I would think the scope would be the same, but maybe not...
I'm going to over-answer while I get to your question again, because I see some difficulties and some "best practices" you could be using. I think it's much preferrable to use a direct subclass of GtkApplication and use ES6 classes.
const Gio = imports.gi.Gio;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
var MyApp = GObject.registerClass({
// This must be unique, although I don't believe you have to specify it
GTypeName: 'MyApp',
// GObject already have their own signal system, so invoking addSignalMethods()
// will override the existing methods and could cause you problems. Here's a
// simple example of how you'd add a GObject signal to a subclass; the resulting
// callback would look like:
//
// myapp.connect('update-ui', (application, bool) => { ... });
Signals: {
'update-ui': {
param_types: [GObject.TYPE_BOOLEAN]
},
}
}, class MyApp extends Gtk.Application {
_init(params) {
// You could pass the regular params straight through
//super._init(params);
// Or setup your class to be initted without having to pass any args in your
// constructor
super._init({
// This will also become your well-known name on DBus, with the matching
// object path of /org/github/username/MyApp.
//
// GApplication framework will also use these for other things like any
// GResource you bundle with your application.
application_id: 'com.github.username.MyApp',
// There are other flags you can set to allow your app to be a target for
// "Open with...", but that's not what you're asking about today :)
flags: Gio.ApplicationFlags.HANDLES_OPEN
});
// You can also do other setup , however it's important to note that
// GApplication's are generally "single-instance" processes, so you want most
// of that setup in ::startup which will only run if this is the primary
// instance.
//
// On the other hand if you pass HANDLES_OPEN, for example, that signal/vfunc
// will still be invoked.
GLib.set_prgname(this.application_id);
GLib.set_application_name('MyApp');
}
_buildUI() {
// ...
}
// defining a virtual function usually means overriding the default handler for a
// a signal. In this case, it's not much of an issue but for frequently emitted
// signals it avoids "marshalling" the C values into JS values and back again.
//
// In other words, defining this function means it will be called as if you
// connected to the ::activate signal.
vfunc_activate() {
this._window.present();
}
// ::activate is a special case, but for most vfunc's is necessary to "chain up",
// which really just means calling the super class's original function inside your
// override.
vfunc_startup() {
// In ::startup we need to chain up first since GApplication does important
// setup checks here
super.vfunc_startup();
// your stuff after
this._buildUI();
// In _buildUI() you correctly passed your GtkApplication as the application
// property, which will keep the application running so long as that window
// is open.
//
// If you wanted your application to stay open regardless, you call hold()
this.hold();
}
vfunc_shutdown() {
// ::shutdown on the other hand is the reverse; first we do our stuff, then
// chain up to super class. This is a good time to do any cleanup you need
// before the process exits.
this._destroyUI();
// chain up
super.vfunc_shutdown();
}
});
// For technical reasons, this is the proper way you should start your application
(new MyApp()).run([imports.system.programInvocationName].concat(ARGV));
The addSignalMethods() function only needs to be called once, but as mentioned it will override the existing signal methods and that could cause you problems if you call it on a GObject that already has signals defined. It basically does this:
addSignalMethods(obj) {
obj.emit = function() {
// custom signal code
}
...
It was really meant for pure JS classes that don't have a signal system already.
// SHOULD SEND A SIGNAL
//
try {
Signals.addSignalMethods(this);
this.emit('update_ui', true);
} catch (e) {
print(e);
}
The reason why this wasn't working for you is that getHeaderbar() is a top-level function in imports.lib.menubar so when you called addSignalMethods(this), this === imports.lib.menubar. Therefore to catch that signal you would had to call:
imports.lib.menubar.connect('update_ui', () => {});
When you called it in _buildUI():
Signals.addSignalMethods(this);
this.connect('update_ui', () => {
try {
print('>>> updating UI');
//this.ui.updateUI();
} catch (e) {
print(e);
}
});
this === MyApp, so you were first overriding MyApp's signal methods, then connecting to itself. The Signals import is fairly lax so it doesn't require a signal to be defined before use; you just connect to what you want and emit what you want. This is why you weren't getting any errors or warnings.
There are couple ways you can solve your problem:
// (1) Pick an object to emit and connect from and only add the signal methods once
Signals.addSignalMethods(headerBar);
// Since you're using an arrow function you can call this in the same place since it
// should still be in scope
headerBar.emit('update_ui', true);
// You can connect by grabbing the headerbar widget from your constructed window
this._window.get_titlebar().connect('update_ui', (headerBar, bool) => {});
// (2) Define a signal *on* your GtkApplication and use it as a relay
// Connect from a function in MyApp (so in `this.connect`, `this === MyApp`)
// To set `this` for the callback itself, use `Function.bind()`
this.connect('update-ui', this.ui.updateUI.bind(ui));
// You can always grab the primary instance of your GtkApplication (from anywhere)
// and emit the signal to invoke the MyApp.ui.updateUI() function
let myApp = Gio.Application.get_default();
myApp.emit('update-ui', true);
// (3) How I'd probably do it; just invoke the method directly
let myApp = Gio.Application.get_default();
myApp.ui.updateUI();

Why TextDocumentContentProvider dont call provideTextDocumentContent on update when query params changes?

as title says, when i wanna update TextDocumentContentProvider with different query params by calling update method provideTextDocumentContent is not called...
only way i managed to get it working was with same URI as in calling
vscode.commands.executeCommand('vscode.previewHtml', URI, 2, 'Storybook');
relevant part of code:
// calculates uri based on editor state - depends on actual caret position
// all uris will start with 'storybook://preview'
function getPreviewUri(editor: vscode.TextEditor): vscode.Uri;
// transforms uri, so web server will understand
// ex: 'storybook://preview?name=fred' -> 'http://localhost:12345/preview/fred?full=1'
function transformUri(uri: vscode.Uri): vscode.Uri;
class StorybookContentProvider implements vscode.TextDocumentContentProvider
{
provideTextDocumentContent(uri: vscode.Uri): string {
var httpUri = transformUri(uri);
return `<iframe src="${httpUri}" />`;
}
onDidChange = new vscode.EventEmitter<vscode.Uri>();
update(uri: vscode.Uri) {
this.onDidChange(uri);
}
}
export function activate(context: vscode.ExtensionContext)
{
vscode.workspace.onDidChangeTextDocument(
(e: vscode.TextDocumentChangeEvent) => {
if (e.document === vscode.window.activeTextEditor.document) {
const previewUri = getPreviewUri(vscode.window.activeTextEditor);
provider.update(previewUri);
}
}
);
vscode.window.onDidChangeTextEditorSelection(
(e: vscode.TextEditorSelectionChangeEvent) => {
if (e.textEditor === vscode.window.activeTextEditor) {
const previewUri = getPreviewUri(vscode.window.activeTextEditor);
provider.update(previewUri);
}
}
);
const provider = new StorybookContentProvider();
context.subscriptions.push(
vscode.commands.registerCommand('extension.showStorybook', () => {
vscode.commands.executeCommand('vscode.previewHtml', vscode.Uri.parse('storybook://preview'), 2, 'Storybook')
}),
vscode.workspace.registerTextDocumentContentProvider('storybook', provider)
);
}

How to use node-simple-schema reactively?

Given that there is not much examples about this, I am following the docs as best as I can, but the validation is not reactive.
I declare a schema :
import { Tracker } from 'meteor/tracker';
import SimpleSchema from 'simpl-schema';
export const modelSchema = new SimpleSchema({
foo: {
type: String,
custom() {
setTimeout(() => {
this.addValidationErrors([{ name: 'foo', type: 'notUnique' }]);
}, 100); // simulate async
return false;
}
}
}, {
tracker: Tracker
});
then I use this schema in my component :
export default class InventoryItemForm extends TrackerReact(Component) {
constructor(props) {
super(props);
this.validation = modelSchema.newContext();
this.state = {
isValid: this.validation.isValid()
};
}
...
render() {
...
const errors = this.validation._validationErrors;
return (
...
)
}
}
So, whenever I try to validate foo, the asynchronous' custom function is called, and the proper addValidationErrors function is called, but the component is never re-rendered when this.validation.isValid() is supposed to be false.
What am I missing?
There are actually two errors in your code. Firstly this.addValidationErrors cannot be used asynchronously inside custom validation, as it does not refer to the correct validation context. Secondly, TrackerReact only registers reactive data sources (such as .isValid) inside the render function, so it's not sufficient to only access _validationErrors in it. Thus to get it working you need to use a named validation context, and call isValid in the render function (or some other function called by it) like this:
in the validation
custom() {
setTimeout(() => {
modelSchema.namedContext().addValidationErrors([
{ name: 'foo', type: 'notUnique' }
]);
}, 100);
}
the component
export default class InventoryItemForm extends TrackerReact(Component) {
constructor(props) {
super(props);
this.validation = modelSchema.namedContext();
}
render() {
let errors = [];
if (!this.validation.isValid()) {
errors = this.validation._validationErrors;
}
return (
...
)
}
}
See more about asynchronous validation here.

Handling tab for lists in Draft.js

I have a wrapper around the Editor provided by Draft.js, and I would like to get the tab/shift-tab keys working like they should for the UL and OL. I have the following methods defined:
_onChange(editorState) {
this.setState({editorState});
if (this.props.onChange) {
this.props.onChange(
new CustomEvent('chimpeditor_update',
{
detail: stateToHTML(editorState.getCurrentContent())
})
);
}
}
_onTab(event) {
console.log('onTab');
this._onChange(RichUtils.onTab(event, this.state.editorState, 6));
}
Here I have a method, _onTab, which is connected to the Editor.onTab, where I call RichUtil.onTab(), which I assume returns the updated EditorState, which I then pass to a generic method that updates the EditorState and calls some callbacks. But, when I hit tab or shift-tab, nothing happens at all.
So this came up while implementing with React Hooks, and a google search had this answer as the #2 result.
I believe the code OP has is correct, and I was seeing "nothing happening" as well. The problem turned out to be not including the Draft.css styles.
import 'draft-js/dist/Draft.css'
import { Editor, RichUtils, getDefaultKeyBinding } from 'draft-js'
handleEditorChange = editorState => this.setState({ editorState })
handleKeyBindings = e => {
const { editorState } = this.state
if (e.keyCode === 9) {
const newEditorState = RichUtils.onTab(e, editorState, 6 /* maxDepth */)
if (newEditorState !== editorState) {
this.handleEditorChange(newEditorState)
}
return
}
return getDefaultKeyBinding(e)
}
render() {
return <Editor onTab={this.handleKeyBindings} />
}
The following example will inject \t into the current location, and update the state accordingly.
function custKeyBindingFn(event) {
if (event.keyCode === 9) {
let newContentState = Modifier.replaceText(
editorState.getCurrentContent(),
editorState.getSelection(),
'\t'
);
setEditorState(EditorState.push(editorState, newContentState, 'insert-characters'));
event.preventDefault(); // For good measure. (?)
return null;
}
return getDefaultKeyBinding(event);
}