123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755 |
- <template>
- <v-container>
- <div v-if="editor" class="menu-editor">
- <v-btn small @click="openImgModal()" plain>
- <v-icon>mdi-image </v-icon>
- </v-btn>
- <v-btn
- plain
- small
- @click="editor.chain().focus().toggleBold().run()"
- :class="{ 'is-active': editor.isActive('bold') }"
- >
- <v-icon> mdi-format-bold </v-icon>
- </v-btn>
- <v-btn
- plain
- small
- @click="editor.chain().focus().toggleItalic().run()"
- :class="{ 'is-active': editor.isActive('italic') }"
- >
- <v-icon> mdi-format-italic </v-icon>
- </v-btn>
- <v-btn
- plain
- small
- @click="editor.chain().focus().setParagraph().run()"
- :class="{ 'is-active': editor.isActive('paragraph') }"
- >
- <v-icon> mdi-format-paragraph </v-icon>
- </v-btn>
- <v-btn small plain @click="addInstagram">
- <v-icon>mdi-instagram </v-icon>
- </v-btn>
- <v-btn small plain @click="addIframe">
- <v-icon></v-icon>
- iframe
- </v-btn>
- <v-btn small plain @click="openGalleryModalInsert">
- <v-icon></v-icon>
- Галерея
- </v-btn>
- <v-btn
- small
- plain
- @click="openLinkModal"
- :class="{ 'is-active': editor.isActive('link') }"
- >
- ссылка
- </v-btn>
- <v-btn
- small
- plain
- @click="setTooltip"
- :class="{ 'is-active': editor.isActive('tooltip') }"
- >
- подсказка
- </v-btn>
- <v-btn
- plain
- small
- @click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
- :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
- >
- h1
- </v-btn>
- <v-btn
- small
- plain
- @click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
- :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
- >
- h2
- </v-btn>
- <v-btn
- small
- plain
- @click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
- :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
- >
- h3
- </v-btn>
- <v-btn
- plain
- small
- @click="editor.chain().focus().toggleHeading({ level: 4 }).run()"
- :class="{ 'is-active': editor.isActive('heading', { level: 4 }) }"
- >
- h4
- </v-btn>
- <v-btn
- plain
- small
- @click="editor.chain().focus().toggleHeading({ level: 5 }).run()"
- :class="{ 'is-active': editor.isActive('heading', { level: 5 }) }"
- >
- ИФ
- </v-btn>
- <v-btn
- plain
- small
- @click="editor.chain().focus().toggleHeading({ level: 6 }).run()"
- :class="{ 'is-active': editor.isActive('heading', { level: 6 }) }"
- >
- Выделить
- </v-btn>
- <v-btn
- plain
- small
- @click="editor.chain().focus().toggleBulletList().run()"
- :class="{ 'is-active': editor.isActive('bulletList') }"
- >
- <v-icon>mdi-format-list-bulleted</v-icon>
- </v-btn>
- <v-btn
- plain
- small
- @click="editor.chain().focus().toggleOrderedList().run()"
- :class="{ 'is-active': editor.isActive('orderedList') }"
- >
- <v-icon>mdi-format-list-numbered</v-icon>
- </v-btn>
- <v-btn
- plain
- small
- @click="editor.chain().focus().toggleBlockquote().run()"
- :class="{ 'is-active': editor.isActive('blockquote') }"
- >
- <v-icon>mdi-format-quote-close-outline</v-icon>
- </v-btn>
- <v-btn plain small @click="editor.chain().focus().clearNodes().unsetAllMarks().run()">
- <v-icon>mdi-format-clear</v-icon>
- </v-btn>
- <v-btn plain small @click="openHtmlModal">
- <v-icon>mdi-file-code-outline</v-icon>
- </v-btn>
- <v-btn plain small @click="editor.chain().focus().undo().run()">
- <v-icon>mdi-undo-variant</v-icon>
- </v-btn>
- <v-btn plain small @click="editor.chain().focus().redo().run()">
- <v-icon> mdi-redo-variant </v-icon>
- </v-btn>
- </div>
- <div class="editor" ref="editor">
- <editor-content @click.native="onClickEditor" :editor="editor" />
- </div>
- <modal name="link-modal" height="auto" v-if="editor">
- <v-text-field v-model="link.href" label="Ссылка"></v-text-field>
- <v-checkbox class="mt-0" v-model="link.nofollow" label="No follow"></v-checkbox>
- <v-btn :disabled="!link.href" @click="setLink" text color="primary">Ок</v-btn>
- <v-btn @click="unsetLink" text color="red" v-if="editor.isActive('link')">Удалить</v-btn>
- </modal>
- <modal name="img-modal" @closed="image.messages = []">
- <v-file-input
- label="Картинка:"
- type="file"
- accept="image/*"
- ref="inputEditor"
- :error-messages="image.messages"
- v-model="image.file"
- ></v-file-input>
- <v-btn @click="addImage">Добавить изображение</v-btn>
- </modal>
- <modal
- height="auto"
- name="gallery-modal"
- @closed="gallery.messages = []"
- :edit-mode="false"
- >
- <div class="gallery-container" ref="galleryContainer">
- <draggable v-model="gallery.list">
- <transition-group type="transition" name="flip-list">
- <div
- class="gallery-item"
- v-for="(img, index) in gallery.list"
- :key="img.url"
- >
- <img :src="img.url" :alt="img.name" />
- <v-text-field
- prepend-icon="mdi-image-outline"
- placeholder="Название изображения"
- v-model="img.name"
- ></v-text-field>
- <div
- class="gallery-item_delete"
- @click="gallery.list.splice(index, 1)"
- >
- <v-icon> mdi-delete </v-icon>
- </div>
- </div>
- </transition-group>
- </draggable>
- </div>
- <v-file-input
- @change="addImageToGallery"
- small-chips
- multiple
- label="Картинка:"
- type="file"
- accept="image/*"
- ref="galleryFileInput"
- :error-messages="gallery.messages"
- v-model="gallery.files"
- ></v-file-input>
- <v-btn
- text
- color="primary"
- @click="insertGallery"
- v-if="!gallery.editMode"
- :disabled="!gallery.list.length"
- >
- Вставить галерею в редактор
- </v-btn>
- <v-btn
- text
- color="primary"
- @click="editGallery"
- v-else
- :disabled="!gallery.list.length"
- >
- Сохранить галерею
- </v-btn>
- </modal>
- <modal name="html-modal" width="80%" height="auto">
- <v-textarea v-model="content" rows="10" outlined label="Исходный код HTML"></v-textarea>
- <v-btn @click="saveHtml" text color="primary" class="mt-0">Сохранить</v-btn>
- </modal>
- </v-container>
- </template>
- <script>
- import Iframe from "./iframe.ts";
- import Tooltip from "./tooltip.ts";
- import Link from "./link.ts";
- import Gallery from "./gallery";
- import Focus from "@tiptap/extension-focus";
- import Image from "@tiptap/extension-image";
- import { Editor, EditorContent } from "@tiptap/vue-2";
- import StarterKit from "@tiptap/starter-kit";
- import Typography from "@tiptap/extension-typography";
- import draggable from "vuedraggable";
- export default {
- components: {
- EditorContent,
- draggable,
- },
- data() {
- return {
- editor: null,
- content: this.value,
- link: {
- href: '',
- nofollow: false,
- },
- image: {
- file: null,
- messages: [],
- },
- gallery: {
- files: null,
- messages: [],
- list: [],
- editMode: false,
- editID: null,
- },
- galleries: [],
- };
- },
- props: {
- value: {
- type: String,
- default: "",
- },
- },
- mounted() {
- this.editor = new Editor({
- content: this.value,
- extensions: [
- Gallery,
- Iframe,
- Tooltip,
- StarterKit,
- Typography,
- Link,
- Focus.configure({
- className: "has-focus",
- mode: "all",
- }),
- Image.configure({
- inline: true,
- }),
- ,
- ],
- onUpdate: ({ editor }) => {
- this.$emit("input", editor.getHTML());
- },
- onCreate: () => {
- const galleriesDOM = this.$refs.editor.querySelectorAll(".editor-gallery");
- galleriesDOM.forEach((galleryDOM) => {
- const imagesDOM = galleryDOM.querySelectorAll("img[src]");
- const images = [...imagesDOM].map((img) => ({
- url: img.src,
- name: img.alt,
- }));
- const gallery = { id: galleryDOM.id, list: images };
- this.galleries.push(gallery);
- });
- },
- });
- this.editor.commands.setContent(this.value);
- },
- beforeDestroy() {
- if (this.editor) {
- this.editor.destroy();
- }
- },
- methods: {
- openHtmlModal() {
- this.content = this.editor.getHTML();
- this.$modal.show('html-modal');
- },
- saveHtml() {
- this.editor.commands.setContent(this.content);
- this.$modal.hide('html-modal');
- },
- openLinkModal() {
- this.link.href = '';
- this.link.nofollow = false;
- if (this.editor.isActive("link")) {
- const attrs = this.editor.getAttributes('link');
- this.link.href = attrs.href;
- this.link.nofollow = attrs.rel.includes('nofollow');
- }
- this.$modal.show('link-modal');
- },
- setLink() {
- let rel = this.link.nofollow ? 'noopener noreferrer nofollow' : 'noopener noreferrer';
- this.editor
- .chain()
- .focus()
- .extendMarkRange("link")
- .unsetLink()
- .setLink({ href: this.link.href, rel })
- .run();
- this.$modal.hide('link-modal');
- },
- unsetLink() {
- this.editor.chain().focus().unsetLink().run();
- this.$modal.hide('link-modal');
- },
- setTooltip() {
- let dataTitle = '', dataText = '';
- if (this.editor.isActive("tooltip")) {
- const attrs = this.editor.getAttributes('tooltip');
- dataTitle = attrs['data-title'];
- dataText = attrs['data-text'];
- }
- const title = window.prompt("Введите название подсказки", dataTitle);
- if(!title) return
- const text = window.prompt("Введите текст подсказки", dataText);
- if(!text) return;
- this.editor
- .chain()
- .focus()
- .extendMarkRange("tooltip")
- .unsetTooltip()
- .setTooltip({ "data-title": title, "data-text": text })
- .run();
- },
- addIframe() {
- // Метод добавляет не только фреймы, но и любой html
- const url = window.prompt("URL");
- if (url) this.editor.commands.insertContent(url);
- },
- addInstagram() {
- const url = window.prompt("URL");
- if (url){
- if(url.slice(-1) == '/') {
- const newsUrl = url.substring(0, url.length - 1);
- const text = '<iframe src="' + newsUrl + '/embed" class="iframe-instagram" width="320" frameborder="0" scrolling="auto"></iframe>';
- this.editor.commands.insertContent(text);
- }else{
- const text = '<iframe src="' + url + '/embed" class="iframe-instagram" width="320" frameborder="0" scrolling="auto"></iframe>';
- this.editor.commands.insertContent(text);
- }
- }
- },
- onClickEditor(event) {
- if (!event.target.classList.contains("button")) return;
- const gallery = event.target.closest(".editor-gallery");
- const galleryID = gallery.id;
- if (event.target.classList.contains("button-edit")) {
- this.openGalleryModalEdit(galleryID);
- }
- if (event.target.classList.contains("button-delete")) {
- this.galleries = this.galleries.filter((g) => g.id != galleryID);
- gallery.remove();
- }
- },
- addImage(file) {
- if (!this.image.file) return;
- this.image.messages = [];
- this.uploadImage(this.image.file)
- .then((url) => {
- this.editor.chain().focus().setImage({ src: url }).run();
- this.$modal.hide("img-modal");
- })
- .catch((err) => {
- this.image.messages.push(err);
- })
- .finally(() => {
- this.image.file = null;
- });
- },
- addImageToGallery() {
- if (!this.gallery.files) return;
- this.gallery.messages = [];
- const promises = this.gallery.files.map((file) => this.uploadImage(file));
- Promise.all(promises)
- .then((urls) => {
- urls.forEach((url) => this.gallery.list.push({ url, name: "" }));
- })
- .catch((err) => {
- this.gallery.messages.push(err);
- })
- .finally(() => {
- this.$refs.galleryFileInput.blur();
- this.gallery.files = null;
- });
- },
- openImgModal() {
- this.$modal.show("img-modal");
- },
- openGalleryModalEdit(id) {
- if (!id) return;
- this.gallery.editMode = true;
- this.gallery.editID = id;
- this.gallery.list = this.galleries.find((g) => g.id == id).list.slice();
- this.$modal.show("gallery-modal");
- },
- openGalleryModalInsert() {
- this.gallery.editMode = false;
- this.gallery.editID = null;
- this.gallery.list = [];
- this.$modal.show("gallery-modal");
- },
- editGallery() {
- const id = this.gallery.editID;
- const galleryInEditor = this.$refs.editor.querySelector(`#${id} .content`);
- galleryInEditor.innerHTML = this.imagesHTML;
- this.galleries.find((g) => g.id == id).list = this.gallery.list;
- this.$modal.hide("gallery-modal");
- },
- insertGallery() {
- const id = `gallery-${this.galleries.length}`;
- const gallery = { id, list: this.gallery.list };
- this.galleries.push(gallery);
- this.$modal.hide("gallery-modal");
- const content = `<div class="gallery" id="${id}">${this.imagesHTML}</div>`;
- this.editor.commands.insertContent(content);
- },
- uploadImage(file) {
- return new Promise((resolve, reject) => {
- let image = new window.Image();
- image.src = window.URL.createObjectURL(file);
- image.onload = () => {
- let width = image.width;
- let height = image.height;
- if (width < 200 || height < 200) {
- reject("Размеры изображения должны быть не менее 200x200");
- }
- if(width > 1300){
- let ratioPhoto;
- if(width > height){
- ratioPhoto = 1300 / width
- }else{
- ratioPhoto = 900 / height
- }
- width = ratioPhoto * width;
- height = ratioPhoto * height;
- }
- let formData = new FormData();
- formData.append("file", file);
- formData.append("destination_width", width);
- formData.append("destination_height", height);
- formData.append("title", "example");
- this.$axios
- .$post("authorized/admin/files/upload/image/news/raw", formData, {
- headers: {
- "Content-Type": `multipart/form-data;`,
- },
- })
- .then((res) => {
- let url = "https://api.amic.ru/" + res.data.relative_path;
- resolve(url);
- })
- .catch((err) => {
- reject(err.errors.join("* "));
- });
- };
- });
- },
- },
- computed: {
- imagesHTML() {
- return this.gallery.list
- .map((img) => `<img src="${img.url}" alt="${img.name}"></img>`)
- .join("");
- },
- },
- };
- </script>
- <style lang="less">
- .flip-list-move {
- transition: transform 0.2s;
- }
- //Gallery in editor
- .editor-gallery {
- .content {
- display: flex;
- background: #f3f1f1;
- padding: 10px;
- flex-wrap: wrap;
- border-radius: 20px;
- pointer-events: none;
- img {
- width: 10%;
- height: auto;
- margin: 0 10px;
- object-fit: contain;
- }
- img.ProseMirror-separator {
- display: none !important;
- }
- }
- .button {
- margin: 5px;
- }
- }
- //Gallery in modal
- .gallery-container {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- max-height: 600px;
- overflow: auto;
- img {
- max-width: 25%;
- margin: 0 5px;
- }
- }
- .gallery-item {
- cursor: move;
- position: relative;
- &_delete {
- cursor: pointer;
- position: absolute;
- top: 0;
- right: 5px;
- }
- }
- //Tooltip in editor
- span.tooltip {
- background: #f3f1f1;
- border-bottom: 1px dashed black;
- html.dark-mode & {
- background: rgba(243, 241, 241, 0.2);
- border-bottom: 1px dashed white;
- }
- }
- .ProseMirror {
- min-height: 800px;
- max-height: 800px;
- overflow-y: scroll;
- outline: none;
- padding: 0px 15px;
- &::-webkit-scrollbar {
- width: 10px;
- &-track {
- background-color: rgba(0, 0, 0, 0.1);
- }
- &-thumb {
- background-color: black;
- border-radius: 15px;
- }
- }
- }
- .editor {
- margin: 10px auto 0;
- border: 1px solid rgba(0, 0, 0, 0.5);
- border-radius: 15px;
- min-height: 600px;
- overflow: hidden;
- p,
- h1,
- h2,
- h3,
- h4 {
- font: "Golos Text", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
- Helvetica, Ubuntu, Arial, sans-serif;
- }
- h1 {
- font: 36px/43px "Golos Text", -apple-system, BlinkMacSystemFont, "Segoe UI",
- Roboto, Helvetica, Ubuntu, Arial, sans-serif;
- font-weight: 700;
- }
- p {
- margin-bottom: 20px;
- font: 18px/29px "Golos Text", -apple-system, BlinkMacSystemFont, "Segoe UI",
- Roboto, Helvetica, Ubuntu, Arial, sans-serif;
- padding: 3px;
- }
- blockquote p {
- font-size: 28px;
- line-height: 140%;
- }
- }
- .menu-editor {
- width: 100%;
- display: flex;
- justify-content: center;
- flex-wrap: wrap;
- margin: 0 auto;
- margin-top: 40px;
- button {
- margin-left: 10px;
- margin-bottom: 15px;
- }
- }
- .has-focus {
- outline: 1px solid #bdb4b4;
- border-radius: 2px;
- }
- .v-btn:not(.v-btn--round).v-size--small {
- min-width: 15px;
- padding: 5px 5px;
- &:hover {
- background: black;
- color: white;
- }
- margin-bottom: 0;
- }
- .no-wrap {
- flex-wrap: nowrap !important;
- }
- .editor {
- img {
- max-width: 100% !important;
- }
- a {
- pointer-events: none;
- }
- }
- .vm--modal {
- padding: 15px;
- }
- </style>
|