How do I Create a non editable block with text in CKEditor 5.
I need something which in the final view generate a:
<div>Non editable message here</div>
I tried to use a UIElement and then set innerHTML but that still makes the element editable.
Just for reference: Her is the plugin I ended up using.
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
import DecoupledEditor from '#ckeditor/ckeditor5-editor-decoupled/src/decouplededitor';
import Essentials from '#ckeditor/ckeditor5-essentials/src/essentials';
import Paragraph from '#ckeditor/ckeditor5-paragraph/src/paragraph';
import Bold from '#ckeditor/ckeditor5-basic-styles/src/bold';
import Heading from '#ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '#ckeditor/ckeditor5-basic-styles/src/italic';
import ButtonView from '#ckeditor/ckeditor5-ui/src/button/buttonview';
import imageIcon from '#ckeditor/ckeditor5-core/theme/icons/image.svg';
import Command from '#ckeditor/ckeditor5-core/src/command';
import { downcastElementToElement,downcastAttributeToAttribute,downcastAttributeToElement } from '#ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
import { upcastElementToElement, upcastAttributeToAttribute,modelToViewAttributeConverter} from '#ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
import attachPlaceholder from "#ckeditor/ckeditor5-engine/src/view/placeholder";
import ViewPosition from '#ckeditor/ckeditor5-engine/src/view/position';
import toWidgetEditable from '#ckeditor/ckeditor5-widget/src/utils';
import ClickObserver from '#ckeditor/ckeditor5-engine/src/view/observer/clickobserver';
import UpcastDispatcher from '#ckeditor/ckeditor5-engine/src/conversion/upcastdispatcher';
import { toWidget } from '#ckeditor/ckeditor5-widget/src/utils';
export default class TestWidget2 extends Plugin {
static get pluginName() {
return 'TestWidget2';
}
init() {
console.log('TestWidget2::init()');
const editor=this.editor;
const model=editor.model;
model.schema.register( 'myWidget', {
inheritAllFrom: '$block',
isObject: true
} );
editor.conversion.for( 'dataDowncast' )
.add( downcastElementToElement( {
model: 'myWidget',
view: ( modelItem, writer ) => {
const elm=writer.createContainerElement( 'div', { class: 'widget' } );
return toWidget( div, writer, { label: 'widget label' } );
}
} ) );
editor.conversion.for( 'editingDowncast' )
.add( downcastElementToElement( {
model: 'myWidget',
view: ( modelItem, writer ) => {
const div = writer.createContainerElement( 'div', { class: 'widget' } );
return toWidget( div, writer, { label: 'widget label' } );
}
} ) );
editor.conversion.for( 'upcast' )
.add( upcastElementToElement( {
view: {
name: 'div',
class: 'widget'
},
model: 'myWidget'
} ) );
}
}
You can create the widget API for that:
import Widget from '#ckeditor/ckeditor5-widget/src/widget';
import { toWidget } from '#ckeditor/ckeditor5-widget/src/utils';
import { downcastElementToElement } from '#ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
import { upcastElementToElement } from '#ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ Essentials, Paragraph, Widget, /* ... other plugins ... */ ],
toolbar: [ 'undo', 'redo' ]
} )
.then( editor => {
window.editor = editor;
const model = editor.model;
model.schema.register( 'myWidget', {
inheritAllFrom: '$block',
isObject: true
} );
editor.conversion.for( 'dataDowncast' )
.add( downcastElementToElement( {
model: 'myWidget',
view: ( modelItem, writer ) => {
return writer.createContainerElement( 'div', { class: 'widget' } );
}
} ) );
editor.conversion.for( 'editingDowncast' )
.add( downcastElementToElement( {
model: 'myWidget',
view: ( modelItem, writer ) => {
const div = writer.createContainerElement( 'div', { class: 'widget' } );
return toWidget( div, writer, { label: 'widget label' } );
}
} ) );
editor.conversion.for( 'upcast' )
.add( upcastElementToElement( {
view: {
name: 'div',
class: 'widget'
},
model: 'myWidget'
} ) );
editor.setData(
'<p>foobar</p>' +
'<div class="widget">Non editable text goes here</div>' +
'<p>foobar</p>'
);
} )
.catch( err => {
console.error( err.stack );
} );
The key point is to use the toWidget() function when downcasting an element to the editing view. Also, that element should be marked as an object in the schema.
Also don't forget to load the Widget plugin which turns on the support for widgets.
I just had the problem yesterday that when having a html table inside the isObject widget content, the table was still editable and also had controls.
The only way to fix it was to disable interaction inside my widget via css.
.ck-widget.my-wdiget *:not(.ck.ck-reset_all.ck-widget__type-around) {
pointer-events: none;
}
.ck.ck-reset_all.ck-widget__type-around is the class of the widget controls to add a linebreak before or after the widget. They are still usable.
I followed your steps without success..
For better knowledge (this guideline it's tested only in custom build for angular application but i think thats work everywhere)
probably you need use this command for follow correctly this guideline
npm install -g webpack
and in add to this you need install globally another dependency but i don't remember what is! (you can easly search the error on google for find it)
Create your custom build here
unzip, open folder and run npm install
install widget(with type def. if necessary) and core dependencies
npm i --save -D #ckeditor/ckeditor5-widget
npm i --save -D #types/ckeditor__ckeditor5-widget
npm i --save -D #ckeditor/ckeditor5-core
go into path src/ckeditor.js and add the following code for create the reinmar custom plugin
import {toWidget, Widget} from "#ckeditor/ckeditor5-widget";
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
class NotEditable extends Plugin {
init() {
const editor = this.editor;
const model = editor.model;
model.schema.register('myWidget', {
inheritAllFrom: '$block',
isObject: true
});
editor.conversion.for('dataDowncast')
.elementToElement({
model: 'myWidget',
view: (modelItem, writer) => {
return writer.writer.createContainerElement('div', {class: 'widget'});
}
});
editor.conversion.for('editingDowncast')
.elementToElement({
model: 'myWidget',
view: (modelItem, writer) => {
const div = writer.writer.createContainerElement('div', {class: 'widget'});
return toWidget(div, writer.writer, {label: 'widget label'});
}
});
editor.conversion.for('upcast')
.elementToElement({
view: {name: 'div', class: 'widget'},
model: 'myWidget'
});
}
}
and in Editor.builtinPlugins add Widget and NotEditable
Editor.builtinPlugins = [
//***other plugin,
Widget,
NotEditable
];
exec in the terminal npm run build
copy the build folder into your project and import the build (follow official Ckeditor5 guideline for this) {in my case i follow this guideline in angular guide}
According to the documentation isReadOnly : Boolean
Set it to true.
Official Documentation
Related
I am trying to develop a simple plugin for CKEditor 5. My ultimate goal is to have the plugin emit the following HTML:
<div class="icms_note">
<div class="span-6 module-note">
<p>User can edit here</p>
</div>
<div class="clear"></div>
</div>
I was having issues with the user editable area being in the wrong spot so I reduced the output to just this:
<div class="icms_note">
<p>User can edit here</p>
</div>
and the data model looks like this:
<icms_note>
<paragraph>User can edit here</paragraph>
</icms_note>
So I can get the data model created correctly and the HTML ends up in the editor the way I expect but I can't edit the text in the paragraph. I click on the text and the cursor just jumps out. I've tried looking at other examples and the tutorials but I can't seem to get it to work right. My plugin code is below. Any help would be appreciated.
note.js
import Plugin from "#ckeditor/ckeditor5-core/src/plugin";
import NoteEditing from "./noteediting";
import NoteUI from "./noteui";
export default class Note extends Plugin {
static get requires() {
return [ NoteEditing, NoteUI ];
}
static get pluginName() {
return "IcmsNote";
}
}
noteui.js
import Plugin from "#ckeditor/ckeditor5-core/src/plugin";
import { ButtonView } from "#ckeditor/ckeditor5-ui";
export default class NoteUI extends Plugin {
init() {
const editor = this.editor;
editor.ui.componentFactory.add( "icms_note", (locale) => {
const button = new ButtonView(locale);
button.set({
label: "Note",
withText: true,
tooltip: true
});
button.on( "execute", () => {
editor.execute("insertNote");
});
return button;
} );
}
}
noteediting.js
import Position from "#ckeditor/ckeditor5-engine/src/view/position";
import NoteCommand from "./notecommand";
export default class NoteEditing extends Plugin {
init() {
this._defineSchema();
this._defineConverters();
this.editor.commands.add("insertNote", new NoteCommand(this.editor));
}
_defineSchema() {
const schema = this.editor.model.schema;
schema.register( "icms_note", {
inheritAllFrom: "$text",
allowIn: [ "$root", "$container" ],
isInline: true
});
}
_defineConverters() {
const conversion = this.editor.conversion;
conversion.for( "downcast" ).elementToElement({
model: "icms_note",
view: ( modelElementValue, conversionApi ) => {
const { writer } = conversionApi;
const outerDivElement = writer.createEditableElement("div", {class: "icms_note"});
return outerDivElement;
}
});//<div><div class=\"span-6 module-note\"><p>Enter note</p></div><div class=\"clear\"></div></div>
conversion.for( "upcast" ).elementToElement({
view: {
name: "div",
attributes: {
classes: [ "icms_note" ]
}
},
model: {
key: "icms_note",
value: viewElement => {
const val = viewElement.getChildren()[0].getChildren()[0].data;
return val;
}
}
});
}
}
notecommand.js
import Command from "#ckeditor/ckeditor5-core/src/command";
export default class NoteCommand extends Command {
constructor(editor) {
super(editor);
}
execute() {
console.log("NoteCommand#execute");
const model = this.editor.model;
const selection = model.document.selection;
model.change( modelWriter => {
let position = selection.getFirstPosition();
const icmsNote = modelWriter.createElement("icms_note");
const paragraph = modelWriter.createElement("paragraph");
modelWriter.insert(paragraph, icmsNote);
modelWriter.insertText("User can edit here", paragraph);
let positionElementName = position.parent.name;
while (positionElementName != "$root" && positionElementName != "$container") {
position = model.createPositionAfter(position.parent);
positionElementName = position.parent.name;
}
model.insertContent(icmsNote, position, null, {
setSelection: "after"
});
});
}
}
I have a modal component and I'm writing the story for it. It looks something like this:
import { Story, Meta } from '#storybook/html';
export default {
title: 'Components/Modal',
argTypes: {
open: {
name: 'Opened',
control: 'boolean'
},
},
args: {
open: false,
}
} as Meta;
const Template: Story = (args) => {
return `
<my-modal open="${args.open}">
Some example content inside the modal
</my-modal>
`;
};
export const Modal: Story = Template.bind({});
I have the arg open on the controls and I can change its value to true and the modal shows. But I would like the story to have a button and when it's clicked, the modal shows.
I can't find a way to do this in the current version of Storybook for web components.
I've seen there are some hooks available for React (import { useArgs } from '#storybook/api';) that allows you to change the arguments value dynamically but I can't see how to do this for web components?
Any helps will be highly appreciated.
Just add that button to the template:
import { Story, Meta } from '#storybook/html';
export default {
title: 'Components/Modal',
argTypes: {
open: {
name: 'Opened',
control: 'boolean'
},
},
args: {
open: false,
}
} as Meta;
const Template: Story = (args) => {
return `
<button
type="button"
onclick="this.nextElementSibling.open = !this.nextElementSibling.open">
Toggle Modal
</button>
<my-modal .open=${args.open}>
Some example content inside the modal
</my-modal>
`;
};
export const Modal: Story = Template.bind({});
Also, for boolean attributes - if implemented properly -
you should work with the property (prefix it in the template with a .) rather than the attribute.
Doing that with all native code isn't rocket science...
<my-dialog id="DIALOG" open>
Hello *Native* Web Components world!
</my-dialog>
<button onclick="DIALOG.open()">OPEN</button>
<script>
customElements.define("my-dialog", class extends HTMLElement {
static get observedAttributes() {
return ["open"];
}
constructor() {
super() // sets and returns 'this'
.attachShadow({mode:"open"}) // sets and return this.shadowRoot
.innerHTML = `<dialog><slot></slot><button>Close</button></dialog>`;
this.dialog = this.shadowRoot.querySelector("dialog");
}
connectedCallback() {
this.onclick = () => this.close(); // or attach to button
}
attributeChangedCallback(name,oldValue,newValue) {
this.open();
}
open() {
this.dialog.showModal(); // or .show()
}
close() {
this.dialog.close();
}
});
</script>
I am using a vue-select plugin and render function is used to create custom buttons for it. Documentation: https://vue-select.org/guide/components.html#deselect
I am using class based components with TypeScript enabled and I want to render those two elements.
Currently I use them like this:
Deselect: object = {
render: (createElement: any) => createElement('i', { attrs: { class: 'ri-close-line' } }),
}
OpenIndicator: object = {
render: (createElement: any) => createElement('i', { class: { 'ri-arrow-down-s-line': true } })
}
It is working fine but I don't want to use "any" types. What type is createElement?
The right type is CreateElement imported from vue :
import { CreateElement } from 'vue'
Deselect: object = {
render: (createElement: CreateElement ) => createElement('i', { attrs: { class: 'ri-close-line' } }),
}
OpenIndicator: object = {
render: (createElement: CreateElement ) => createElement('i', { class: { 'ri-arrow-down-s-line': true } })
}
I'm a beginner on Gatsby. I try to display a batch of images in grid from a specific folder. I find a script to do that, but that's catch all pics from my data folder and my purpose is target a specific one. I try a script but that's don't work with allImageSharp when I try to filter this one allImageSharp(filter: { sourceInstanceName: { eq: "media" } }) {.
When I try allImageSharp(filter: { id: { regex: "/media/" } }) { just display a blank window, but that's work fine like that allImageSharp {
javascript
import React from "react"
import { graphql } from "gatsby"
import Img from "gatsby-image"
import Layout from "../components/layout"
const img_grid_style = {
display: "grid",
gridTemplateColumns: `repeat(auto-fill, 200px)`,
}
export default ({ data }) => (
<div>
<Layout title="IMAGE GRID"></Layout>
<div style={img_grid_style}>
{data.allImageSharp.edges.map(edge => (
<Img fluid={edge.node.fluid} />
))}
</div>
</div>
)
export const query = graphql`
query {
allImageSharp(filter: { sourceInstanceName: { eq: "media" } }) {
edges {
node {
id
fluid(maxWidth: 200, maxHeight: 200) {
...GatsbyImageSharpFluid
}
}
}
}
}
`
config
module.exports = {
plugins: [
`gatsby-transformer-sharp`,
{
resolve: `gatsby-plugin-sharp`,
options: {
// Available options and their defaults:
base64Width: 20,
// forceBase64Format: ``, // valid formats: png,jpg,webp // don't work on OSX
useMozJpeg: process.env.GATSBY_JPEG_ENCODER === `MOZJPEG`,
stripMetadata: true,
defaultQuality: 50,
failOnError: true,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `media`,
path: `./media/`,
},
},
],
}
Change your path in your gatsby-source-filesystem to:
{
resolve: `gatsby-source-filesystem`,
options: {
name: `media`,
path: `${__dirname}/media/`,
},
},
The snippet above will match. rootOfYourProject/media, change it to your media path keeping the ${__dirname}.
Now, your filesystem can be filtered by media (without slashes).
export const query = graphql`
{
allFile(filter: {sourceInstanceName: {eq: "media"}}) {
nodes {
childImageSharp {
fluid {
base64
tracedSVG
srcWebp
srcSetWebp
originalImg
originalName
}
}
}
}
}
`
Since you can't use fragments in the GraphQL playground (localhost:8000/___graphql) I've used the extended version, however, you should use the ...GatsbyImageSharpFluid once applied to your code.
allFile or allImageSharp should do the trick.
I've fixed your project in this PR. The query was retrieving properly the results, however, you were missing some nested objects to get the final fluid image:
export default ({ data }) => {
return <div>
<Layout title="IMAGE GRID FROM SPECIFIC FOLDER"></Layout>
<div style={img_grid_style}>
{data.allFile.edges.map(({ node }) => (
<Img fluid={node.childImageSharp.fluid} />
))}
</div>
</div>
}
Background: I've built a standard single file component that takes a name prop and looks in different places my app's directory structure and provides the first matched component with that name. It was created to allow for "child theming" in my Vue.js CMS, called Resto. It's a similar principle to how WordPress looks for template files, first by checking the Child theme location, then reverting to the parent them if not found, etc.
Usage : The component can be used like this:
<!-- Find the PageHeader component
in the current child theme, parent theme,
or base components folder --->
<theme-component name="PageHeader">
<h1>Maybe I'm a slot for the page title!</h1>
</theme-component>
My goal : I want to convert to a functional component so it doesn't affect my app's render performance or show up in the Vue devtools. It looks like this:
<template>
<component
:is="dynamicComponent"
v-if="dynamicComponent"
v-bind="{ ...$attrs, ...$props }"
v-on="$listeners"
#hook:mounted="$emit('mounted')"
>
<slot />
</component>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'ThemeComponent',
props: {
name: {
type: String,
required: true,
default: '',
},
},
data() {
return {
dynamicComponent: null,
resolvedPath: '',
}
},
computed: {
...mapGetters('site', ['getThemeName']),
customThemeLoader() {
if (!this.name.length) {
return null
}
// console.log(`Trying custom theme component for ${this.customThemePath}`)
return () => import(`#themes/${this.customThemePath}`)
},
defaultThemeLoader() {
if (!this.name.length) {
return null
}
// console.log(`Trying default component for ${this.name}`)
return () => import(`#restoBaseTheme/${this.componentPath}`)
},
baseComponentLoader() {
if (!this.name.length) {
return null
}
// console.log(`Trying base component for ${this.name}`)
return () => import(`#components/Base/${this.name}`)
},
componentPath() {
return `components/${this.name}`
}, // componentPath
customThemePath() {
return `${this.getThemeName}/${this.componentPath}`
}, // customThemePath()
},
mounted() {
this.customThemeLoader()
.then(() => {
// If found in the current custom Theme dir, load from there
this.dynamicComponent = () => this.customThemeLoader()
this.resolvedPath = `#themes/${this.customThemePath}`
})
.catch(() => {
this.defaultThemeLoader()
.then(() => {
// If found in the default Theme dir, load from there
this.dynamicComponent = () => this.defaultThemeLoader()
this.resolvedPath = `#restoBaseTheme/${this.defaultThemePath}`
})
.catch(() => {
this.baseComponentLoader()
.then(() => {
// Finally, if it can't be found, try the Base folder
this.dynamicComponent = () => this.baseComponentLoader()
this.resolvedPath = `#components/Base/${this.name}`
})
.catch(() => {
// If found in the /components dir, load from there
this.dynamicComponent = () => import(`#components/${this.name}`)
this.resolvedPath = `#components/${this.name}`
})
})
})
},
}
</script>
I've tried SO many different approaches but I'm fairly new to functional components and render functions (never got into React).
The roadblock : I can't seem to figure out how to run the chained functions that I call in my original mounted() function. I've tried running it from inside the render function with no success.
Big Question
How can I find and dynamically import the component I'm targeting before I pass that component to the createElement function (or within my single file <template functional><template/>)?
Thanks all you Vue-heads! ✌️
Update: I stumbled across this solution for using the h() render function and randomly loading a component, but I'm not sure how to make it work to accept the name prop...
Late to the party, but I was in a similar situation, where I had a component in charge of conditionally render one of 11 different child components:
<template>
<v-row>
<v-col>
<custom-title v-if="type === 'title'" :data="data" />
<custom-paragraph v-else-if="type === 'paragraph'" :data="data" />
<custom-text v-else-if="type === 'text'" :data="data" />
... 8 more times
</v-col>
</v-row>
</template>
<script>
export default {
name: 'ProjectDynamicFormFieldDetail',
components: {
CustomTitle: () => import('#/modules/path/to/CustomTitle'),
CustomParagraph: () => import('#/modules/path/to/CustomParagraph'),
CustomText: () => import('#/modules/path/to/CustomText'),
... 8 more times
},
props: {
type: {
type: String,
required: true,
},
data: {
type: Object,
default: null,
}
},
}
</script>
which of course is not ideal and pretty ugly.
The functional equivalent I came up with is the following
import Vue from 'vue'
export default {
functional: true,
props: { type: { type: String, required: true }, data: { type: Object, default: null } },
render(createElement, { props: { type, data } } ) {
// prop 'type' === ['Title', 'Paragraph', 'Text', etc]
const element = `Custom${type}`
// register the custom component globally
Vue.component(element, require(`#/modules/path/to/${element}`).default)
return createElement(element, { props: { data } })
}
}
Couple of things:
lazy imports don't seem to work inside Vue.component, hence require().default is the way to go
in this case the prop 'type' needs to be formatted, either in the parent component or right here