TEditor.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. <template>
  2. <div>
  3. <div class="teditor-box" :class="[spinShow?'teditor-loadstyle':'teditor-loadedstyle']">
  4. <textarea ref="myTextarea" :id="id">{{ content }}</textarea>
  5. <Spin fix v-if="spinShow">
  6. <Icon type="ios-loading" size=18 class="teditor-spin-icon-load"></Icon>
  7. <div>加载组件中...</div>
  8. </Spin>
  9. <img-upload ref="myUpload" class="teditor-upload" type="callback" @on-callback="editorImage" num="50" style="margin-top:5px;height:26px;"></img-upload>
  10. </div>
  11. <Modal v-model="transfer" class="teditor-transfer" @on-visible-change="transferChange" footer-hide fullscreen transfer>
  12. <div slot="close">
  13. <Button type="primary" size="small">完成</Button>
  14. </div>
  15. <div class="teditor-transfer-body">
  16. <textarea :id="'T_' + id">{{ content }}</textarea>
  17. </div>
  18. </Modal>
  19. </div>
  20. </template>
  21. <style lang="scss">
  22. .teditor-box {
  23. textarea {
  24. opacity: 0;
  25. }
  26. .tox-tinymce {
  27. box-shadow: none;
  28. box-sizing: border-box;
  29. border-color: #dddee1;
  30. border-radius: 4px;
  31. overflow: hidden;
  32. .tox-statusbar {
  33. span.tox-statusbar__branding {
  34. a {
  35. display: none;
  36. }
  37. }
  38. }
  39. }
  40. }
  41. .teditor-transfer {
  42. background-color: #ffffff;
  43. .ivu-modal-header {
  44. display: none;
  45. }
  46. .ivu-modal-close {
  47. top: 7px;
  48. }
  49. .teditor-transfer-body {
  50. position: absolute;
  51. top: 0;
  52. left: 0;
  53. width: 100%;
  54. height: 100%;
  55. padding: 0;
  56. margin: 0;
  57. textarea {
  58. opacity: 0;
  59. }
  60. .tox-tinymce {
  61. border: 0;
  62. .tox-statusbar {
  63. span.tox-statusbar__branding {
  64. a {
  65. display: none;
  66. }
  67. }
  68. }
  69. }
  70. }
  71. }
  72. .tox {
  73. &.tox-silver-sink {
  74. z-index: 13000;
  75. }
  76. }
  77. </style>
  78. <style lang="scss" scoped>
  79. .teditor-loadstyle {
  80. width: 100%;
  81. height: 180px;
  82. overflow: hidden;
  83. position: relative;
  84. }
  85. .teditor-loadedstyle {
  86. width: 100%;
  87. max-height: inherit;
  88. overflow: inherit;
  89. position: relative;
  90. }
  91. .teditor-spin-icon-load {
  92. animation: ani-teditor-spin 1s linear infinite;
  93. }
  94. @keyframes ani-teditor-spin {
  95. from { transform: rotate(0deg);}
  96. 50% { transform: rotate(180deg);}
  97. to { transform: rotate(360deg);}
  98. }
  99. .teditor-upload {
  100. display: none;
  101. width: 0;
  102. height: 0;
  103. overflow: hidden;
  104. }
  105. </style>
  106. <script>
  107. import tinymce from 'tinymce/tinymce';
  108. import ImgUpload from "./ImgUpload";
  109. export default {
  110. name: 'TEditor',
  111. components: {ImgUpload},
  112. props: {
  113. id: {
  114. type: String,
  115. default: () => {
  116. return "tinymce_" + Math.round(Math.random() * 10000);
  117. }
  118. },
  119. value: {
  120. default: ''
  121. },
  122. height: {
  123. default: 360,
  124. },
  125. htmlClass: {
  126. default: '',
  127. type: String
  128. },
  129. plugins: {
  130. type: Array,
  131. default: () => {
  132. return [
  133. 'advlist autolink lists link image charmap print preview hr anchor pagebreak imagetools',
  134. 'searchreplace visualblocks visualchars code',
  135. 'insertdatetime media nonbreaking save table contextmenu directionality',
  136. 'emoticons paste textcolor colorpicker imagetools codesample'
  137. ];
  138. }
  139. },
  140. toolbar1: {
  141. type: String,
  142. default: ' undo redo | styleselect | uploadImages | bold italic underline forecolor backcolor | alignleft aligncenter alignright | bullist numlist outdent indent | link image emoticons media codesample | preview screenload',
  143. },
  144. toolbar2: {
  145. type: String,
  146. default: '',
  147. },
  148. other_options: {
  149. type: Object,
  150. default: () => {
  151. return {};
  152. }
  153. },
  154. readonly: {
  155. type: Boolean,
  156. default: false
  157. }
  158. },
  159. data() {
  160. return {
  161. content: '',
  162. editor: null,
  163. editorT: null,
  164. cTinyMce: null,
  165. checkerTimeout: null,
  166. isTyping: false,
  167. spinShow: true,
  168. transfer: false,
  169. };
  170. },
  171. mounted() {
  172. this.content = this.value;
  173. this.init();
  174. },
  175. activated() {
  176. this.content = this.value;
  177. this.init();
  178. },
  179. deactivated() {
  180. if (this.editor !== null) {
  181. this.editor.destroy();
  182. }
  183. this.spinShow = true;
  184. $A(this.$refs.myTextarea).show();
  185. },
  186. watch: {
  187. value(newValue) {
  188. if (newValue == null) {
  189. newValue = "";
  190. }
  191. if (!this.isTyping) {
  192. if (this.getEditor() !== null) {
  193. this.getEditor().setContent(newValue);
  194. } else{
  195. this.content = newValue;
  196. }
  197. }
  198. },
  199. readonly(value) {
  200. if (this.editor !== null) {
  201. if (value) {
  202. this.editor.setMode('readonly');
  203. } else {
  204. this.editor.setMode('design');
  205. }
  206. }
  207. }
  208. },
  209. methods: {
  210. init() {
  211. this.$nextTick(() => {
  212. tinymce.init(this.concatAssciativeArrays(this.options(false), this.other_options));
  213. });
  214. },
  215. initTransfer() {
  216. this.$nextTick(() => {
  217. tinymce.init(this.concatAssciativeArrays(this.options(true), this.other_options));
  218. });
  219. },
  220. options(isFull) {
  221. return {
  222. selector: (isFull ? '#T_' : '#') + this.id,
  223. language: "zh_CN",
  224. toolbar1: this.toolbar1,
  225. toolbar2: this.toolbar2,
  226. plugins: this.plugins,
  227. menu: {
  228. view: {
  229. title: 'View',
  230. items: 'code | visualaid visualchars visualblocks | spellchecker | preview fullscreen screenload | showcomments'
  231. },
  232. insert: {
  233. title: "Insert",
  234. items: "image link media addcomment pageembed template codesample inserttable | charmap emoticons hr | pagebreak nonbreaking anchor toc | insertdatetime | uploadImages browseImages"
  235. }
  236. },
  237. codesample_languages: [
  238. {text:"HTML/VUE/XML",value:"markup"},
  239. {text:"JavaScript",value:"javascript"},
  240. {text:"CSS",value:"css"},
  241. {text:"PHP",value:"php"},
  242. {text:"Ruby",value:"ruby"},
  243. {text:"Python",value:"python"},
  244. {text:"Java",value:"java"},
  245. {text:"C",value:"c"},
  246. {text:"C#",value:"csharp"},
  247. {text:"C++",value:"cpp"}
  248. ],
  249. height: isFull ? '100%' : ($A.runNum(this.height) || 360),
  250. resize: !isFull,
  251. convert_urls:false,
  252. toolbar_drawer: 'floating',
  253. setup: (editor) => {
  254. editor.ui.registry.addMenuButton('uploadImages', {
  255. text: '图片',
  256. tooltip: '上传/浏览 图片',
  257. fetch: (callback) => {
  258. let items = [{
  259. type: 'menuitem',
  260. text: '上传图片',
  261. onAction: () => {
  262. this.$refs.myUpload.handleClick();
  263. }
  264. }, {
  265. type: 'menuitem',
  266. text: '浏览图片',
  267. onAction: () => {
  268. this.$refs.myUpload.browsePicture();
  269. }
  270. }];
  271. callback(items);
  272. }
  273. });
  274. editor.ui.registry.addMenuItem('uploadImages', {
  275. text: '上传图片',
  276. onAction: () => {
  277. this.$refs.myUpload.handleClick();
  278. }
  279. });
  280. editor.ui.registry.addMenuItem('browseImages', {
  281. text: '浏览图片',
  282. onAction: () => {
  283. this.$refs.myUpload.browsePicture();
  284. }
  285. });
  286. if (isFull) {
  287. editor.ui.registry.addButton('screenload', {
  288. icon: 'fullscreen',
  289. tooltip: '退出全屏',
  290. onAction: () => {
  291. this.closeFull();
  292. }
  293. });
  294. editor.ui.registry.addMenuItem('screenload', {
  295. text: '退出全屏',
  296. onAction: () => {
  297. this.closeFull();
  298. }
  299. });
  300. editor.on('Init', (e) => {
  301. this.editorT = editor;
  302. this.editorT.setContent(this.content);
  303. if (this.readonly) {
  304. this.editorT.setMode('readonly');
  305. } else {
  306. this.editorT.setMode('design');
  307. }
  308. });
  309. }else{
  310. editor.ui.registry.addButton('screenload', {
  311. icon: 'fullscreen',
  312. tooltip: '全屏',
  313. onAction: () => {
  314. this.content = editor.getContent();
  315. this.transfer = true;
  316. this.initTransfer();
  317. }
  318. });
  319. editor.ui.registry.addMenuItem('screenload', {
  320. text: '全屏',
  321. onAction: () => {
  322. this.content = editor.getContent();
  323. this.transfer = true;
  324. this.initTransfer();
  325. }
  326. });
  327. editor.on('Init', (e) => {
  328. this.spinShow = false;
  329. this.editor = editor;
  330. this.editor.setContent(this.content);
  331. if (this.readonly) {
  332. this.editor.setMode('readonly');
  333. } else {
  334. this.editor.setMode('design');
  335. }
  336. this.$emit('editorInit', this.editor);
  337. });
  338. editor.on('KeyUp', (e) => {
  339. if (this.editor !== null) {
  340. this.submitNewContent();
  341. }
  342. });
  343. editor.on('Change', (e) => {
  344. if (this.editor !== null) {
  345. if (this.getContent() !== this.value) {
  346. this.submitNewContent();
  347. }
  348. this.$emit('editorChange', e);
  349. }
  350. });
  351. }
  352. },
  353. };
  354. },
  355. closeFull() {
  356. this.content = this.getContent();
  357. this.$emit('input', this.content);
  358. this.transfer = false;
  359. if (this.editorT != null) {
  360. this.editorT.destroy();
  361. this.editorT = null;
  362. }
  363. },
  364. transferChange(visible) {
  365. if (!visible && this.editorT != null) {
  366. this.content = this.editorT.getContent();
  367. this.$emit('input', this.content);
  368. this.editorT.destroy();
  369. this.editorT = null;
  370. }
  371. },
  372. getEditor() {
  373. return this.transfer ? this.editorT : this.editor;
  374. },
  375. concatAssciativeArrays(array1, array2) {
  376. if (array2.length === 0) return array1;
  377. if (array1.length === 0) return array2;
  378. let dest = [];
  379. for (let key in array1) {
  380. if (array1.hasOwnProperty(key)) {
  381. dest[key] = array1[key];
  382. }
  383. }
  384. for (let key in array2) {
  385. if (array2.hasOwnProperty(key)) {
  386. dest[key] = array2[key];
  387. }
  388. }
  389. return dest;
  390. },
  391. submitNewContent() {
  392. this.isTyping = true;
  393. if (this.checkerTimeout !== null) {
  394. clearTimeout(this.checkerTimeout);
  395. }
  396. this.checkerTimeout = setTimeout(() => {
  397. this.isTyping = false;
  398. }, 300);
  399. this.$emit('input', this.getContent());
  400. },
  401. insertContent(content) {
  402. if (this.getEditor() !== null) {
  403. this.getEditor().insertContent(content);
  404. }else{
  405. this.content+= content;
  406. }
  407. },
  408. getContent() {
  409. if (this.getEditor() === null) {
  410. return "";
  411. }
  412. return this.getEditor().getContent();
  413. },
  414. insertImage(src) {
  415. this.insertContent('<img src="' + src + '">');
  416. },
  417. editorImage(lists) {
  418. for (let i = 0; i < lists.length; i++) {
  419. let item = lists[i];
  420. if (typeof item === 'object' && typeof item.url === "string") {
  421. this.insertImage(item.url);
  422. }
  423. }
  424. },
  425. }
  426. }
  427. </script>