Editor.vue 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. <template>
  2. <!-- Two-way Data-Binding -->
  3. <section class="editor">
  4. <div class="columns">
  5. <div class="column is-6">
  6. <b-field label="Format">
  7. <div>
  8. <b-radio v-model="form.radioFormat"
  9. @input="onChangeFormat" :disabled="disabled" name="format"
  10. native-value="richtext">Rich text</b-radio>
  11. <b-radio v-model="form.radioFormat"
  12. @input="onChangeFormat" :disabled="disabled" name="format"
  13. native-value="html">Raw HTML</b-radio>
  14. </div>
  15. </b-field>
  16. </div>
  17. <div class="column is-6 has-text-right">
  18. <b-button @click="togglePreview" type="is-primary"
  19. icon-left="file-find-outline">Preview</b-button>
  20. </div>
  21. </div>
  22. <!-- wsywig //-->
  23. <quill-editor
  24. v-if="form.format === 'richtext'"
  25. v-model="form.body"
  26. ref="quill"
  27. :options="options"
  28. :disabled="disabled"
  29. placeholder="Content here"
  30. @change="onEditorChange($event)"
  31. @ready="onEditorReady($event)"
  32. />
  33. <!-- raw html editor //-->
  34. <div v-if="form.format === 'html'"
  35. ref="htmlEditor" id="html-editor" class="html-editor"></div>
  36. <!-- campaign preview //-->
  37. <campaign-preview v-if="isPreviewing"
  38. @close="togglePreview"
  39. type='campaign'
  40. :id='id'
  41. :title='title'
  42. :body="form.body"></campaign-preview>
  43. <!-- image picker -->
  44. <b-modal scroll="keep" :aria-modal="true" :active.sync="isMediaVisible" :width="900">
  45. <div class="modal-card content" style="width: auto">
  46. <section expanded class="modal-card-body">
  47. <media isModal @selected="onMediaSelect" />
  48. </section>
  49. </div>
  50. </b-modal>
  51. </section>
  52. </template>
  53. <script>
  54. import 'quill/dist/quill.snow.css';
  55. import 'quill/dist/quill.core.css';
  56. import { quillEditor } from 'vue-quill-editor';
  57. import CodeFlask from 'codeflask';
  58. import CampaignPreview from './CampaignPreview.vue';
  59. import Media from '../views/Media.vue';
  60. export default {
  61. components: {
  62. Media,
  63. CampaignPreview,
  64. quillEditor,
  65. },
  66. props: {
  67. id: Number,
  68. title: String,
  69. body: String,
  70. contentType: String,
  71. disabled: Boolean,
  72. },
  73. data() {
  74. return {
  75. isPreviewing: false,
  76. isMediaVisible: false,
  77. form: {
  78. body: '',
  79. format: this.contentType,
  80. // Model bound to the checkboxes. This changes on click of the radio,
  81. // but is reverted by the change handler if the user cancels the
  82. // conversion warning. This is used to set the value of form.format
  83. // that the editor uses to render content.
  84. radioFormat: this.contentType,
  85. },
  86. // Quill editor options.
  87. options: {
  88. placeholder: 'Content here',
  89. modules: {
  90. toolbar: {
  91. container: [
  92. [{ header: [1, 2, 3, false] }],
  93. ['bold', 'italic', 'underline', 'strike', 'blockquote', 'code'],
  94. [{ color: [] }, { background: [] }, { size: [] }],
  95. [
  96. { list: 'ordered' },
  97. { list: 'bullet' },
  98. { indent: '-1' },
  99. { indent: '+1' },
  100. ],
  101. [
  102. { align: '' },
  103. { align: 'center' },
  104. { align: 'right' },
  105. { align: 'justify' },
  106. ],
  107. ['link', 'image'],
  108. ['clean', 'font'],
  109. ],
  110. handlers: {
  111. image: this.toggleMedia,
  112. },
  113. },
  114. },
  115. },
  116. };
  117. },
  118. methods: {
  119. onChangeFormat(format) {
  120. this.$utils.confirm(
  121. 'The content may lose some formatting. Are you sure?',
  122. () => {
  123. this.form.format = format;
  124. this.onEditorChange();
  125. },
  126. () => {
  127. // On cancel, undo the radio selection.
  128. this.form.radioFormat = format === 'richtext' ? 'html' : 'richtext';
  129. },
  130. );
  131. },
  132. onEditorReady() {
  133. // Hack to focus the editor on page load.
  134. this.$nextTick(() => {
  135. window.setTimeout(() => this.$refs.quill.quill.focus(), 100);
  136. });
  137. },
  138. onEditorChange() {
  139. // The parent's v-model gets { contentType, body }.
  140. this.$emit('input', { contentType: this.form.format, body: this.form.body });
  141. },
  142. initHTMLEditor() {
  143. // CodeFlask editor is rendered in a shadow DOM tree to keep its styles
  144. // sandboxed away from the global styles.
  145. const el = document.createElement('code-flask');
  146. el.attachShadow({ mode: 'open' });
  147. el.shadowRoot.innerHTML = `
  148. <style>
  149. .codeflask .codeflask__flatten { font-size: 15px; }
  150. .codeflask .codeflask__lines { background: #fafafa; }
  151. </style>
  152. <div id="area"></area>
  153. `;
  154. this.$refs.htmlEditor.appendChild(el);
  155. const flask = new CodeFlask(el.shadowRoot.getElementById('area'), {
  156. language: 'html',
  157. lineNumbers: true,
  158. styleParent: el.shadowRoot,
  159. readonly: this.disabled,
  160. });
  161. flask.updateCode(this.form.body);
  162. flask.onUpdate((b) => {
  163. this.form.body = b;
  164. this.$emit('input', { contentType: this.form.format, body: this.form.body });
  165. });
  166. },
  167. togglePreview() {
  168. this.isPreviewing = !this.isPreviewing;
  169. },
  170. toggleMedia() {
  171. this.isMediaVisible = !this.isMediaVisible;
  172. },
  173. onMediaSelect(m) {
  174. this.$refs.quill.quill.insertEmbed(10, 'image', m.url);
  175. },
  176. },
  177. computed: {
  178. htmlFormat() {
  179. return this.form.format;
  180. },
  181. },
  182. watch: {
  183. // Capture contentType and body passed from the parent as props.
  184. contentType(f) {
  185. this.form.format = f;
  186. this.form.radioFormat = f;
  187. // Trigger the change event so that the body and content type
  188. // are propagated to the parent on first load.
  189. this.onEditorChange();
  190. },
  191. body(b) {
  192. this.form.body = b;
  193. },
  194. htmlFormat(f) {
  195. if (f !== 'html') {
  196. return;
  197. }
  198. this.$nextTick(() => {
  199. this.initHTMLEditor();
  200. });
  201. },
  202. },
  203. mounted() {
  204. },
  205. };
  206. </script>