diff --git a/client/components/editor/editor-markdown.vue b/client/components/editor/editor-markdown.vue index ded4833e..9df31611 100644 --- a/client/components/editor/editor-markdown.vue +++ b/client/components/editor/editor-markdown.vue @@ -396,6 +396,13 @@ export default { } return lvl }, + /** + * Insert content at cursor + */ + insertAtCursor({ content }) { + const cursor = this.cm.doc.getCursor('head') + this.cm.doc.replaceRange(content, cursor) + }, /** * Insert content after current line */ @@ -457,6 +464,25 @@ export default { toggleFullscreen () { this.cm.setOption('fullScreen', true) } + }, + mounted() { + this.$root.$on('editorInsert', opts => { + switch (opts.kind) { + case 'IMAGE': + this.insertAtCursor({ + content: `![${opts.text}](${opts.path})` + }) + break + case 'BINARY': + this.insertAtCursor({ + content: `[${opts.text}](${opts.path})` + }) + break + } + }) + }, + beforeDestroy() { + this.$root.$off('editorInsert') } } diff --git a/client/components/editor/editor-modal-blocks.vue b/client/components/editor/editor-modal-blocks.vue index 82c9eb6f..42ca4e0f 100644 --- a/client/components/editor/editor-modal-blocks.vue +++ b/client/components/editor/editor-modal-blocks.vue @@ -2,7 +2,7 @@ v-card.editor-modal-blocks.animated.fadeInLeft(flat, tile) v-container.pa-3(grid-list-lg, fluid) v-layout(row, wrap) - v-flex(xs3) + v-flex(xs12, lg4, xl3) v-card.radius-7(light) v-card-text .d-flex @@ -82,5 +82,10 @@ export default { width: calc(100vw - 64px - 17px); height: calc(100vh - 112px - 24px); background-color: rgba(darken(mc('grey', '900'), 3%), .9) !important; + + @include until($tablet) { + left: 40px; + width: calc(100vw - 40px); + } } diff --git a/client/components/editor/editor-modal-media.vue b/client/components/editor/editor-modal-media.vue index 209812b3..d5f7dce3 100644 --- a/client/components/editor/editor-modal-media.vue +++ b/client/components/editor/editor-modal-media.vue @@ -11,9 +11,30 @@ v-btn.ml-3.my-0.radius-7(outline, large, color='teal', disabled, :icon='$vuetify.breakpoint.xsOnly') v-icon(:left='$vuetify.breakpoint.mdAndUp') keyboard_arrow_up span.hidden-sm-and-down Parent Folder - v-btn.my-0.mr-0.radius-7(outline, large, color='teal', :icon='$vuetify.breakpoint.xsOnly') - v-icon(:left='$vuetify.breakpoint.mdAndUp') add - span.hidden-sm-and-down New Folder + v-dialog(v-model='newFolderDialog', max-width='550') + v-btn.my-0.mr-0.radius-7(outline, large, color='teal', :icon='$vuetify.breakpoint.xsOnly', slot='activator') + v-icon(:left='$vuetify.breakpoint.mdAndUp') add + span.hidden-sm-and-down New Folder + v-card.wiki-form + .dialog-header.is-short New Folder + v-card-text + v-text-field.md2( + outline + background-color='grey lighten-3' + prepend-icon='folder' + v-model='newFolderName' + label='Folder Name' + counter='255' + @keyup.enter='createFolder' + @keyup.esc='newFolderDialog = false' + ref='folderNameIpt' + hint='Lowercase. No spaces allowed.' + persistent-hint + ) + v-card-chin + v-spacer + v-btn(flat, @click='newFolderDialog = false') Cancel + v-btn(color='primary', @click='createFolder', :disabled='!isFolderNameValid') Create v-data-table( :items='assets' :headers='headers' @@ -26,40 +47,40 @@ template(slot='items', slot-scope='props') tr.is-clickable( @click.left='currentFileId = props.item.id' - @click.right='' + @click.right.prevent='' :class='currentFileId === props.item.id ? `teal lighten-5` : ``' ) td.text-xs-right(v-if='$vuetify.breakpoint.smAndUp') {{ props.item.id }} td .body-2(:class='currentFileId === props.item.id ? `teal--text` : ``') {{ props.item.filename }} - .caption {{ props.item.description }} + .caption.grey--text {{ props.item.description }} td.text-xs-center(v-if='$vuetify.breakpoint.lgAndUp') - v-chip(small, :color='$vuetify.dark ? `grey darken-4` : `grey lighten-4`') + v-chip.ma-0(small, :color='$vuetify.dark ? `grey darken-4` : `grey lighten-4`') .caption {{props.item.ext.toUpperCase().substring(1)}} td(v-if='$vuetify.breakpoint.mdAndUp') {{ props.item.fileSize | prettyBytes }} - td(v-if='$vuetify.breakpoint.mdAndUp') {{ props.item.updatedAt | moment('from') }} + td(v-if='$vuetify.breakpoint.mdAndUp') {{ props.item.createdAt | moment('from') }} td(v-if='$vuetify.breakpoint.smAndUp') v-menu(offset-x) - v-btn(icon, slot='activator') + v-btn.ma-0(icon, slot='activator') v-icon(color='grey darken-2') more_horiz v-list.py-0 - v-list-tile + v-list-tile(@click='') v-list-tile-avatar v-icon(color='teal') short_text v-list-tile-content Properties v-divider template(v-if='props.item.kind === `IMAGE`') - v-list-tile + v-list-tile(@click='') v-list-tile-avatar v-icon(color='indigo') crop_rotate v-list-tile-content Edit v-divider - v-list-tile + v-list-tile(@click='') v-list-tile-avatar v-icon(color='blue') keyboard v-list-tile-content Rename / Move v-divider - v-list-tile + v-list-tile(@click='') v-list-tile-avatar v-icon(color='red') delete v-list-tile-content Delete @@ -147,6 +168,8 @@ import 'filepond/dist/filepond.min.css' import listAssetQuery from 'gql/editor/editor-media-query-list.gql' const FilePond = vueFilePond() +const localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i +const disallowedFolderChars = /[A-Z()=.!@#$%?&*+`~<>,;:\\/[\]¬{| ]/ export default { components: { @@ -172,7 +195,9 @@ export default { ], imageAlignment: '', currentFileId: null, - loading: false + loading: false, + newFolderDialog: false, + newFolderName: '' } }, computed: { @@ -191,12 +216,24 @@ export default { headers() { return _.compact([ this.$vuetify.breakpoint.smAndUp && { text: 'ID', value: 'id', width: 50, align: 'right' }, - { text: 'Title', value: 'title' }, - this.$vuetify.breakpoint.lgAndUp && { text: 'Type', value: 'path', width: 50 }, - this.$vuetify.breakpoint.mdAndUp && { text: 'File Size', value: 'createdAt', width: 150 }, - this.$vuetify.breakpoint.mdAndUp && { text: 'Last Updated', value: 'updatedAt', width: 150 }, - this.$vuetify.breakpoint.smAndUp && { text: '', value: '', width: 50, sortable: false } + { text: 'Filename', value: 'filename' }, + this.$vuetify.breakpoint.lgAndUp && { text: 'Type', value: 'ext', width: 50 }, + this.$vuetify.breakpoint.mdAndUp && { text: 'File Size', value: 'fileSize', width: 110 }, + this.$vuetify.breakpoint.mdAndUp && { text: 'Added', value: 'createdAt', width: 150 }, + this.$vuetify.breakpoint.smAndUp && { text: 'Actions', value: '', width: 40, sortable: false, align:'right' } ]) + }, + isFolderNameValid() { + return this.newFolderName.length > 1 && !localeSegmentRegex.test(this.newFolderName) && !disallowedFolderChars.test(this.newFolderName) + } + }, + watch: { + newFolderDialog(newValue, oldValue) { + if (newValue) { + this.$nextTick(() => { + this.$refs.folderNameIpt.focus() + }) + } } }, filters: { @@ -227,8 +264,13 @@ export default { }, methods: { insert () { + const asset = _.find(this.assets, ['id', this.currentFileId]) + this.$root.$emit('editorInsert', { + kind: asset.kind, + path: `/${asset.filename}`, + text: asset.filename + }) this.activeModal = '' - }, browse () { this.$refs.pond.browse() @@ -262,6 +304,9 @@ export default { }, 5000) await this.$apollo.queries.assets.refetch() + }, + async createFolder() { + } }, apollo: { diff --git a/client/scss/components/v-dialog.scss b/client/scss/components/v-dialog.scss index c7dc20f1..93f7a6fe 100644 --- a/client/scss/components/v-dialog.scss +++ b/client/scss/components/v-dialog.scss @@ -26,6 +26,12 @@ background-image: radial-gradient(ellipse at top, mc('grey', '800'), mc('grey', '900')), radial-gradient(ellipse at bottom, mc('grey', '800'), mc('grey', '900')); } + + &.is-teal { + background-color: mc('teal', '700'); + background-image: radial-gradient(ellipse at top, mc('teal', '500'), mc('teal', '700')), + radial-gradient(ellipse at bottom, mc('teal', '800'), mc('teal', '700')); + } } .v-dialog--fullscreen { diff --git a/client/themes/default/scss/app.scss b/client/themes/default/scss/app.scss index 2212ad48..b235f252 100644 --- a/client/themes/default/scss/app.scss +++ b/client/themes/default/scss/app.scss @@ -235,6 +235,92 @@ li + li { margin-top: .5rem; } + + &.links-list { + li { + background-color: mc('grey', '50'); + background-image: linear-gradient(to bottom, #FFF, mc('grey', '50')); + border-right: 1px solid mc('grey', '200'); + border-bottom: 1px solid mc('grey', '200'); + border-left: 5px solid mc('grey', '300'); + box-shadow: 0 3px 8px 0 rgba(116, 129, 141, 0.1); + padding: 1rem; + border-radius: 5px; + font-weight: 500; + + &:hover { + background-image: linear-gradient(to bottom, #FFF, lighten(mc('blue', '50'), 4%)); + border-left-color: mc('blue', '500'); + cursor: pointer; + } + + &::before { + content: ''; + display: none; + } + + > a { + display: block; + text-decoration: none; + margin: -1rem; + padding: 1rem; + } + + @at-root .theme--dark & { + background-color: mc('grey', '50'); + background-image: linear-gradient(to bottom, lighten(mc('grey', '900'), 5%), mc('grey', '900')); + border-right: 1px solid mc('grey', '900'); + border-bottom: 1px solid mc('grey', '900'); + border-left: 5px solid mc('grey', '700'); + box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.1); + + &:hover { + background-image: linear-gradient(to bottom, lighten(mc('grey', '900'), 2%), darken(mc('grey', '900'), 3%)); + border-left-color: mc('blue', '500'); + cursor: pointer; + } + } + } + } + + &.grid-list { + margin: 1rem 24px 0 24px; + background-color: #FFF; + border: 1px solid mc('grey', '200'); + padding: 1px; + display: inline-block; + + @at-root .theme--dark & { + background-color: #000; + border: 1px solid mc('grey', '800'); + } + + li { + background-color: mc('grey', '50'); + padding: .6rem 1rem; + display: block; + + &:nth-child(odd) { + background-color: mc('grey', '100'); + } + + & + li { + margin-top: 0; + } + + &::before { + color: mc('grey', '400'); + } + + @at-root .theme--dark & { + background-color: mc('grey', '900'); + + &:nth-child(odd) { + background-color: darken(mc('grey', '900'), 5%); + } + } + } + } } ul { @@ -264,6 +350,11 @@ &::before, &::after { display: none; } + + @at-root .theme--dark & { + background-color: darken(mc('grey', '900'), 5%); + color: mc('indigo', '100'); + } } .prismjs{ diff --git a/server/models/assets.js b/server/models/assets.js index 99297976..b4422365 100644 --- a/server/models/assets.js +++ b/server/models/assets.js @@ -77,6 +77,7 @@ module.exports = class Asset extends Model { kind: _.startsWith(opts.mimetype, 'image/') ? 'image' : 'binary', mime: opts.mimetype, fileSize: opts.size, + folderId: null, authorId: opts.userId })