You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

603 lines
22 KiB

  1. <template lang="pug">
  2. transition(:duration="400")
  3. .modal(v-show='isShown', v-cloak)
  4. transition(name='modal-background')
  5. .modal-background(v-show='isShown')
  6. .modal-container
  7. transition(name='modal-content')
  8. .modal-content.is-expanded(v-show='isShown')
  9. header.is-green
  10. span {{ (mode === 'file') ? $t('editor.filetitle') : $t('editor.imagetitle') }}
  11. p.modal-notify(:class='{ "is-active": isLoading }')
  12. span {{ isLoadingText }}
  13. i
  14. .modal-toolbar.is-green
  15. a.button(@click='newFolder')
  16. i.nc-icon-outline.files_folder-14
  17. span {{ $t('editor.newfolder') }}
  18. a.button#btn-editor-file-upload
  19. i.nc-icon-outline.arrows-1_cloud-upload-94
  20. span {{ (mode === 'file') ? $t('editor.fileupload') : $t('editor.imageupload') }}
  21. label
  22. input(type='file', multiple, :disabled='isLoading', ref='editorFileUploadInput')
  23. a.button(v-if='mode === "image"', @click='fetchFromUrl')
  24. i.nc-icon-outline.arrows-1_cloud-download-93
  25. span Fetch from URL
  26. section.is-gapless
  27. .columns.is-stretched
  28. .column.is-one-quarter.modal-sidebar.is-green(style={'max-width':'350px'})
  29. .model-sidebar-header {{ $t('editor.folders') }}
  30. ul.model-sidebar-list
  31. li(v-for='fld in folders')
  32. a(@click='selectFolder(fld)', :class='{ "is-active": currentFolder === fld }')
  33. i.nc-icon-outline.files_folder-17
  34. span / {{ fld }}
  35. .model-sidebar-header(v-if='mode === "image"') Alignment
  36. .model-sidebar-content(v-if='mode === "image"')
  37. p.control.is-fullwidth
  38. select(v-model='currentAlign')
  39. option(value='left') {{ $t('editor.imagealignleft') }}
  40. option(value='center') {{ $t('editor.imagealigncenter') }}
  41. option(value='right') {{ $t('editor.imagealignright') }}
  42. option(value='logo') {{ $t('editor.imagealignlogo') }}
  43. .column.editor-modal-choices.editor-modal-file-choices(v-if='mode === "file"')
  44. figure(v-for='fl in files', :class='{ "is-active": currentFile === fl._id }', @click='selectFile(fl._id)', :data-uid='fl._id')
  45. i(class='icon-file')
  46. span: strong {{ fl.filename }}
  47. span {{ fl.mime }}
  48. span {{ filesize(fl.filesize) }}
  49. em(v-show='files.length < 1')
  50. i.icon-marquee-minus
  51. | {{ $t('editor.filefolderempty') }}
  52. .column.editor-modal-choices.editor-modal-image-choices(v-if='mode === "image"')
  53. figure(v-for='img in files', v-bind:class='{ "is-active": currentFile === img._id }', v-on:click='selectFile(img._id)', v-bind:data-uid='img._id')
  54. img(v-bind:src='"/uploads/t/" + img._id + ".png"')
  55. span: strong {{ img.basename }}
  56. span {{ filesize(img.filesize) }}
  57. em(v-show='files.length < 1')
  58. i.icon-marquee-minus
  59. | {{ $t('editor.filefolderempty') }}
  60. footer
  61. a.button.is-grey.is-outlined(@click='cancel') {{ $t('editor.discard') }}
  62. a.button.is-green(@click='insertFileLink') {{ (mode === 'file') ? $t('editor.fileinsert') : $t('editor.imageinsert') }}
  63. transition(:duration="400")
  64. .modal.is-superimposed(v-show='newFolderShow')
  65. transition(name='modal-background')
  66. .modal-background(v-show='newFolderShow')
  67. .modal-container
  68. transition(name='modal-content')
  69. .modal-content(v-show='newFolderShow')
  70. header.is-light-blue {{ $t('modal.newfoldertitle') }}
  71. section
  72. label.label {{ $t('modal.newfoldername') }}
  73. p.control.is-fullwidth
  74. input.input(type='text', :placeholder='$t("modal.newfoldernameplaceholder")', v-model='newFolderName', ref='editorFileNewFolderInput', @keyup.enter='newFolderCreate', @keyup.esc='newFolderDiscard')
  75. span.help.is-danger(v-show='newFolderError') {{ $t('modal.newfolderinvalid') }}
  76. footer
  77. a.button.is-grey.is-outlined(@click='newFolderDiscard') {{ $t('modal.discard') }}
  78. a.button.is-light-blue(@click='newFolderCreate') {{ $t('modal.create') }}
  79. transition(:duration="400")
  80. .modal.is-superimposed(v-show='fetchFromUrlShow')
  81. transition(name='modal-background')
  82. .modal-background(v-show='fetchFromUrlShow')
  83. .modal-container
  84. transition(name='modal-content')
  85. .modal-content(v-show='fetchFromUrlShow')
  86. header.is-light-blue Fetch Image from URL
  87. section
  88. label.label Enter full URL path to the image:
  89. p.control.is-fullwidth
  90. input.input(type='text', placeholder='http://www.example.com/some-image.png', v-model='fetchFromUrlURL', ref='editorFileFetchInput', @keyup.enter='fetchFromUrlGo', @keyup.esc='fetchFromUrlDiscard')
  91. span.help.is-danger.is-hidden This URL path is invalid!
  92. footer
  93. a.button.is-grey.is-outlined(v-on:click='fetchFromUrlDiscard') Discard
  94. a.button.is-light-blue(v-on:click='fetchFromUrlGo') Fetch
  95. transition(:duration="400")
  96. .modal.is-superimposed(v-show='renameFileShow')
  97. transition(name='modal-background')
  98. .modal-background(v-show='renameFileShow')
  99. .modal-container
  100. transition(name='modal-content')
  101. .modal-content(v-show='renameFileShow')
  102. header.is-indigo {{ $t('modal.renamefiletitle') }}
  103. section
  104. label.label {{ $t('modal.renamefilename') }}
  105. p.control.is-fullwidth
  106. input.input#txt-editor-file-rename(type='text', :placeholder='$t("modal.renamefilenameplaceholder")', v-model='renameFileFilename', ref='editorFileRenameInput', @keyup.enter='renameFileGo', @keyup.esc='renameFileDiscard')
  107. span.help.is-danger.is-hidden {{ $t('modal.renamefileinvalid') }}
  108. footer
  109. a.button.is-grey.is-outlined(@click='renameFileDiscard') {{ $t('modal.discard') }}
  110. a.button.is-light-blue(@click='renameFileGo') {{ $t('modal.renamefile') }}
  111. transition(:duration="400")
  112. .modal.is-superimposed(v-show='deleteFileShow')
  113. transition(name='modal-background')
  114. .modal-background(v-show='deleteFileShow')
  115. .modal-container
  116. transition(name='modal-content')
  117. .modal-content(v-show='deleteFileShow')
  118. header.is-red {{ $t('modal.deletefiletitle') }}
  119. section
  120. span {{ $t('modal.deletefilewarn') }} #[strong {{deleteFileFilename}}]?
  121. footer
  122. a.button.is-grey.is-outlined(@click='deleteFileWarn(false)') {{ $t('modal.discard') }}
  123. a.button.is-red(@click='deleteFileGo') {{ $t('modal.delete') }}
  124. </template>
  125. <script>
  126. export default {
  127. name: 'editor-file',
  128. data () {
  129. return {
  130. isLoading: false,
  131. isLoadingText: '',
  132. newFolderName: '',
  133. newFolderShow: false,
  134. newFolderError: false,
  135. fetchFromUrlURL: '',
  136. fetchFromUrlShow: false,
  137. folders: [],
  138. currentFolder: '',
  139. currentFile: '',
  140. currentAlign: 'left',
  141. files: [],
  142. uploadSucceeded: false,
  143. postUploadChecks: 0,
  144. renameFileShow: false,
  145. renameFileId: '',
  146. renameFileFilename: '',
  147. deleteFileShow: false,
  148. deleteFileId: '',
  149. deleteFileFilename: ''
  150. }
  151. },
  152. computed: {
  153. isShown () {
  154. return this.$store.state.editorFile.shown
  155. },
  156. mode () {
  157. return this.$store.state.editorFile.mode
  158. }
  159. },
  160. methods: {
  161. init () {
  162. $(this.$refs.editorFileUploadInput).on('change', this.upload)
  163. this.refreshFolders()
  164. },
  165. cancel () {
  166. $(this.$refs.editorFileUploadInput).off('change', this.upload)
  167. this.$store.dispatch('editorFile/close')
  168. },
  169. filesize (rawSize) {
  170. return this.$helpers.common.filesize(rawSize)
  171. },
  172. // -------------------------------------------
  173. // INSERT LINK TO FILE
  174. // -------------------------------------------
  175. selectFile(fileId) {
  176. this.currentFile = fileId
  177. },
  178. insertFileLink() {
  179. let selFile = this._.find(this.files, ['_id', this.currentFile])
  180. selFile.normalizedPath = (selFile.folder === 'f:') ? selFile.filename : selFile.folder.slice(2) + '/' + selFile.filename
  181. selFile.titleGuess = this._.startCase(selFile.basename)
  182. let textToInsert = ''
  183. if (this.mode === 'image') {
  184. textToInsert = '![' + selFile.titleGuess + '](/uploads/' + selFile.normalizedPath + ' "' + selFile.titleGuess + '")'
  185. switch (this.currentAlign) {
  186. case 'center':
  187. textToInsert += '{.align-center}'
  188. break
  189. case 'right':
  190. textToInsert += '{.align-right}'
  191. break
  192. case 'logo':
  193. textToInsert += '{.pagelogo}'
  194. break
  195. }
  196. } else {
  197. textToInsert = '[' + selFile.titleGuess + '](/uploads/' + selFile.normalizedPath + ' "' + selFile.titleGuess + '")'
  198. }
  199. this.$store.dispatch('editor/insert', textToInsert)
  200. this.$store.dispatch('alert', {
  201. style: 'blue',
  202. icon: 'ui-1_check-square-09',
  203. msg: (this.mode === 'file') ? this.$t('editor.filesuccess') : this.$t('editor.imagesuccess')
  204. })
  205. this.cancel()
  206. },
  207. // -------------------------------------------
  208. // NEW FOLDER
  209. // -------------------------------------------
  210. newFolder() {
  211. let self = this
  212. this.newFolderName = ''
  213. this.newFolderError = false
  214. this.newFolderShow = true
  215. this._.delay(() => { self.$refs.editorFileNewFolderInput.focus() }, 400)
  216. },
  217. newFolderDiscard() {
  218. this.newFolderShow = false
  219. },
  220. newFolderCreate() {
  221. let self = this
  222. let regFolderName = new RegExp('^[a-z0-9][a-z0-9-]*[a-z0-9]$')
  223. this.newFolderName = this._.kebabCase(this._.trim(this.newFolderName))
  224. if (this._.isEmpty(this.newFolderName) || !regFolderName.test(this.newFolderName)) {
  225. this.newFolderError = true
  226. return
  227. }
  228. this.newFolderDiscard()
  229. this.isLoadingText = this.$t('modal.newfolderloading')
  230. this.isLoading = true
  231. this.$nextTick(() => {
  232. socket.emit('uploadsCreateFolder', { foldername: self.newFolderName }, (data) => {
  233. self.folders = data
  234. self.currentFolder = self.newFolderName
  235. self.files = []
  236. self.isLoading = false
  237. self.$store.dispatch('alert', {
  238. style: 'blue',
  239. icon: 'files_folder-check',
  240. msg: self.$t('modal.newfoldersuccess', { name: self.newFolderName })
  241. })
  242. })
  243. })
  244. },
  245. // -------------------------------------------
  246. // FETCH FROM URL
  247. // -------------------------------------------
  248. fetchFromUrl() {
  249. let self = this
  250. this.fetchFromUrlURL = ''
  251. this.fetchFromUrlShow = true
  252. this._.delay(() => { self.$refs.editorFileFetchInput.focus() }, 400)
  253. },
  254. fetchFromUrlDiscard() {
  255. this.fetchFromUrlShow = false
  256. },
  257. fetchFromUrlGo() {
  258. let self = this
  259. this.fetchFromUrlDiscard()
  260. this.isLoadingText = 'Fetching image...'
  261. this.isLoading = true
  262. this.$nextTick(() => {
  263. socket.emit('uploadsFetchFileFromURL', { folder: self.currentFolder, fetchUrl: self.fetchFromUrlURL }, (data) => {
  264. if (data.ok) {
  265. self.waitChangeComplete(self.files.length, true)
  266. } else {
  267. self.isLoading = false
  268. self.$store.dispatch('alert', {
  269. style: 'red',
  270. icon: 'ui-2_square-remove-09',
  271. msg: self.$t('editor.fileuploaderror', { err: data.msg })
  272. })
  273. }
  274. })
  275. })
  276. },
  277. // -------------------------------------------
  278. // RENAME FILE
  279. // -------------------------------------------
  280. renameFile() {
  281. let self = this
  282. let c = this._.find(this.files, [ '_id', this.renameFileId ])
  283. this.renameFileFilename = c.basename || ''
  284. this.renameFileShow = true
  285. this._.delay(() => {
  286. self.$refs.editorFileRenameInput.select()
  287. }, 100)
  288. },
  289. renameFileDiscard() {
  290. this.renameFileShow = false
  291. },
  292. renameFileGo() {
  293. let self = this
  294. this.renameFileDiscard()
  295. this.isLoadingText = this.$t('modal.renamefileloading')
  296. this.isLoading = true
  297. this.$nextTick(() => {
  298. socket.emit('uploadsRenameFile', { uid: self.renameFileId, folder: self.currentFolder, filename: self.renameFileFilename }, (data) => {
  299. if (data.ok) {
  300. self.waitChangeComplete(self.files.length, false)
  301. } else {
  302. self.isLoading = false
  303. self.$store.dispatch('alert', {
  304. style: 'red',
  305. icon: 'ui-2_square-remove-09',
  306. msg: self.$t('modal.renamefileerror', { err: data.msg })
  307. })
  308. }
  309. })
  310. })
  311. },
  312. // -------------------------------------------
  313. // MOVE FILE
  314. // -------------------------------------------
  315. moveFile(uid, fld) {
  316. let self = this
  317. this.isLoadingText = this.$t('editor.filemoveloading')
  318. this.isLoading = true
  319. this.$nextTick(() => {
  320. socket.emit('uploadsMoveFile', { uid, folder: fld }, (data) => {
  321. if (data.ok) {
  322. self.loadFiles()
  323. self.$store.dispatch('alert', {
  324. style: 'blue',
  325. icon: 'files_check',
  326. msg: self.$t('editor.filemovesuccess')
  327. })
  328. } else {
  329. self.isLoading = false
  330. self.$store.dispatch('alert', {
  331. style: 'red',
  332. icon: 'ui-2_square-remove-09',
  333. msg: self.$t('editor.filemoveerror', { err: data.msg })
  334. })
  335. }
  336. })
  337. })
  338. },
  339. // -------------------------------------------
  340. // DELETE FILE
  341. // -------------------------------------------
  342. deleteFileWarn(show) {
  343. if (show) {
  344. let c = this._.find(this.files, [ '_id', this.deleteFileId ])
  345. this.deleteFileFilename = c.filename || this.$t('editor.filedeletedefault')
  346. }
  347. this.deleteFileShow = show
  348. },
  349. deleteFileGo() {
  350. let self = this
  351. this.deleteFileWarn(false)
  352. this.isLoadingText = this.$t('editor.filedeleteloading')
  353. this.isLoading = true
  354. this.$nextTick(() => {
  355. socket.emit('uploadsDeleteFile', { uid: this.deleteFileId }, (data) => {
  356. self.loadFiles()
  357. self.$store.dispatch('alert', {
  358. style: 'blue',
  359. icon: 'ui-1_trash',
  360. msg: self.$t('editor.filedeletesuccess')
  361. })
  362. })
  363. })
  364. },
  365. // -------------------------------------------
  366. // LOAD FROM REMOTE
  367. // -------------------------------------------
  368. selectFolder(fldName) {
  369. this.currentFolder = fldName
  370. this.loadFiles()
  371. },
  372. refreshFolders() {
  373. let self = this
  374. this.isLoadingText = this.$t('editor.foldersloading')
  375. this.isLoading = true
  376. this.currentFolder = ''
  377. this.currentImage = ''
  378. this.$nextTick(() => {
  379. socket.emit('uploadsGetFolders', { }, (data) => {
  380. self.folders = data
  381. self.loadFiles()
  382. })
  383. })
  384. },
  385. loadFiles(silent) {
  386. let self = this
  387. if (!silent) {
  388. this.isLoadingText = this.$t('editor.fileloading')
  389. this.isLoading = true
  390. }
  391. return new Promise((resolve, reject) => {
  392. self.$nextTick(() => {
  393. let loadAction = (self.mode === 'image') ? 'uploadsGetImages' : 'uploadsGetFiles'
  394. socket.emit(loadAction, { folder: self.currentFolder }, (data) => {
  395. self.files = data
  396. if (!silent) {
  397. self.isLoading = false
  398. }
  399. self.attachContextMenus()
  400. resolve(true)
  401. })
  402. })
  403. })
  404. },
  405. waitChangeComplete(oldAmount, expectChange) {
  406. let self = this
  407. expectChange = (this._.isBoolean(expectChange)) ? expectChange : true
  408. this.postUploadChecks++
  409. this.isLoadingText = this.$t('editor.fileprocessing')
  410. this.$nextTick(() => {
  411. self.loadFiles(true).then(() => {
  412. if ((self.files.length !== oldAmount) === expectChange) {
  413. self.postUploadChecks = 0
  414. self.isLoading = false
  415. } else if (self.postUploadChecks > 5) {
  416. self.postUploadChecks = 0
  417. self.isLoading = false
  418. self.$store.dispatch('alert', {
  419. style: 'red',
  420. icon: 'ui-2_square-remove-09',
  421. msg: self.$t('editor.fileerror')
  422. })
  423. } else {
  424. self._.delay(() => {
  425. self.waitChangeComplete(oldAmount, expectChange)
  426. }, 1500)
  427. }
  428. })
  429. })
  430. },
  431. // -------------------------------------------
  432. // IMAGE CONTEXT MENU
  433. // -------------------------------------------
  434. attachContextMenus() {
  435. let self = this
  436. let moveFolders = this._.map(this.folders, (f) => {
  437. return {
  438. name: (f !== '') ? f : '/ (root)',
  439. icon: 'nc-icon-outline files_folder-15',
  440. callback: (key, opt) => {
  441. let moveFileId = self._.toString($(opt.$trigger).data('uid'))
  442. let moveFileDestFolder = self._.nth(self.folders, key)
  443. self.moveFile(moveFileId, moveFileDestFolder)
  444. }
  445. }
  446. })
  447. $.contextMenu('destroy', '.editor-modal-choices > figure')
  448. $.contextMenu({
  449. selector: '.editor-modal-choices > figure',
  450. appendTo: '.editor-modal-choices',
  451. position: (opt, x, y) => {
  452. $(opt.$trigger).addClass('is-contextopen')
  453. let trigPos = $(opt.$trigger).position()
  454. let trigDim = { w: $(opt.$trigger).width() / 5, h: $(opt.$trigger).height() / 2 }
  455. opt.$menu.css({ top: trigPos.top + trigDim.h, left: trigPos.left + trigDim.w })
  456. },
  457. events: {
  458. hide: (opt) => {
  459. $(opt.$trigger).removeClass('is-contextopen')
  460. }
  461. },
  462. items: {
  463. rename: {
  464. name: self.$t('editor.filerenameaction'),
  465. icon: 'nc-icon-outline files_vector',
  466. callback: (key, opt) => {
  467. self.renameFileId = self._.toString(opt.$trigger[0].dataset.uid)
  468. self.renameFile()
  469. }
  470. },
  471. move: {
  472. name: self.$t('editor.filemoveaction'),
  473. icon: 'fa-folder-open-o',
  474. items: moveFolders
  475. },
  476. delete: {
  477. name: self.$t('editor.filedeleteaction'),
  478. icon: 'icon-trash2',
  479. callback: (key, opt) => {
  480. self.deleteFileId = self._.toString(opt.$trigger[0].dataset.uid)
  481. self.deleteFileWarn(true)
  482. }
  483. }
  484. }
  485. })
  486. },
  487. upload() {
  488. let self = this
  489. let curFileAmount = this.files.length
  490. let uplUrl = (self.mode === 'image') ? '/uploads/img' : '/uploads/file'
  491. $(this.$refs.editorFileUploadInput).simpleUpload(uplUrl, {
  492. name: (self.mode === 'image') ? 'imgfile' : 'binfile',
  493. data: {
  494. folder: self.currentFolder
  495. },
  496. limit: 20,
  497. expect: 'json',
  498. allowedExts: (self.mode === 'image') ? ['jpg', 'jpeg', 'gif', 'png', 'webp'] : undefined,
  499. allowedTypes: (self.mode === 'image') ? ['image/png', 'image/jpeg', 'image/gif', 'image/webp'] : undefined,
  500. maxFileSize: (self.mode === 'image') ? 3145728 : 0, // max 3 MB
  501. init: (totalUploads) => {
  502. self.uploadSucceeded = false
  503. self.isLoadingText = 'Preparing to upload...'
  504. self.isLoading = true
  505. },
  506. progress: (progress) => {
  507. self.isLoadingText = 'Uploading...' + Math.round(progress) + '%'
  508. },
  509. success: (data) => {
  510. if (data.ok) {
  511. let failedUpls = self._.filter(data.results, ['ok', false])
  512. if (failedUpls.length) {
  513. self._.forEach(failedUpls, (u) => {
  514. self.$store.dispatch('alert', {
  515. style: 'red',
  516. icon: 'ui-2_square-remove-09',
  517. msg: self.$t('editor.fileuploaderror', { err: u.msg })
  518. })
  519. })
  520. if (failedUpls.length < data.results.length) {
  521. self.uploadSucceeded = true
  522. }
  523. } else {
  524. self.uploadSucceeded = true
  525. self.$store.dispatch('alert', {
  526. style: 'blue',
  527. icon: 'arrows-1_cloud-upload-96',
  528. msg: self.$t('editor.fileuploadsuccess')
  529. })
  530. }
  531. } else {
  532. self.$store.dispatch('alert', {
  533. style: 'red',
  534. icon: 'ui-2_square-remove-09',
  535. msg: self.$t('editor.fileuploaderror', { err: data.msg })
  536. })
  537. }
  538. },
  539. error: (error) => {
  540. self.$store.dispatch('alert', {
  541. style: 'red',
  542. icon: 'ui-2_square-remove-09',
  543. msg: self.$t('editor.fileuploaderror', { err: error.message })
  544. })
  545. },
  546. finish: () => {
  547. if (self.uploadSucceeded) {
  548. self.waitChangeComplete(curFileAmount, true)
  549. } else {
  550. self.isLoading = false
  551. }
  552. }
  553. })
  554. }
  555. },
  556. mounted() {
  557. this.$root.$on('editorFile/init', this.init)
  558. }
  559. }
  560. </script>