CodeMirror 6, how to get editor value on input/update/change event? - input

I want the contents / value of a CodeMirror 6 editor instance to be reflected in an external variable, when the text changes the variable changes / syncs.
How to do that in CodeMirror 6?
Pseudo Code
let sync_val = "";
import {EditorView} from "#codemirror/view"
import {EditorState} from "#codemirror/state"
let myView = new EditorView({
state: EditorState.create({doc: "hello"}),
parent: document.body
})
myView.onChange(function(val) {
sync_val = val;
});

The following works (use the updateListener extension):
let sync_val = "";
import {EditorView} from "#codemirror/view"
import {EditorState} from "#codemirror/state"
let myView = new EditorView({
state: EditorState.create({
doc: "hello",
extensions: [
EditorView.updateListener.of(function(e) {
sync_val = e.state.doc.toString();
})
]
}),
parent: document.body
})

Related

Vue3 Composition API with Typescript Default Values for nested objects not working as expected

I want to be able to set default values for nested datasets in Vue, basically, if a key does not have a value in the dataset that is being parsed to the component, I would like that value to be filled in automatically. This kinda works looking into this example here. However, my sample which is nested with several keys in the main object seems not to work. Why is that? what am I doing wrong?
Test Component
<script setup lang="ts">
import {defineProps, PropType} from 'vue';
export type KitchenType = {
windows: number;
};
export type DefaultTestType = {
kitchen?: KitchenType;
rooms?: number;
};
const props = defineProps({
dataset: {
type: Object as PropType<DefaultTestType>,
default: () => ({
kitchen: {
windows: 5,
},
rooms: 3,
})
},
});
console.log('IAM DATASET: ', props.dataset);
</script>
Calling the Component
<DefaultTest :dataset="{
rooms: 5,
}" />
Console.log result
{ rooms: 5 }
Console.log expected result
{ kitchen: { windows: 5, }, rooms: 5 }
I'm using Laravel 9 with Vite, Inertia and Vue3 and have enabled the reactivityTransform: true in the vite.config file as told here through here
If I don't pass any dataset object, I am getting all the default values
I do not think it is possible to do such a thing with the use of default. Although you can create a default object then with the use of the Object.assign() method create a new object with default values, which will be overwritten by the prop you will receive.
const deafult = { kitchen: { windows: 5, }, rooms: 5 };
const recive = {rooms: 3}; //a prop
const mergedValues = Object.assign(deafult, recive);
console.log(mergedValues)
Have you ever try withDefaults macro for define props with default value

Manipulate innerText of a CKEditor ViewElement

I am creating a little custom plugin for the CKEditor5 for the #neoscms.
Neos is using the #ckeditor5 but with a custom view.
The plugin is more or less a placeholder plugin. The user can configure a data-source with a key value store for items (identifier and labels). The dropdown in the CKEditor is filled with the items and when the user selects an item from the dropdown, it creates a placeholder element that should end in a span element with some data-attributes.
The main idea was to have an empty element and just data-attributes to identify the element and being able to assign live data. But it turns out that the live data thing is tricky. When I manipulate the span with an extra JS snippet on the Website, the CKEditor cannot handle this.
Is it possible to manipulate a view element in the DOM and still have a working Editor?
The Plugin works fine if I just add inner Text in the downCasting and don't replace something. But the live data would be nice.
Neos Backend with a element
Maybe that code gives an idea of the package.
It is not ready yet as this is more or less the main feature ;)
import {Plugin, toWidget, viewToModelPositionOutsideModelElement, Widget,} from "ckeditor5-exports";
import PlaceholderCommand from "./placeHolderCommand";
export default class PlaceholderEditing extends Plugin {
static get requires() {
return [Widget];
}
init() {
this._defineSchema();
this._defineConverters();
this.editor.commands.add(
"placeholder",
new PlaceholderCommand(this.editor)
);
this.editor.editing.mapper.on(
"viewToModelPosition",
viewToModelPositionOutsideModelElement(this.editor.model, (viewElement) =>
viewElement.hasClass("internezzo-placeholder")
)
);
this.editor.config.define("placeholderProps", {
types: ["name", "node", "nodePath"],
});
this.editor.config.define("placeholderBrackets", {
open: "[",
close: "]",
});
}
_defineSchema() {
const schema = this.editor.model.schema;
schema.register("placeholder", {
allowWhere: "$text",
isInline: true,
isObject: true,
allowAttributes: [
"name",
"node",
"nodePath",
"data-placeholder-identifier",
"data-node-identifier",
"data-node-path",
],
});
}
_defineConverters() {
const conversion = this.editor.conversion;
const config = this.editor.config;
conversion.for("upcast").elementToElement({
view: {
name: "span",
classes: ["foobar-placeholder"],
},
model: (viewElement, writer) => {
const name = viewElement.getAttribute('data-placeholder-identifier');
const node = viewElement.getAttribute('data-node-identifier');
const nodePath = viewElement.getAttribute('data-node-path');
const modelWriter = writer.writer || writer;
return modelWriter.createElement("placeholder", {name, node, nodePath, editable: false});
},
});
conversion.for("editingDowncast").elementToElement({
model: "placeholder",
view: (modelItem, writer) => {
const viewWriter = writer.writer || writer;
const widgetElement = createPlaceholderView(modelItem, viewWriter);
return toWidget(widgetElement, viewWriter);
},
});
conversion.for("dataDowncast").elementToElement({
model: "placeholder",
view: (modelItem, writer) => {
const viewWriter = writer.writer || writer;
return createPlaceholderView(modelItem, viewWriter);
},
});
// Helper method for downcast converters.
function createPlaceholderView(modelItem, viewWriter) {
const name = modelItem.getAttribute("name");
const node = modelItem.getAttribute("node");
const nodePath = modelItem.getAttribute("nodePath");
const placeholderView = viewWriter.createContainerElement("span", {
class: "foobar-placeholder",
"data-placeholder-identifier": name,
"data-node-identifier": node,
"data-node-path": nodePath,
});
// Would be nice to remove that and have just empty spans that get dynamic data
let innerText = config.get("placeholderBrackets.open") + name;
innerText += config.get("placeholderBrackets.close");
viewWriter.insert(
viewWriter.createPositionAt(placeholderView, 0),
viewWriter.createText(innerText)
);
return placeholderView;
}
}
}
So, the extra JS snippet that is used by the website is searching for spans with the class foobar-placeholder and writes a value with live data into the span. That works in the frontend, of course, but the backend of the CMS that uses CKEditor has issues with the changing data.
I could not find a solution with docs of CKEditor, and maybe I misuse the API somehow, but I now found a working solution for me.
My website snippet is now communicating with the Plugin via Broadcast messages. And then I search for placeholder elements and check if I need to change an attribute.
const broadcastChannel = new BroadcastChannel('placeholder:changeData');
broadcastChannel.postMessage({identifier: name, value});
And in the plugin
// Receive new values for placeholder via broadcast
const broadcastChannel = new BroadcastChannel('placeholder:changeData');
broadcastChannel.onmessage = (message) => {
const identifier = get('data.identifier', message);
const newValue = get('data.value', message);
this.editor.model.change( writer => {
if (identifier) {
this._replaceAttribute(writer, identifier, newValue);
}
});
};
Only downside now is that I need to reload the page, but already read that this is maybe cause by my element down casting and I change attributes.

simply replace a node's content in prosemirror

I'm in a function that receives a string as input:
(text) => {
}
I have access to the editor via Vue props (props.editor). I would like to replace the current node's content with this text. I cannot seem to find out how to do this. I'm using tiptap2, which is a wrapper around ProseMirror and has access to all of ProseMirror's api.
I'd rather not try to replace the whole node unless necessary, which I also tried, doing below – but cannot get that to work either:
(text) => {
props.editor
.chain()
.focus()
.command(({ tr }) => {
const node = props.editor.state.schema.nodes.paragraph.create(
{ content: text}
);
tr.replaceSelectionWith(node);
return true;
})
.run();
}
Much thanks
This solution works for me in Tiptap version 2.
A precondition for this to work is, that the text to be replaced is marked (highlighted).
const selection = editor.view.state.selection;
editor.chain().focus().insertContentAt({
from: selection.from,
to: selection.to
}, "replacement text").run();
I'm late to the party but this is the top result I came across when trying to find a solution for myself.
My code is in the context of a React NodeView, so I'm given a getPos() prop that gives the position of the React node in the Prosemirror document (I believe this number more-or-less means how many characters precede the React NodeView node). With that I was able to use this command chain to replace the content:
import { Node as ProsemirrorNode } from "prosemirror-model";
import { JSONContent, NodeViewProps } from "#tiptap/react";
const NodeViewComponent = (props: NodeViewProps) =>
// ...
/**
* Replace the current node with one containing newContent.
*/
const setContent = (newContent: JSONContent[]) => {
const thisPos = props.getPos();
props.editor
.chain()
.setNodeSelection(thisPos)
.command(({ tr }) => {
const newNode = ProsemirrorNode.fromJSON(props.editor.schema, {
type: props.node.type.name,
attrs: { ...props.attrs },
content: newContent,
});
tr.replaceSelectionWith(newNode);
return true;
})
.run();
};
// ...
};
Basically you want to:
Set the current selection to the node you want to replace the content of
Create and update a new node that is a copy of the current node
Replace your selection with the new node.

Handle paste event in vue2-editor

I'm using this text editor https://github.com/davidroyer/vue2-editor that is based on Quilljs
I want to handle the paste event so it pastes only the plain text without any format but seems in the documentation that paste is not a supported event by default.
Is there any way to add the paste event?
I've already tried using v-on:paste in the Editor and adding the Quill custom module Clipboard but haven't had any success.
As I didn't find a way of doing it with the library I did it with the DOM
onPaste() {
const x = document.getElementById("removePasteFormat");
console.log(x);
x.addEventListener("paste", (e) => {
e.stopPropagation();
e.preventDefault();
let text = e.clipboardData.getData("text/plain");
// access the clipboard using the api
if (document.queryCommandSupported("insertText")) {
document.execCommand("insertText", false, text);
} else {
document.execCommand("paste", false, text);
}
});
},
Added the id to the div containing the text editors like this:
<div id="removePasteFormat"> *<<Here goes the text editor component>>* </div>
And register the method on mounted()
mounted() {
this.onPaste();
},
I think it would be good to make a plugin.
I made it simple.
src/utils/vue2Plugin/clipboard.ts
import Delta from 'quill/node_modules/quill-delta';
import Clipboard from 'quill/modules/clipboard';
import { Quill } from 'vue2-editor';
export class CustomClipboardPlugin extends Clipboard {
public quill!: Quill;
public options: any = {};
constructor(quill: Quill) {
super(quill, {
matchers: [],
});
this.quill = quill;
this.quill.root.addEventListener('paste', this.onPaste.bind(this), true);
}
onPaste(event: ClipboardEvent) {
event.preventDefault();
event.stopPropagation();
const range = this.quill.getSelection(true);
if (range === null) return;
let _textHtml;
if (event.clipboardData?.getData('text/html')) {
_textHtml = event.clipboardData?.getData('text/html');
} else if (event.clipboardData?.getData('text/plain')) {
_textHtml = `<p>${event.clipboardData?.getData('text/plain')}</p>`;
}
if (_textHtml) {
const pastedDelta = this.quill.clipboard.convert(_textHtml);
const delta = new Delta()
.retain(range.index)
.delete(range.length)
.concat(pastedDelta);
this.quill.updateContents(delta, Quill.sources.USER);
this.quill.setSelection(delta.length() - range.length, 0, Quill.sources.SILENT);
}
}
}
vue file
<template>
...
<VueEditor
...
:custom-modules="customModulesForEditor"
...
/>
...
</template>
// script
import CustomClipboardPlugin fro 'src/utils/vue2Plugin/clipboard.ts';
...
data() {
return {
customModulesForEditor: [{ alias: 'clipboard', module: CustomClipboardPlugin }],
};
},
...
I was wondering the same and came up with the following solution.
1 - Use the :editorOptions option referenced here
<template>
VueEditor(:editorOptions="editorSettings")
</template>
2 - Fill with the module.clipboard module with the option described here
3 - You can then handle the paste with your personal function (applied after quill's matcher). I've written mine following this answer on github
<script>
data() {
return {
editorSettings: {
modules: {
clipboard: {
matchers: [[Node.ELEMENT_NODE, this.customQuillClipboardMatcher]]
}
}
}
}
},
methods: {
customQuillClipboardMatcher(node, delta) {
delta.ops = delta.ops.map((op) => {
return {
insert: op.insert
}
})
return delta
},
}
</script>

Mobx / React update issue

I'm just learning Mobx and I'm trying to get this component to update with no luck. It seems like it should update, it just doesn't. What am I missing?
'use strict';
const observer = mobxReact.observer;
const makeAutoObservable = mobx.makeAutoObservable;
const isObservable = mobx.isObservable;
const isObservableProp = mobx.isObservableProp;
const configure = mobx.configure;
configure({
// This gives you good warnings to read in the console
// enforceActions: "always",
computedRequiresReaction: true,
reactionRequiresObservable: true,
observableRequiresReaction: true,
disableErrorBoundaries: true
});
const HeadingBug = observer(function ({heading}) {
return React.createElement("span", null, "This is the heading bug. Current value: ", heading);
});
let heading = makeAutoObservable({
value: 100
});
ReactDOM.render(React.createElement(HeadingBug, {
heading: heading.value
}), document.getElementById('root'));
setTimeout(() => {
console.log(`isObservable: ${isObservable(heading)}`);
console.log(`isObservableProp: ${isObservableProp(heading, 'value')}`);
console.log('Changing Heading to 200');
heading.value = 200;
}, 2000);
Codepen: https://codepen.io/mtwomey/pen/qBNLwXz
You are dereferencing heading.value too early here:
ReactDOM.render(React.createElement(HeadingBug, {
heading: heading.value
}), document.getElementById('root'));
You basically passing primitive number value inside your observer component, not observable value.
To fix it just pass whole heading object as a prop and use value inside heading component, like so:
const HeadingBug = observer(function ({heading}) {
return React.createElement("span", null, "This is the heading bug. Current value: ", heading.value);
});
More info here: https://mobx.js.org/understanding-reactivity.html