Inserting image url in quill only displays the image icon and not the image itself - file-upload

I am using quill in Next.js and I have ran into a problem. Instead of using the default base64 encoding for images, I have added a custom handler that creates an object url for the images and I later pass it to the quill.insertEmbed() method. However, once I go to the browser, quill only displays an image icon like the one displayed by alt. I have tried to use the blotFormatter module but it still does not work. Honestly I do not know how blotFormatter works. Please help.
Here is my code
`import React, { useEffect, useState, useRef } from 'react';
import dynamic from 'next/dynamic';
import { useQuill } from 'react-quilljs';
// const BlotFormatter =
// typeof window === 'object'
// ? require('quill-blot-formatter')
// : () => false;
const BlotFormatter = dynamic(
() => {
import('quill-blot-formatter');
},
{ ssr: false, loading: () => <p>Text Editor Loading...</p> }
);
const TOOLBAR_OPTIONS = [
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ font: [] }],
[{ list: 'ordered' }, { list: 'bullet' }],
['bold', 'italic', 'underline', 'strike'],
[{ indent: '-1' }, { indent: '+1' }, { align: [] }],
[{ color: [] }, { background: [] }],
[{ script: 'sub' }, { script: 'super' }],
[{ align: [] }],
['image', 'blockquote', 'code-block'],
['clean'],
];
const QuillEditor = () => {
const { quill, quillRef, Quill } = useQuill({
modules: {
toolbar: {
container: TOOLBAR_OPTIONS,
},
},
blotFormatter: {},
clipboard: {
// toggle to add extra line breaks when pasting HTML:
matchVisual: false,
},
theme: 'snow',
placeholder: 'Describe your question',
});
const openInputImageRef = useRef();
if (Quill && !quill) {
Quill.register('modules/blotFormatter', BlotFormatter);
}
useEffect(() => {
if (quill) {
quill.getModule('toolbar').addHandler('image', imageHandler);
quill.getModule('blotFormatter');
}
}, [quill, Quill]);
const imageHandler = () => {
openInputImageRef.current.click();
};
// handle image insertions
const quillImageCallback = (e) => {
e.preventDefault();
e.stopPropagation();
if (
e.currentTarget &&
e.currentTarget.files &&
e.currentTarget.files.length > 0
) {
const file = e.currentTarget.files[0];
let formData = new FormData();
formData.append(file.name, file);
// create an objectUrl for the file
const url = URL.createObjectURL(file);
console.log(url);
// display the image on quill
quill.focus();
let range = quill.getSelection();
let position = range ? range.index : 0;
console.log('position....', position);
quill.insertEmbed(
position,
'image',
url,
Quill.sources.USER
);
quill.setSelection(position + 1);
}
};`
return (
<div>
<div ref={quillRef} />
<div>
<input
type="file"
accept="image/*"
ref={openInputImageRef}
onChange={(e) => quillImageCallback(e)}
className="hidden"
/>
</div>
</div>
);
};
export default QuillEditor;
I am currently working on a project that works like stack overflow. In the project people can ask questions which have titles, describe their questions including posting images and add tags for topical targeting. For the rich text editor, I chose Quill. Quill handles image uploads by converting images to base 64. I however, prefer to handle images as formData objects. I tried to handle image uploads independently so that I could store them separately in a file system. After searching throughout the internet, I found ways to use the imageHandler in next js. In my imageHandler, I create an objectUrl using the URL.createObjectUrl() method and then insertEmbed it to quill for display to the user. However, quill only displays an image icon. Registering the blotFormatter module does not also work. I also admit that I do not understand how blotFormatter works exactly. I have seen some code snippets where people use Class components but it does not seem to work for me. Please advise me on how to go about this problem.
Quill displays an image icon

Related

TipTap Vue custom component selects the entire line, not the selected one

I will immediately introduce the custom extension Tag.js
import { mergeAttributes, Node } from "#tiptap/core";
import { VueNodeViewRenderer } from "#tiptap/vue-3";
import { markInputRule } from "#tiptap/core";
import { markPasteRule } from "#tiptap/core";
import Component from "~/components/Editor/Tag.vue";
const starInputRegex = /(?:^|\s)((?:\*)((?:[^*]+))(?:\*))$/;
const starPasteRegex = /(?:^|\s)((?:\*)((?:[^*]+))(?:\*))/g;
const underscoreInputRegex = /(?:^|\s)((?:_)((?:[^_]+))(?:_))$/;
const underscorePasteRegex = /(?:^|\s)((?:_)((?:[^_]+))(?:_))/g;
export default Node.create({
name: "vuetag",
group: "block",
content: "inline*",
selectable: true,
parseHTML() {
return [
{
tag: "tag",
},
];
},
renderHTML({ HTMLAttributes }) {
return ["tag", mergeAttributes(HTMLAttributes), 0];
},
addNodeView() {
return VueNodeViewRenderer(Component);
},
addInputRules() {
return [
markInputRule({
find: starInputRegex,
type: this.type,
}),
markInputRule({
find: underscoreInputRegex,
type: this.type,
}),
];
},
addPasteRules() {
return [
markPasteRule({
find: starPasteRegex,
type: this.type,
}),
markPasteRule({
find: underscorePasteRegex,
type: this.type,
}),
];
},
addCommands() {
return {
setTag:
() =>
({ commands }) => {
return commands.setNode(this.name);
},
};
},
});
Component Tag.vue
<template>
<node-view-wrapper>
<el-tag><node-view-content /></el-tag>
</node-view-wrapper>
</template>
<script>
import { NodeViewContent, nodeViewProps, NodeViewWrapper } from "#tiptap/vue-3";
export default {
components: {
NodeViewWrapper,
NodeViewContent,
},
props: nodeViewProps,
};
</script>
<style lang="scss"></style>
There is a text: Did you see that? That’s a Vue component. We are really living in the future.
Let's say I want the phrase Did you see that? specify as a tag. I highlight this phrase and click on the button, the event setTag() is triggered
The result I get is this:<tag>Did you see that? That’s a Vue component. We are really living in the future.</tag>
The problem is that here the whole one line becomes a tag, that is, inside the Tag component.Vue
And there should be such a result: <tag>Did you see that?</tag> That’s a Vue component. We are really living in the future.
As an el-tag, I took from https://element-plus.org/en-US/component/tag.html

RESOLVED - DevExpress controller in Vue3 Composition API

i'm developing in Vue3 with DevExpress controllers. On DevExpress website all the Vue demos show the Options API mode but i'm using the Composition API, and initially i have some problems about the customization form on runtime because append/remove items worked just in Options API.
I've undestand my error depends on reactivity.
On demos DevExtreme components use SimpleItems generated by itemOptions that is an array of objects related the options (parameters) of SimpleItem component.
[{
name: 'trash',
location: 'after',
options: {
type: 'danger',
stylingMode: 'text',
icon: 'trash',
onClick: () => {
employee.Phone.splice(index, 1);
itemOptions = this.getPhonesOptions(this.payload.Phone);
},
},
}]
Initially in Composition API i was instanceted just the values to show on each SimpleItem
const employee = reactive({
Phone: ['123']
})
but it was a mistake! I've understood that is necessary to make reactive itemOptions too!
Below the correct examples based on demo
<script setup>
import { reactive } from 'vue'
import {
DxForm,
DxSimpleItem,
DxGroupItem,
DxButtonItem,
DxLabel,
} from 'devextreme-vue/form';
const employee = reactive({
Phone: ['123'],
phoneOptions: []
})
employee.itemOptions = reactive([])
var phoneOptions = getPhoneItems(employee.Phone)
var addPhoneButton = {
icon: 'plus',
type: 'green',
stylingMode: 'contained',
onClick: () => {
employee.Phone.push('');
phoneOptions = getPhoneItems(employee.Phone);
},
}
function getPhoneItems(items) {
const options = [];
for (let i = 0; i < items.length; i += 1) {
options.push(newPhoneOptions(i));
}
return options;
}
function newPhoneOptions(index) {
return {
buttons: [{
name: 'trash',
location: 'after',
options: {
type: 'danger',
stylingMode: 'text',
icon: 'trash',
onClick: () => {
employee.Phone.splice(index, 1);
phoneOptions = getPhoneItems(employee.Phone);
},
},
}],
};
}
</script>
<template>
<DxForm
id="form"
:form-data="employee"
>
<DxGroupItem
name="items-container"
>
<DxGroupItem
item-type="group"
name="phones"
>
<DxSimpleItem
v-for="(p, index) in phoneOptions"
:key="'Phone' + (index + 1)"
:data-field="'Phone[' + index + ']'"
:editor-options="p"
>
<DxLabel :text="'ITEM ' + (index + 1)"/>
</DxSimpleItem>
</DxGroupItem>
<DxButtonItem
:button-options="addPhoneButton"
css-class="add-phone-button"
horizontal-alignment="left"
/>
</DxGroupItem>
</DxForm>
</template>
Regards
Davide

Storybook.js (Vue) Docs Template Output

Using StoryBook.js, when I navigate to a component, view its "Docs" and click the "Show Code" button, why do I get code that looks like this...
(args, { argTypes }) => ({
components: { Button },
props: Object.keys(argTypes),
template: '<Button v-bind="$props" />',
})
...as opposed to this...
<Button type="button" class="btn btn-primary">Label</Button>
Button.vue
<template>
<button
:type="type"
:class="'btn btn-' + (outlined ? 'outline-' : '') + variant"
:disabled="disabled">Label</button>
</template>
<script>
export default {
name: "Button",
props: {
disabled: {
type: Boolean,
default: false,
},
outlined: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'button',
},
variant: {
type: String,
default: 'primary',
validator(value) {
return ['primary', 'success', 'warning', 'danger'].includes(value)
}
}
}
}
</script>
Button.stories.js
import Button from '../components/Button'
export default {
title: 'Button',
component: Button,
parameters: {
componentSubtitle: 'Click to perform an action or submit a form.',
},
argTypes: {
disabled: {
description: 'Make a button appear to be inactive and un-clickable.',
},
outlined: {
description: 'Add a border to the button and remove the fill colour.',
},
type: {
options: ['button', 'submit'],
control: { type: 'inline-radio' },
description: 'Use "submit" when you want to submit a form. Use "button" otherwise.',
},
variant: {
options: ['primary', 'success'],
control: { type: 'select' },
description: 'Bootstrap theme colours.',
},
},
}
const Template = (args, { argTypes }) => ({
components: { Button },
props: Object.keys(argTypes),
template: '<Button v-bind="$props" />',
})
export const Filled = Template.bind({})
Filled.args = { disabled: false, outlined: false, type: 'button', variant: 'primary' }
export const Outlined = Template.bind({})
Outlined.args = { disabled: false, outlined: true, type: 'button', variant: 'primary' }
export const Disabled = Template.bind({})
Disabled.args = { disabled: true, outlined: false, type: 'button', variant: 'primary' }
I thought I followed their guides to the letter, but I just can't understand why the code output doesn't look the way I expect it to.
I simply want any of my colleagues using this to be able to copy the code from the template and paste it into their work if they want to use the component without them having to be careful what they select from the code output.
For anyone else who encounters this issue, I discovered that this is a known issue for StoryBook with Vue 3.
As mine is currently a green-field project at the time of writing this, I put a temporary workaround in place by downgrading Vue to ^2.6.
This is OK for me. I'm using the options API to build my components anyway so I'll happily upgrade to Vue ^3 when Storybook resolve the above linked issue.
One of possible options is to use current workaround that I found in the GH issue mentioned by Simon K https://github.com/storybookjs/storybook/issues/13917:
Create file withSource.js in the .storybook folder with following content:
import { addons, makeDecorator } from "#storybook/addons";
import kebabCase from "lodash.kebabcase"
import { h, onMounted } from "vue";
// this value doesn't seem to be exported by addons-docs
export const SNIPPET_RENDERED = `storybook/docs/snippet-rendered`;
function templateSourceCode (
templateSource,
args,
argTypes,
replacing = 'v-bind="args"',
) {
const componentArgs = {}
for (const [k, t] of Object.entries(argTypes)) {
const val = args[k]
if (typeof val !== 'undefined' && t.table && t.table.category === 'props' && val !== t.defaultValue) {
componentArgs[k] = val
}
}
const propToSource = (key, val) => {
const type = typeof val
switch (type) {
case "boolean":
return val ? key : ""
case "string":
return `${key}="${val}"`
default:
return `:${key}="${val}"`
}
}
return templateSource.replace(
replacing,
Object.keys(componentArgs)
.map((key) => " " + propToSource(kebabCase(key), args[key]))
.join(""),
)
}
export const withSource = makeDecorator({
name: "withSource",
wrapper: (storyFn, context) => {
const story = storyFn(context);
// this returns a new component that computes the source code when mounted
// and emits an events that is handled by addons-docs
// this approach is based on the vue (2) implementation
// see https://github.com/storybookjs/storybook/blob/next/addons/docs/src/frameworks/vue/sourceDecorator.ts
return {
components: {
Story: story,
},
setup() {
onMounted(() => {
try {
// get the story source
const src = context.originalStoryFn().template;
// generate the source code based on the current args
const code = templateSourceCode(
src,
context.args,
context.argTypes
);
const channel = addons.getChannel();
const emitFormattedTemplate = async () => {
const prettier = await import("prettier/standalone");
const prettierHtml = await import("prettier/parser-html");
// emits an event when the transformation is completed
channel.emit(
SNIPPET_RENDERED,
(context || {}).id,
prettier.format(`<template>${code}</template>`, {
parser: "vue",
plugins: [prettierHtml],
htmlWhitespaceSensitivity: "ignore",
})
);
};
setTimeout(emitFormattedTemplate, 0);
} catch (e) {
console.warn("Failed to render code", e);
}
});
return () => h(story);
},
};
},
});
And then add this decorator to preview.js:
import { withSource } from './withSource'
...
export const decorators = [
withSource
]

Vue storybook globalTypes not re-rendering preview

Hello all i am currently in the process to integrate Storybook-Vue into my own pattern-lib.
So far everything worked like a charm. Expect for one thing and that is adding a globalType in the preview.js and then using it inside a decorators. Registering the new Global type works, i see it in the toolbar but when i change the selected value it does not re-render the preview.
My first guess is that the context is not an observable object so Vue never knows when this object actually gets an update. But i am not sure how i could change this.
// preview.js
import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n);
export const globalTypes = {
locale: {
name: 'Locale',
description: 'Internationalization locale',
defaultValue: 'en-US',
toolbar: {
icon: 'globe',
items: [
{ value: 'de-DE', right: '🇩🇪', title: 'German' },
{ value: 'en-US', right: '🇺🇸 ', title: 'English' },
{ value: 'cs-CZ', right: '🇨🇿', title: 'Czech' },
{ value: 'zh-CN', right: '🇨🇳', title: 'Chinese' },
],
},
},
};
const localeSelect = (story, context) => {
const wrapped = story(context)
const locale = context.globals.locale
return Vue.extend({
components: { wrapped, BiToast },
data () {
return {
locale
}
},
watch: {
locale: {
deep: true,
handler (val) {
this.$i18n.locale = val
}
}
},
template: `
<div>
{{ locale }}
<wrapped/>
</div>
`
})
}
export const decorators = [localeSelect]
Finally got it working. I just had to use useGlobals and set i18n outsite of the returned object.
import { useGlobals } from '#storybook/client-api';
const localeSelect = (story, context) => {
const [{locale}] = useGlobals();
i18n.locale = locale
return Vue.extend({
...

Svg icon not showing in toast-ui vue image editor

I am using vue-cliand toast-ui-vue-image-editor.
// vue.config.js
const path = require('path')
let HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
module.exports = {
chainWebpack: config => {
config.module
.rule('svg')
.use('file-loader')
.options({
name: '[name].[ext]',
outputPath: ''
})
}
And added these line in Vue compoenent
import 'tui-image-editor/dist/svg/icon-a.svg'
import 'tui-image-editor/dist/svg/icon-b.svg'
import 'tui-image-editor/dist/svg/icon-c.svg'
import 'tui-image-editor/dist/svg/icon-d.svg'
import { ImageEditor } from '#toast-ui/vue-image-editor'
Everything is working but editor's tool Svg icon is not showing. See most bottom section of editor where white square showing instead of icons (undo ,redo, crop etc.)
I give my answer to this hoping this will help someone in the future.
I too faced this issue and solved issue with answer from this link
Here is my script section!
import 'tui-image-editor/dist/tui-image-editor.css'
import ImageEditor from '#toast-ui/vue-image-editor/src/ImageEditor.vue'
export default {
name: 'ToastUI',
components: {
'image-editor': ImageEditor
},
data () {
const icona = require('tui-image-editor/dist/svg/icon-a.svg')
const iconb = require('tui-image-editor/dist/svg/icon-b.svg')
const iconc = require('tui-image-editor/dist/svg/icon-c.svg')
const icond = require('tui-image-editor/dist/svg/icon-d.svg')
var whiteTheme = {
'menu.normalIcon.path': icond,
'menu.activeIcon.path': iconb,
'menu.disabledIcon.path': icona,
'menu.hoverIcon.path': iconc,
'submenu.normalIcon.path': icond,
'submenu.activeIcon.path': iconb,
'common.bi.image': 'https://uicdn.toast.com/toastui/img/tui-image-editor-bi.png',
'common.bisize.width': '251px',
'common.bisize.height': '21px',
'common.backgroundImage': './img/bg.png',
'common.backgroundColor': '#fff',
'common.border': '1px solid #c1c1c1',
}
return {
useDefaultUI: true,
options: {
includeUI: {
loadImage: {
path: '',
name: ''
},
theme: whiteTheme,
initMenu: '',
menuBarPosition: 'bottom',
menu: ['crop', 'flip', 'rotate', 'draw', 'shape', 'icon', 'text', 'mask'], //, 'filter',
},
cssMaxWidth: document.documentElement.clientWidth,
cssMaxHeight: document.documentElement.clientHeight,
selectionStyle: {
cornerSize: 20,
rotatingPointOffset: 70
}
}
}
}
}