Add tinymce editor

pull/3890/head
Julio Montoya 4 years ago
parent d55430ce89
commit 53207cf9d7
  1. 111
      assets/vue/components/documents/FormNewDocument.vue
  2. 8
      assets/vue/router/documents.js
  3. 1
      assets/vue/router/index.js
  4. 397
      assets/vue/views/documents/DocumentManager.vue
  5. 4
      assets/vue/views/documents/List.vue

@ -1,49 +1,50 @@
<template>
<q-form>
<q-input
id="item_title"
v-model="item.title"
:error="v$.item.title.$error"
:error-message="titleErrors"
:placeholder="$t('Title')"
@input="v$.item.title.$touch()"
@blur="v$.item.title.$touch()"
/>
<editor
id="item_content"
v-if="(item.resourceNode && item.resourceNode.resourceFile && item.resourceNode.resourceFile.text) || item.newDocument"
v-model="item.contentFile"
:error-message="contentFileErrors"
required
:init="{
<q-form>
<q-input
id="item_title"
v-model="item.title"
:error="v$.item.title.$error"
:error-message="titleErrors"
:placeholder="$t('Title')"
@input="v$.item.title.$touch()"
@blur="v$.item.title.$touch()"
/>
<editor
id="item_content"
v-if="(item.resourceNode && item.resourceNode.resourceFile && item.resourceNode.resourceFile.text) || item.newDocument"
v-model="item.contentFile"
:error-message="contentFileErrors"
required
:init="{
skin_url: '/build/libs/tinymce/skins/ui/oxide',
content_css: '/build/libs/tinymce/skins/content/default/content.css',
branding:false,
branding: false,
relative_urls: false,
height: 500,
toolbar_mode: 'sliding',
file_picker_callback : browser,
/*file_picker_callback: function(callback, value, meta) {
// Provide file and text for the link dialog
if (meta.filetype == 'file') {
callback('mypage.html', {text: 'My text'});
}
// Provide image and alt text for the image dialog
if (meta.filetype == 'image') {
callback('myimage.jpg', {alt: 'My alt text'});
}
// Provide alternative source and posted for the media dialog
if (meta.filetype == 'media') {
callback('movie.mp4', {source2: 'alt.ogg', poster: 'image.jpg'});
}
},*/
/*images_upload_handler: (blobInfo, success, failure) => {
const img = 'data:image/jpeg;base64,' + blobInfo.base64();
//console.log(img);
success(img);
},*/
// Provide file and text for the link dialog
if (meta.filetype == 'file') {
callback('mypage.html', {text: 'My text'});
}
// Provide image and alt text for the image dialog
if (meta.filetype == 'image') {
callback('myimage.jpg', {alt: 'My alt text'});
}
// Provide alternative source and posted for the media dialog
if (meta.filetype == 'media') {
callback('movie.mp4', {source2: 'alt.ogg', poster: 'image.jpg'});
}
},*/
/*images_upload_handler: (blobInfo, success, failure) => {
const img = 'data:image/jpeg;base64,' + blobInfo.base64();
//console.log(img);
success(img);
},*/
//menubar: true,
autosave_ask_before_unload: true,
plugins: [
@ -54,11 +55,10 @@
toolbar: 'undo redo | bold italic underline strikethrough | fontselect fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | forecolor backcolor removeformat | pagebreak | charmap emoticons | fullscreen preview save print | insertfile image media template link anchor code codesample | ltr rtl',
}
"
/>
<!-- For extra content-->
<slot></slot>
</q-form>
/>
<!-- For extra content-->
<slot></slot>
</q-form>
</template>
<script>
@ -67,6 +67,7 @@ import useVuelidate from '@vuelidate/core';
import { required } from '@vuelidate/validators';
//import UploadAdapter from './UploadAdapter';
import Editor from '../Editor'
import {useRouter} from "vue-router";
export default {
name: 'DocumentsForm',
@ -132,8 +133,28 @@ export default {
},
methods: {
browser (callback, value, meta) {
let url = '/resources/document/4/manager?cid=1&sid=0&gid=0';
if (meta.filetype === 'image') {
url = url + "&type=images";
} else {
url = url + "&type=files";
}
console.log(url);
window.addEventListener('message', function (event) {
var data = event.data;
if (data.url) {
url = data.url;
console.log(meta); // {filetype: "image", fieldname: "src"}
callback(url);
}
});
tinymce.activeEditor.windowManager.openUrl({
url: '/',// use an absolute path!
url: url,// use an absolute path!
title: 'file manager',
/*width: 900,
height: 450,

@ -10,6 +10,14 @@ export default {
path: '',
component: () => import('../views/documents/List.vue')
},
{
name: 'DocumentManager',
path: 'manager',
component: () => import('../views/documents/DocumentManager.vue'),
meta: {
layout: 'Empty'
}
},
{
name: 'DocumentsCreate',
path: 'new',

@ -42,7 +42,6 @@ const router = createRouter({
path: '/courses', name: 'MyCourses', component: MyCourseList,
meta: {requiresAuth: true},
},
{
path: '/sessions', name: 'MySessions', component: MySessionList,
meta: {requiresAuth: true},

@ -0,0 +1,397 @@
<template>
<Toolbar class="p-mb-4">
<template #left>
<div v-if="isAuthenticated && isCurrentTeacher" class="flex flex-row gap-2" >
<!-- <Button label="New" icon="pi pi-plus" class="p-button-primary p-button-sm p-mr-2" @click="openNew" />-->
<Button label="New" icon="pi pi-plus" class="btn btn-primary" @click="openNew" />
<!-- <Button label="New folder" icon="pi pi-plus" class="p-button-success p-mr-2" @click="addHandler()" />-->
<!-- <Button label="New document" icon="pi pi-plus" class="p-button-sm p-button-primary p-mr-2" @click="addDocumentHandler()" />-->
<Button label="New document" icon="pi pi-plus" class="btn btn-primary" @click="addDocumentHandler()" />
<Button label="Upload" icon="pi pi-plus" class="btn btn-primary" @click="uploadDocumentHandler()" />
</div>
</template>
</Toolbar>
<DataTable
class="p-datatable-sm"
:value="items"
v-model:selection="selectedItems"
dataKey="iid"
v-model:filters="filters"
filterDisplay="menu"
:lazy="true"
:paginator="true"
:rows="10"
:totalRecords="totalItems"
:loading="isLoading"
@page="onPage($event)"
@sort="sortingChanged($event)"
paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
:rowsPerPageOptions="[5, 10, 20, 50]"
responsiveLayout="scroll"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords}"
:globalFilterFields="['resourceNode.title', 'resourceNode.updatedAt']"
>
<Column field="resourceNode.title" :header="$t('Title')" :sortable="true">
<template #body="slotProps">
<div v-if="slotProps.data && slotProps.data.resourceNode && slotProps.data.resourceNode.resourceFile">
<ResourceFileLink :resource="slotProps.data" />
</div>
<div v-else>
<a
v-if="slotProps.data"
@click="handleClick(slotProps.data)"
class="cursor-pointer " >
<font-awesome-icon
icon="folder"
size="lg"
/>
{{ slotProps.data.resourceNode.title }}
</a>
</div>
</template>
</Column>
<Column field="resourceNode.updatedAt" :header="$t('Modified')" :sortable="true">
<template #body="slotProps">
{{$luxonDateTime.fromISO(slotProps.data.resourceNode.updatedAt).toRelative() }}
</template>
</Column>
<Column field="resourceNode.resourceFile.size" :header="$t('Size')" :sortable="true">
<template #body="slotProps">
{{
slotProps.data.resourceNode.resourceFile ? $filters.prettyBytes(slotProps.data.resourceNode.resourceFile.size) : ''
}}
</template>
</Column>
<Column :exportable="false">
<template #body="slotProps">
<div class="flex flex-row gap-2">
<Button label="Select" class="p-button-sm p-button p-mr-2" @click="returnToEditor(slotProps.data)" />
</div>
</template>
</Column>
</DataTable>
<Dialog v-model:visible="itemDialog" :style="{width: '450px'}" :header="$t('New folder')" :modal="true" class="p-fluid">
<div class="p-field">
<label for="name">{{ $t('Name') }}</label>
<InputText
autocomplete="off"
id="title"
v-model.trim="item.title"
required="true"
autofocus
:class="{'p-invalid': submitted && !item.title}"
/>
<small class="p-error" v-if="submitted && !item.title">$t('Title is required')</small>
</div>
<template #footer>
<Button label="Cancel" icon="pi pi-times" class="p-button-text" @click="hideDialog"/>
<Button label="Save" icon="pi pi-check" class="p-button-text" @click="saveItem" />
</template>
</Dialog>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import ListMixin from '../../mixins/ListMixin';
import ActionCell from '../../components/ActionCell.vue';
//import Toolbar from '../../components/Toolbar.vue';
import ResourceFileIcon from './ResourceFileIcon.vue';
import ResourceFileLink from './ResourceFileLink.vue';
import { useRoute } from 'vue-router'
import DataFilter from '../../components/DataFilter';
import DocumentsFilterForm from '../../components/documents/Filter';
import { ref, reactive, onMounted, computed } from 'vue';
import { useStore } from 'vuex';
import isEmpty from 'lodash/isEmpty';
import moment from "moment";
export default {
name: 'DocumentsList',
servicePrefix: 'Documents',
components: {
//8Toolbar,
ActionCell,
ResourceFileIcon,
ResourceFileLink,
DocumentsFilterForm,
DataFilter
},
mixins: [ListMixin],
data() {
return {
sortBy: 'title',
sortDesc: false,
columns: [
{ label: this.$i18n.t('Title'), field: 'title', name: 'title', sortable: true},
{ label: this.$i18n.t('Modified'), field: 'resourceNode.updatedAt', name: 'updatedAt', sortable: true},
{ label: this.$i18n.t('Size'), field: 'resourceNode.resourceFile.size', name: 'size', sortable: true},
{ label: this.$i18n.t('Actions'), name: 'action', sortable: false}
],
pageOptions: [10, 20, 50, this.$i18n.t('All')],
selected: [],
isBusy: false,
options: [],
selectedItems: [],
// prime vue
itemDialog: false,
deleteItemDialog: false,
deleteMultipleDialog: false,
item: {},
filters: {},
submitted: false,
};
},
created() {
console.log('created - vue/views/documents/List.vue');
/*const route = useRoute();
let nodeId = route.params['node'];
if (!isEmpty(nodeId)) {
this.findResourceNode('/api/resource_nodes/' + nodeId);
}
this.onUpdateOptions(this.options);*/
//this.initFilters1();
/*
this.onRequest({
pagination: this.pagination,
});*/
},
mounted() {
console.log('mounted - vue/views/documents/List.vue');
const route = useRoute()
/*let nodeId = route.params['node'];
if (!isEmpty(nodeId)) {
this.findResourceNode('/api/resource_nodes/' + nodeId);
}*/
this.onUpdateOptions(this.options);
/*this.onRequest({
pagination: this.pagination,
});*/
// Detect when scrolled to bottom.
/*const listElm = document.querySelector('#documents');
listElm.addEventListener('scroll', e => {
console.log('aaa');
if(listElm.scrollTop + listElm.clientHeight >= listElm.scrollHeight) {
this.onScroll();
}
});*/
//const tableScrollBody = this.$refs['selectableTable'].$el;
/* Consider debouncing the event call */
//tableScrollBody.addEventListener("scroll", this.onScroll);
//window.addEventListener('scroll', this.onScroll)
window.addEventListener('scroll', () =>{
/*if(window.top.scrollY > window.outerHeight){
if (!this.isBusy) {
this.fetchItems();
}
}*/
});
/*const tableScrollBody = this.$refs['documents'];
tableScrollBody.addEventListener("scroll", this.onScroll);*/
},
computed: {
// From crud.js list function
...mapGetters('resourcenode', {
resourceNode: 'getResourceNode'
}),
...mapGetters({
'isAuthenticated': 'security/isAuthenticated',
'isAdmin': 'security/isAdmin',
'isCurrentTeacher': 'security/isCurrentTeacher',
}),
...mapGetters('documents', {
items: 'list',
}),
//...getters
// From ListMixin
...mapFields('documents', {
deletedItem: 'deleted',
error: 'error',
isLoading: 'isLoading',
resetList: 'resetList',
totalItems: 'totalItems',
view: 'view'
}),
},
methods: {
// prime
onPage(event) {
console.log(event);
console.log(event.page);
console.log(event.sortField);
console.log(event.sortOrder);
this.options.itemsPerPage = event.rows;
this.options.page = event.page + 1;
this.options.sortBy = event.sortField;
this.options.sortDesc = event.sortOrder === -1;
this.onUpdateOptions(this.options);
},
sortingChanged(event) {
console.log('sortingChanged');
console.log(event);
this.options.sortBy = event.sortField;
this.options.sortDesc = event.sortOrder === -1;
this.onUpdateOptions(this.options);
// ctx.sortBy ==> Field key for sorting by (or null for no sorting)
// ctx.sortDesc ==> true if sorting descending, false otherwise
},
openNew() {
this.item = {};
this.submitted = false;
this.itemDialog = true;
},
hideDialog() {
this.itemDialog = false;
this.submitted = false;
},
saveItem() {
this.submitted = true;
if (this.item.title.trim()) {
if (this.item.id) {
} else {
//this.products.push(this.product);
this.item.parentResourceNodeId = this.$route.params.node;
this.item.resourceLinkList = JSON.stringify([{
gid: this.$route.query.gid,
sid: this.$route.query.sid,
c_id: this.$route.query.cid,
visibility: 2, // visible by default
}]);
this.create(this.item);
this.showMessage('Saved');
}
this.itemDialog = false;
this.item = {};
}
},
editItem(item) {
this.item = {...item};
this.itemDialog = true;
},
confirmDeleteItem(item) {
this.item = item;
this.deleteItemDialog = true;
},
confirmDeleteMultiple() {
this.deleteMultipleDialog = true;
},
deleteMultipleItems() {
console.log('deleteMultipleItems');
console.log(this.selectedItems);
this.deleteMultipleAction(this.selectedItems);
this.onRequest({
pagination: this.pagination,
});
this.deleteMultipleDialog = false;
this.selectedItems = null;
//this.$toast.add({severity:'success', summary: 'Successful', detail: 'Products Deleted', life: 3000});*/
},
returnToEditor(item) {
console.log(item.contentUrl);
window.parent.postMessage({
url: item.contentUrl
}, '*');
parent.tinymce.activeEditor.windowManager.close();
},
deleteItemButton() {
console.log('deleteItem');
this.deleteItem(this.item);
//this.items = this.items.filter(val => val.iid !== this.item.iid);
this.deleteItemDialog = false;
this.item = {};
this.onUpdateOptions(this.options);
},
async fetchItems() {
console.log('fetchItems');
/* No need to call if all items retrieved */
if (this.items.length === this.totalItems) return;
/* Enable busy state */
this.isBusy = true;
/* Missing error handling if call fails */
let currentPage = this.options.page;
console.log(currentPage);
const startIndex = currentPage++ * this.options.itemsPerPage;
const endIndex = startIndex + this.options.itemsPerPage;
console.log(this.items.length);
console.log(this.totalItems);
console.log(startIndex, endIndex);
this.options.page = currentPage;
await this.fetchNewItems(this.options);
//const newItems = await this.callDatabase(startIndex, endIndex);
/* Add new items to existing ones */
//this.items = this.items.concat(newItems);
/* Disable busy state */
this.isBusy = false;
return true;
},
onRowSelected(items) {
this.selected = items
},
selectAllRows() {
this.$refs.selectableTable.selectAllRows()
},
clearSelected() {
this.$refs.selectableTable.clearSelected()
},
async deleteSelected() {
console.log('deleteSelected');
/*for (let i = 0; i < this.selected.length; i++) {
let item = this.selected[i];
//this.deleteHandler(item);
this.deleteItem(item);
}*/
this.deleteMultipleAction(this.selected);
this.onRequest({
pagination: this.pagination,
});
console.log('end -- deleteSelected');
},
//...actions,
// From ListMixin
...mapActions('documents', {
getPage: 'fetchAll',
create: 'create',
deleteItem: 'del',
deleteMultipleAction: 'delMultiple'
}),
...mapActions('resourcenode', {
findResourceNode: 'findResourceNode',
}),
}
};
</script>

@ -189,8 +189,8 @@
<Column :exportable="false">
<template #body="slotProps">
<div class="flex flex-row gap-2">
<Button label="Show" class="p-button-sm p-button p-button-success p-mr-2" @click="showHandler(slotProps.data)" />
<Button v-if="isAuthenticated && isCurrentTeacher" label="Edit" icon="pi pi-pencil" class="p-button-sm p-button p-button-success p-mr-2" @click="editHandler(slotProps.data)" />
<Button label="Show" class="p-button-sm p-button p-mr-2" @click="showHandler(slotProps.data)" />
<Button v-if="isAuthenticated && isCurrentTeacher" label="Edit" icon="pi pi-pencil" class="p-button-sm p-button p-mr-2" @click="editHandler(slotProps.data)" />
<Button v-if="isAuthenticated && isCurrentTeacher" label="Delete" icon="pi pi-trash" class="p-button-sm p-button p-button-danger" @click="confirmDeleteItem(slotProps.data)" />
</div>
</template>

Loading…
Cancel
Save