Tiptap with Bubble Menu Extension - javascript

I am using Livewire/Alpinejs stack and have also installed tiptap editor. So far following this link the editor works along with it's basic functionality/buttons. What I want is to add the Bubble Menu extension in the game. I have installed the package and also following the docs in this link but it's not working (after selecting a word bubble menu wont appear). Here is the code below.
import { Editor } from '#tiptap/core'
import StarterKit from '#tiptap/starter-kit'
import BubbleMenu from '#tiptap/extension-bubble-menu'
window.setupEditor = function (content) {
return {
editor: null,
content: content,
init(element) {
this.editor = new Editor({
element: element,
extensions: [
StarterKit,
BubbleMenu.configure({
element: document.querySelector('#menu'),
}),
],
content: this.content,
onUpdate: ({ editor }) => {
this.content = editor.getHTML()
}
})
this.$watch('content', (content) => {
// If the new content matches TipTap's then we just skip.
if (content === this.editor.getHTML()) return
/*
Otherwise, it means that a force external to TipTap
is modifying the data on this Alpine component,
which could be Livewire itself.
In this case, we just need to update TipTap's
content and we're good to do.
For more information on the `setContent()` method, see:
https://www.tiptap.dev/api/commands/set-content
*/
this.editor.commands.setContent(content, false)
})
}
}
}
<div
x-data="setupEditor($wire.entangle('{{ $attributes->wire('model')->value() }}').defer)"
x-init="() => init($refs.editor)"
wire:ignore
{{ $attributes->whereDoesntStartWith('wire:model') }}
>
<template>
<div id="menu">
<div x-if="editor">
<button #click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
bold
</button>
</div>
</div>
</template>
<div x-ref="editor"></div>
</div>

I needed to change and put the id to the parent element (template).
<template x-if="editor" id="menu">
<div>
<div>
<button #click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
bold
</button>
</div>
</div>
</template>

Related

Creating and showing complex JSON object dynamically using VueJS component and HTML

I am developing a Vuejs application within which I have a field extension. For this field, users can provide the values and this field expands dynamically (both vertically and horizontally) based on the user-provided values.
I am using the recursive component ExtensionComponent to display and store the values. But it's not working as expected for me.
The field name extensions displays, initially an Add Extension. With on click of the button, a bootstrap modal will be displayed which has 3 fields: namespace (text), localname (text), datatype(dropdown: string/complex). If the datatype is string then a simple text field will be displayed. However, if the datatype is complex then another button should be displayed and on click of the button again the same bootstrap modal is displayed with fields and the process continues. So the created JSON based on this will expand horizontally and vertically.
I am able to create the Vue component to show the recursive data but I am a bit confused about the addition of the values to extensionList array dynamically. Can someone please help me with this issue?
Following is my Test.vue page which will display the Extensions and Add Button:
<template>
<div>
<span>Extensions</span>
<button class="btn btn-primary" #click="createExtensions">
Add Another
</button>
<ExtensionComponent :extension-list="$store.state.extensionList" />
<TestModal v-if="$store.state.showModal" />
</div>
</template>
<script>
import TestModal from '#/components/TestModal.vue'
export default {
computed: {
TestModal
},
methods: {
// Method to create extensions and add
createExtensions () {
this.$store.commit('toggleExtensionModal')
}
}
}
</script>
<style>
</style>
Following is my Bootstrap modal (TestModal.vue) which will be displayed whenever Add Another button is clicked (initially and for complex):
<template>
<b-modal
id="Extension"
title="Add Another Element"
size="lg"
width="100%"
:visible="$store.state.showModal"
>
<b-form id="AddExtension" #submit.prevent="submitExtension">
<div class="form-group">
<label for="message-text" class="col-form-label">Namespace URI:</label>
<input
v-model="extension.namespace"
type="text"
class="form-control"
required
>
</div>
<div class="form-group">
<label for="message-text" class="col-form-label">Local Name:</label>
<input
v-model="extension.localName"
type="text"
class="form-control"
required
>
</div>
<div class="form-group">
<label
for="AddExtensionDataType"
class="col-form-label"
>Data Type:</label>
<b-form-select v-model="extension.dataType" class="form-control">
<b-form-select-option value="string">
String
</b-form-select-option>
<b-form-select-option value="complex">
Complex
</b-form-select-option>
</b-form-select>
</div>
</b-form>
<template #modal-footer="{ cancel }">
<b-btn #click="cancel">
Cancel
</b-btn>
<b-btn variant="primary" type="submit" form="AddExtension">
OK
</b-btn>
</template>
</b-modal>
</template>
<script>
export default {
data () {
return {
extension: {
namespace: '',
localName: '',
dataType: 'string'
},
showModal: false
}
},
methods: {
submitExtension () {
// Call vuex store to save information
this.$store.commit('addExtension', this.extension)
// Hide modal after submitting modal
this.$store.commit('toggleExtensionModal')
}
}
}
</script>
<style>
</style>
Following is my ExtensionComponent.vue which will display data recursively:
<template>
<div>
<div
v-for="extension in extensionList"
:key="extension.ID"
class="form-inline"
>
<span>{{ extension.namespace + ":" + extension.localName }}</span>
<input
v-if="extension.dataType == 'string'"
type="text"
#input="$emit('AddExtensionText', {$event, id: extension.ID})"
>
<ExtensionComponent v-if="extension.dataType == 'complex'" :extension-list="extension" #AddExtensionText="AddExtensionText($event)" />
<button
v-if="extension.dataType == 'complex'"
#click="AddComplextExtension(extension.ID, extension)"
>
Add another
</button>
</div>
</div>
</template>
<script>
import ExtensionComponent from '#/components/ExtensionComponent.vue'
export default {
components: {
ExtensionComponent
},
props: {
extensionList: Array,
extension: Object
},
methods: {
AddComplextExtension (extensionID, extension) {
this.$store.commit('modules/ExtensionDataStore/showExtensionModal')
},
AddExtensionText ({ value, id }) {
const i = this.extensionList.findIndex(el => el.ID === id)
this.$set(this.extensionList, i, value)
}
}
}
</script>
Following is my index.js Vuex store:
export const state = () => ({
extensionID: 0,
extensionList: [],
showModal: false
})
export const mutations = {
toggleExtensionModal (state) {
// Function to show/hide the extension Modal
state.showModal = !state.showModal
},
addExtension (state, extension) {
console.log(extension)
extension.ID = state.extensionID
state.extensionList.push(extension)
state.extensionID++
}
}
export const actions = {}
Following is the front-end I have:
I want to create JSON and show it something like this:
As we can see if the value is complex then the field is created dynamically and displayed in a parent-child relationship way. I am a bit confused about how to establish the parent-child relationship within complex JSON and show them to the user. Someone,Ad

Reusable Alpine.js components?

How do I use Alpine.js to create a reusable component and display it? E.g., maybe I want to define an generic Alpine.js button component that changes text and color from parameters, then have my Alpine.js navbar component use the button component to show a login button.
Can I do this in pure client-side code, without relying on a server templating out all of the button HTML everywhere the button component is used?
Can I do this in pure client-side code, without relying on a server templating?
Yes, you can.
Alpine.js always will try to persuade you to use a server side templating engine.
But just like you, I don't let myself be persuaded:
<template x-component="dropdown">
<div x-data="{ ...dropdown(), ...$el.parentElement.data() }">
<button x-on:click="open">Open</button>
<div x-show="isOpen()" x-on:click.away="close" x-text="content"></div>
</div>
</template>
<x-dropdown content="Content for my first dropdown"></x-dropdown>
<div> Random stuff... </div>
<x-dropdown content="Content for my second dropdown"></x-dropdown>
<x-dropdown></x-dropdown>
<script>
function dropdown() {
return {
show: false,
open() { this.show = true },
close() { this.show = false },
isOpen() { return this.show === true },
content: 'Default content'
}
}
// The pure client-side code
document.querySelectorAll('[x-component]').forEach(component => {
const componentName = `x-${component.getAttribute('x-component')}`
class Component extends HTMLElement {
connectedCallback() {
this.append(component.content.cloneNode(true))
}
data() {
const attributes = this.getAttributeNames()
const data = {}
attributes.forEach(attribute => {
data[attribute] = this.getAttribute(attribute)
})
return data
}
}
customElements.define(componentName, Component)
})
</script>
Alpine.js contributer #ryangjchandler remarks that reusable templates are out of scope for Alpine.js:
The proposed [Alpine.js version 3] x-component directive will NOT have anything to do with templating or the markup for your component. Instead it will provide a way of writing more immediately reusable data sets & functions, whilst reducing the amount of directives you need to define in your markup.
If you need re-usable templates, I would consider using a server-side template engine or a more monolithic front end framework such as Vue or React. (link)
and
The functionality you are looking for is far out of the scope of Alpine. It's designed to work alongside your existing markup from the server or static files, not replace / component-ise your markup. (link)
With Alpine.js v3 and Global Alpine Components, you can use Alpine.component() to encapsulate this functionality.
<div x-data="dropdown">
...
</div>
<script>
Alpine.component('dropdown', () => ({
open: false,
toggle() { this.open = !this.open }
}))
</script>
use alpinejs-component
one same page by cdn:
<div
x-data="{
people: [
{ name: 'John', age: '25', skills: ['JavaScript', 'CSS'] },
{ name: 'Jane', age: '30', skills: ['Laravel', 'MySQL', 'jQuery'] }
]
}"
>
<ul>
<template x-for="person in people">
<!-- use the person template to find the <template id="person"> element. -->
<x-component-wrapper x-component template="person" x-data="{ item: person }"></x-component-wrapper>
</template>
</ul>
</div>
<template id="person">
<li class="user-card">
<h2 x-text="item.name"></h2>
<p x-text="item.age"></p>
<ul>
<template x-for="skill in item.skills">
<li x-text="skill"></li>
</template>
</ul>
</li>
</template>
<script src="https://unpkg.com/alpinejs-component#1.x.x/dist/component.min.js"></script>
<script defer src="https://unpkg.com/alpinejs#3.x.x/dist/cdn.min.js"></script>
use url import html template:
<div
x-data="{
people: [
{ name: 'John', age: '25', skills: ['JavaScript', 'CSS'] },
{ name: 'Jane', age: '30', skills: ['Laravel', 'MySQL', 'jQuery'] }
]
}"
>
<ul>
<template x-for="person in people">
<x-component-wrapper x-component url="/public/person.html" x-data="{ item: person }"></x-component-wrapper>
</template>
</ul>
</div>
<script src="https://unpkg.com/alpinejs-component#1.x.x/dist/component.min.js"></script>
<script defer src="https://unpkg.com/alpinejs#3.x.x/dist/cdn.min.js"></script>
person.html:
<li class="user-card">
<h2 x-text="item.name"></h2>
<p x-text="item.age"></p>
<ul>
<template x-for="skill in item.skills">
<li x-text="skill"></li>
</template>
</ul>
</li>
install by npm :
npm i -D alpinejs-component
yarn add -D alpinejs-component
register plugin:
import Alpine from "alpinejs";
import component from "alpinejs-component";
Alpine.plugin(component);
window.Alpine = Alpine;
Alpine.start();
or use module in browser:
<x-component-wrapper x-component template="dropdown" x-data="dropdown"></x-component-wrapper>
<x-component-wrapper x-component template="dropdown" x-data="dropdown"></x-component-wrapper>
<template id="dropdown">
<div #click="close" class="dropdown-toggle">
<button x-on:click="open">Open</button>
<div x-show="show" x-text="content"></div>
</div>
</template>
<script type="module">
import { default as Alpine } from 'https://cdn.skypack.dev/alpinejs'
import alpinejsComponent from 'https://cdn.skypack.dev/alpinejs-component'
function dropdown() {
return {
show: false,
open() {
console.log('open')
this.show = true
console.log(this.show)
},
close(event) {
const button = this.$el.querySelector('button')
const target = event.target
if (this.$el.contains(target) && !button.contains(target)) {
this.show = false
}
},
get isOpen() {
return this.show === true
},
content: 'Default content',
init() {
console.log(this.$el.parentElement)
console.log('dropdown --- init')
},
}
}
Alpine.data('dropdown', dropdown)
Alpine.plugin(alpinejsComponent)
Alpine.start()
</script>
work well.
more info alpinejs-component
You can do this with Alpine.data and the documented approach for encapsulating directives with x-bind. The trick is to bind the x-html directive. In your HTML do this:
<div x-data="dropdown" x-bind="bind"></div>
In your Javascript:
document.addEventListener('alpine:init', () => {
Alpine.data('dropdown', () => ({
show: false,
bind: {
['x-html']() { return `
<button #click="show = !show">Click me!</button>
<div x-show="show">Hello World</div>
`},
},
}));
})
JSFiddle here.
It's a little hacky as you are encapsulating all your nested content in a multiline HTML string bound in the x-html directive (though perhaps no more hacky than the alternative of cloning templates everywhere). Make sure you don't use the backtick character in the content. Nevertheless, the content can be nested as deeply as you like and can contain Alpine.js directives. You can initialise your component by declaring and passing parameters into Alpine.data. You could also bind x-modelable to expose any properties of your component as outputs.
If you prefer to use templates, maybe because your editor does better syntax highlighting when markup is not embedded in a string, you can combine this approach with templates. Here's an example that demonstrates x-modelable and the use of templates too. In effect, Alpine does your template cloning for you.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script defer src="https://unpkg.com/alpinejs#3.x.x/dist/cdn.js"></script>
</head>
<body>
<div x-data="{clicked: false}">
<div>Clicked is <span x-text="clicked"></span></div>
<div x-data="dropdown" x-bind="bind" x-model="clicked"></div>
</div>
<template id="dropdown">
<button #click="show = !show">Click me!</button>
<div x-show="show">Hello World</div>
</template>
</body>
<script type="text/javascript">
document.addEventListener('alpine:init', () => {
Alpine.data('dropdown', () => ({
show: false,
bind: {
['x-modelable']: 'show',
['x-html']() { return document.querySelector('#dropdown').innerHTML},
},
}));
})
</script>
</html>
JSFiddle here.
The x-component with native custom element in Vimesh UI (https://github.com/vimeshjs/vimesh-ui) is a more complete reusable component implementation :
<head>
<script src="https://unpkg.com/#vimesh/style" defer></script>
<script src="https://unpkg.com/#vimesh/ui"></script>
<script src="https://unpkg.com/alpinejs" defer></script>
</head>
<body x-cloak class="p-2" x-data="{name: 'Counter to rename', winner: 'Jacky'}">
Rename the 2nd counter : <input type="text" x-model="name" class="rounded-md border-2 border-blue-500">
<vui-counter x-data="{step: 1}" :primary="true" title="First" x-init="console.log('This is the first one')" owner-name="Tom"></vui-counter>
<vui-counter x-data="{step: 5}" :title="name + ' # ' + $prop('owner-name')" owner-name="Frank"></vui-counter>
<vui-counter x-data="{step: 10, value: 1000}" :owner-name="winner">
<vui-counter-trigger></vui-counter-trigger>
</vui-counter>
<template x-component.unwrap="counter" :class="$prop('primary') ? 'text-red-500' : 'text-blue-500'"
x-data="{ step : 1, value: 0}" x-init="$api.init && $api.init()" title="Counter" owner-name="nobody">
<div>
<span x-text="$prop('title')"></span><br>
Owner: <span x-text="$prop('owner-name')"></span><br>
Step: <span x-text="step"></span><br>
Value : <span x-text="value"></span><br>
<button #click="$api.increase()"
class="inline-block rounded-lg bg-indigo-600 px-4 py-1.5 text-white shadow ring-1 ring-indigo-600 hover:bg-indigo-700 hover:ring-indigo-700">
Increase
</button>
<slot></slot>
</div>
<script>
return {
init() {
console.log(`Value : ${this.value} , Step : ${this.step}`)
},
increase() {
this.value += this.step
}
}
</script>
</template>
<template x-component="counter-trigger">
<button #click="$api.of('counter').increase()"
class="inline-block rounded-lg mt-2 bg-green-600 px-4 py-1.5 text-white shadow ring-1 ring-green-600 hover:bg-green-700 hover:ring-green-700">
Tigger from child element</button>
</template>
</body>

Apply an Class by using VueJS condition

I'm trying to apply a class to a <p> tag using a VueJS condition but it doesn't work at all, despite it working on JSfiddle. It's been 3 days now that I'm stuck, please help me.
VueJS (in main.js)
new Vue({
el: "#test",
data: {
selected: "2"
}
});
HTML (App.vue)
<div id="app">
<div id="test">
{{ selected }}
<p :class="{ selection: 1 === selected }">
heyyy 1
</p>
<p :class="{ selection: 2 == selected }">
heyyy 2
</p>
<button #click="selected = 2">Switch</button>
</div>
</div>
I'll assume you're using Vue-CLI since you have a .vue file, but your main.js is using syntax from Vue CDN. Maybe you copied the code from the fiddle instead.
I'll assume too that you have a CSS class named .selection for this to work.
Either way, App.vue has its own component instance which is not the main.js mounting instance (it's one level deeper). Your code creates selected in the mounting root instead of App.vue.
To fix, create it in App.vue <script> section instead:
<template>
<div id="app">
<div id="test">
{{ selected }}
<p :class="{ selection: 1 === selected }">
heyyy 1
</p>
<p :class="{ selection: 2 == selected }">
heyyy 2
</p>
<button #click="selected = 2">Switch</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
selected: "2"
}
}
}
</script>
<style>
.selection {
/* CSS */
}
</style>
to apply class binding in VueJs you can use it as :
:class="{ 'selection(your class name here)': selected === 1}"

In Vue&Vuex, how can I activate onclick method in child component?

I'm trying to edit JS library that already existed but it consisted of Vue. So I studied Vue a little.
The problem is that I made child component called 'Analysis' and want to anchor function. I made tag and bind 'moveAnchor' method to onclick, also declared 'moveAnchor' on methods part. but it didn't work. How can I fix? I'm sorry for being inexperienced.. :(
it is script.js of analysis.
import { mapActions, mapState } from 'vuex';
export default {
name: 'Analysis',
computed: {
checkName : function(){
var flag = this.$store.state.analysis.name;
if(flag.indexOf("/PCs/") != -1){
console.log(flag);
}
}
},
methods: {
moveAnchor: function (id){
var div = document.getElementById(id).scrollIntoView();
}
it is template.html of analysis.
<div :class="$style.scrollarea">
<div :class="$style.dropdown">
<button :class="$style.dropbtn">Analysess</button>
<div :class="$style.dropContent">
<a v-for="item in analyData" v-bind:key="item.id" #onclick="moveAnchor(item.id)">
{{ item.title }}
</a>
</div>
</div>
<span>
{{ checkName }}
</span>
<div v-for="item in analyData">
<h1 v-bind:id="item.id">{{ item.title }}</h1>
<img v-bind:src="item.src" style="width: 100%; height: auto">
</div>
Welcome to StackExchange!
The correct binding for Vue's click event is v-on:click, or #click for shorthand. So when you write #onclick, Vue will never call that.
Just change #onclick to #click and all should work fine.

Vuejs - Accordion

I'm trying to create an accordion using vuejs.
I found some examples online, but what I want is different. For SEO purpose I use "is" and "inline-template", so the accordion is kind of static not fully created in Vuejs.
I have 2 problems/questions:
1) I need to add a class "is-active" on the component based on user interaction(clicks), because of this I receive the following error.
Property or method "contentVisible" is not defined on the instance but
referenced during render. Make sure to declare reactive data
properties in the data option.
This probable because I need to set it at instance level. But "contentVisible" have a value (true or false) different for each component.
So I thought using at instance level an array of "contentVisible" and a props (pass thru instance) and custom events on child to update the instance values.
2) Could work but it is a static array. How can I make a dynamic array (not knowing the number of item components) ?
<div class="accordion">
<div>
<div class="accordion-item" is="item" inline-template :class="{ 'is-active': contentVisible}" >
<div>
<a #click="toggle" class="accordion-title"> Title A1</a>
<div v-show="contentVisible" class="accordion-content">albatros</div>
</div>
</div>
<div class="accordion-item" is="item" inline-template :class="{ 'is-active': contentVisible}" >
<div>
<a #click="toggle" class="accordion-title"> Title A2</a>
<div v-show="contentVisible" class="accordion-content">lorem ipsum</div>
</div>
</div>
</div>
var item = {
data: function() {
return {
contentVisible: true
}
},
methods: {
toggle: function(){
this.contentVisible = !this.contentVisible
}
}
}
new Vue({
el:'.accordion',
components: {
'item': item
}
})
Update
I create the following code but the custom event to send the modification from component to instance is not working, tabsactive is not changing
var item = {
props: ['active'],
data: function() {
return {
contentVisible: false
}
},
methods: {
toggle: function(index){
this.contentVisible = !this.contentVisible;
this.active[index] = this.contentVisible;
**this.$emit('tabisactive', this.active);**
console.log(this.active);
}
}
}
new Vue({
el:'.accordion',
data: {
tabsactive: [false, false]
},
components: {
'item': item
}
})
<div class="accordion" **#tabisactive="tabsactive = $event"**>
<div class="accordion-item" is="item" inline-template :active="tabsactive" :class="{'is-active': tabsactive[0]}">
<div>
<a #click="toggle(0)" class="accordion-title"> Title A1</a>
<div v-show="contentVisible" class="accordion-content">albatros</div>
</div>
</div>
<div class="accordion-item" is="item" inline-template :active="tabsactive" :class="{'is-active': tabsactive[1]}">
<div>
<a #click="toggle(1)" class="accordion-title" > Title A2</a>
<div v-show="contentVisible" class="accordion-content">lorem ipsum</div>
</div>
</div>
</div>
This works for me:
<template>
<div>
<ul>
<li v-for="index in list" :key="index._id">
<button #click="contentVisible === index._id ? contentVisible = false : contentVisible = index._id">{{ index.title }}</button>
<p v-if='contentVisible === index._id'>{{ index.item }}</p>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "sameName",
data() {
return {
contentVisible: false,
list: [
{
_id: id1,
title: title1,
item: item1
},
{
_id: id2,
title: title2,
item: item2
}
]
};
},
};
</script>
On point 1:
You have to define contentVisible as a vue instance variable, as you have accessed it with vue directive v-show, it searches this in vue data, watchers, methods, etc, and if it does not find any reference, it throws this error.
As your accordion element is associated with the parent component, you may have to add contentVisible data there, like following:
new Vue({
el:'.accordion',
data: {
contentVisible: true
}
components: {
'item': item
}
})
If you have multiple items, you may use some other technique to show one of them, like have a data variable visibleItemIndex which can change from 1 to n-1, where n is number of items.
In that case, you will have v-show="visibleItemIndex == currentIndex" in the HTML.
You can as well have hash for saving which index are to de displayed and which to be collapsed.
On point 2:
You can use v-for if you have dynamic arrays. you can see the documentation here.
I'm having a real hard time understanding what exactly it is you want or why you would want it, but I think this does it?
Vue.component('accordion-item', {
template: '#accordion-item',
methods: {
toggle() {
if(this.contentVisible){
return
}
if(this.$parent.activeTab.length >= 2){
this.$parent.activeTab.shift()
}
this.$parent.activeTab.push(this)
}
},
computed: {
contentVisible() {
return this.$parent.activeTab.some(c => c === this)
}
}
})
const Accordion = Vue.extend({
data() {
return {
activeTab: []
}
},
methods: {
handleToggle($event) {
this.activeTab = []
}
}
})
document.querySelectorAll('.accordion').forEach(el => new Accordion().$mount(el))
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<template id="accordion-item">
<div class="accordion-item" :class="{ 'is-active': contentVisible}">
<slot name="title"></slot>
<div v-show="contentVisible" class="accordion-content" #click="$emit('toggle', $event)">
<slot name="content"></slot>
</div>
</div>
</template>
<div class="accordion">
<accordion-item #toggle="handleToggle">
<p slot="title">a title</p>
<p slot="content">there are words here</p>
</accordion-item>
<accordion-item #toggle="handleToggle">
<p slot="title">titles are for clicking</p>
<p slot="content">you can also click on the words</p>
</accordion-item>
<accordion-item #toggle="handleToggle">
<p slot="title">and another</p>
<p slot="content">only two open at a time!</p>
</accordion-item>
<accordion-item #toggle="handleToggle">
<p slot="title">and #4</p>
<p slot="content">amazing</p>
</accordion-item>
</div>

Categories

Resources