TEditor.vue 16 KB

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