add nova
This commit is contained in:
334
nova/resources/js/composables/useActions.js
Normal file
334
nova/resources/js/composables/useActions.js
Normal file
@@ -0,0 +1,334 @@
|
||||
import { Errors } from '@/mixins'
|
||||
import { computed, nextTick, reactive } from 'vue'
|
||||
import each from 'lodash/each'
|
||||
import find from 'lodash/find'
|
||||
import filter from 'lodash/filter'
|
||||
import isNil from 'lodash/isNil'
|
||||
import isObject from 'lodash/isObject'
|
||||
import map from 'lodash/map'
|
||||
import tap from 'lodash/tap'
|
||||
import trim from 'lodash/trim'
|
||||
import { useLocalization } from '@/composables/useLocalization'
|
||||
|
||||
const { __ } = useLocalization()
|
||||
|
||||
export function useActions(props, emitter, store) {
|
||||
const state = reactive({
|
||||
working: false,
|
||||
errors: new Errors(),
|
||||
actionModalVisible: false,
|
||||
responseModalVisible: false,
|
||||
selectedActionKey: '',
|
||||
endpoint: props.endpoint || `/nova-api/${props.resourceName}/action`,
|
||||
actionResponseData: null,
|
||||
})
|
||||
|
||||
const selectedResources = computed(() => props.selectedResources)
|
||||
|
||||
const selectedAction = computed(() => {
|
||||
if (state.selectedActionKey) {
|
||||
return find(allActions.value, a => a.uriKey === state.selectedActionKey)
|
||||
}
|
||||
})
|
||||
|
||||
const allActions = computed(() =>
|
||||
props.actions.concat(props.pivotActions?.actions || [])
|
||||
)
|
||||
|
||||
const encodedFilters = computed(
|
||||
() => store.getters[`${props.resourceName}/currentEncodedFilters`]
|
||||
)
|
||||
|
||||
const searchParameter = computed(() =>
|
||||
props.viaRelationship
|
||||
? props.viaRelationship + '_search'
|
||||
: props.resourceName + '_search'
|
||||
)
|
||||
|
||||
const currentSearch = computed(
|
||||
() => store.getters.queryStringParams[searchParameter.value] || ''
|
||||
)
|
||||
|
||||
const trashedParameter = computed(() =>
|
||||
props.viaRelationship
|
||||
? props.viaRelationship + '_trashed'
|
||||
: props.resourceName + '_trashed'
|
||||
)
|
||||
|
||||
const currentTrashed = computed(
|
||||
() => store.getters.queryStringParams[trashedParameter.value] || ''
|
||||
)
|
||||
|
||||
const availableActions = computed(() => {
|
||||
return filter(
|
||||
props.actions,
|
||||
action => selectedResources.value.length > 0 && !action.standalone
|
||||
)
|
||||
})
|
||||
|
||||
const availablePivotActions = computed(() => {
|
||||
if (!props.pivotActions) {
|
||||
return []
|
||||
}
|
||||
|
||||
return filter(props.pivotActions.actions, action => {
|
||||
if (selectedResources.value.length === 0) {
|
||||
return action.standalone
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
const hasPivotActions = computed(() => availablePivotActions.value.length > 0)
|
||||
|
||||
const selectedActionIsPivotAction = computed(() => {
|
||||
return (
|
||||
hasPivotActions.value &&
|
||||
Boolean(find(props.pivotActions.actions, a => a === selectedAction.value))
|
||||
)
|
||||
})
|
||||
|
||||
const actionRequestQueryString = computed(() => {
|
||||
return {
|
||||
action: state.selectedActionKey,
|
||||
pivotAction: selectedActionIsPivotAction.value,
|
||||
search: currentSearch.value,
|
||||
filters: encodedFilters.value,
|
||||
trashed: currentTrashed.value,
|
||||
viaResource: props.viaResource,
|
||||
viaResourceId: props.viaResourceId,
|
||||
viaRelationship: props.viaRelationship,
|
||||
}
|
||||
})
|
||||
|
||||
const actionFormData = computed(() => {
|
||||
return tap(new FormData(), formData => {
|
||||
if (selectedResources.value === 'all') {
|
||||
formData.append('resources', 'all')
|
||||
} else {
|
||||
let pivotIds = filter(
|
||||
map(selectedResources.value, resource =>
|
||||
isObject(resource) ? resource.id.pivotValue : null
|
||||
)
|
||||
)
|
||||
|
||||
each(selectedResources.value, resource =>
|
||||
formData.append(
|
||||
'resources[]',
|
||||
isObject(resource) ? resource.id.value : resource
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
selectedResources.value !== 'all' &&
|
||||
selectedActionIsPivotAction.value === true &&
|
||||
pivotIds.length > 0
|
||||
) {
|
||||
each(pivotIds, pivotId => formData.append('pivots[]', pivotId))
|
||||
}
|
||||
}
|
||||
|
||||
each(selectedAction.value.fields, field => {
|
||||
field.fill(formData)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function determineActionStrategy() {
|
||||
if (selectedAction.value.withoutConfirmation) {
|
||||
executeAction()
|
||||
} else {
|
||||
openConfirmationModal()
|
||||
}
|
||||
}
|
||||
|
||||
function openConfirmationModal() {
|
||||
state.actionModalVisible = true
|
||||
}
|
||||
|
||||
function closeConfirmationModal() {
|
||||
state.actionModalVisible = false
|
||||
}
|
||||
|
||||
function openResponseModal() {
|
||||
state.responseModalVisible = true
|
||||
}
|
||||
|
||||
function closeResponseModal() {
|
||||
state.responseModalVisible = false
|
||||
}
|
||||
|
||||
function emitResponseCallback(callback) {
|
||||
emitter('actionExecuted')
|
||||
Nova.$emit('action-executed')
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
function showActionResponseMessage(data) {
|
||||
if (data.danger) {
|
||||
return Nova.error(data.danger)
|
||||
}
|
||||
|
||||
Nova.success(data.message || __('The action was executed successfully.'))
|
||||
}
|
||||
|
||||
function executeAction(then) {
|
||||
state.working = true
|
||||
Nova.$progress.start()
|
||||
|
||||
let responseType = selectedAction.value.responseType ?? 'json'
|
||||
|
||||
Nova.request({
|
||||
method: 'post',
|
||||
url: state.endpoint,
|
||||
params: actionRequestQueryString.value,
|
||||
data: actionFormData.value,
|
||||
responseType,
|
||||
})
|
||||
.then(async response => {
|
||||
closeConfirmationModal()
|
||||
handleActionResponse(response.data, response.headers, then)
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.response && error.response.status === 422) {
|
||||
if (responseType === 'blob') {
|
||||
error.response.data.text().then(data => {
|
||||
state.errors = new Errors(JSON.parse(data).errors)
|
||||
})
|
||||
} else {
|
||||
state.errors = new Errors(error.response.data.errors)
|
||||
}
|
||||
|
||||
Nova.error(__('There was a problem executing the action.'))
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
state.working = false
|
||||
Nova.$progress.done()
|
||||
})
|
||||
}
|
||||
|
||||
function handleActionResponse(data, headers, then) {
|
||||
let contentDisposition = headers['content-disposition']
|
||||
|
||||
if (
|
||||
data instanceof Blob &&
|
||||
isNil(contentDisposition) &&
|
||||
data.type === 'application/json'
|
||||
) {
|
||||
data.text().then(jsonStringData => {
|
||||
handleActionResponse(JSON.parse(jsonStringData), headers)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (data instanceof Blob) {
|
||||
return emitResponseCallback(async () => {
|
||||
let fileName = 'unknown'
|
||||
|
||||
if (contentDisposition) {
|
||||
let fileNameMatch = contentDisposition
|
||||
.split(';')[1]
|
||||
.match(/filename=(.+)/)
|
||||
if (fileNameMatch.length === 2) fileName = trim(fileNameMatch[1], '"')
|
||||
}
|
||||
|
||||
await nextTick(() => {
|
||||
let url = window.URL.createObjectURL(new Blob([data]))
|
||||
let link = document.createElement('a')
|
||||
|
||||
link.href = url
|
||||
link.setAttribute('download', fileName)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
|
||||
window.URL.revokeObjectURL(url)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (data.modal) {
|
||||
state.actionResponseData = data
|
||||
|
||||
showActionResponseMessage(data)
|
||||
|
||||
return openResponseModal()
|
||||
}
|
||||
|
||||
if (data.download) {
|
||||
return emitResponseCallback(async () => {
|
||||
showActionResponseMessage(data)
|
||||
|
||||
await nextTick(() => {
|
||||
let link = document.createElement('a')
|
||||
link.href = data.download
|
||||
link.download = data.name
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (data.deleted) {
|
||||
return emitResponseCallback(() => showActionResponseMessage(data))
|
||||
}
|
||||
|
||||
if (data.redirect) {
|
||||
window.location = data.redirect
|
||||
}
|
||||
|
||||
if (data.visit) {
|
||||
showActionResponseMessage(data)
|
||||
|
||||
return Nova.visit({
|
||||
url: Nova.url(data.visit.path, data.visit.options),
|
||||
remote: false,
|
||||
})
|
||||
}
|
||||
|
||||
if (data.openInNewTab) {
|
||||
return emitResponseCallback(() =>
|
||||
window.open(data.openInNewTab, '_blank')
|
||||
)
|
||||
}
|
||||
|
||||
emitResponseCallback(() => showActionResponseMessage(data))
|
||||
}
|
||||
|
||||
function handleActionClick(uriKey) {
|
||||
state.selectedActionKey = uriKey
|
||||
determineActionStrategy()
|
||||
}
|
||||
|
||||
function setSelectedActionKey(key) {
|
||||
state.selectedActionKey = key
|
||||
}
|
||||
|
||||
return {
|
||||
errors: computed(() => state.errors),
|
||||
working: computed(() => state.working),
|
||||
actionModalVisible: computed(() => state.actionModalVisible),
|
||||
responseModalVisible: computed(() => state.responseModalVisible),
|
||||
selectedActionKey: computed(() => state.selectedActionKey),
|
||||
determineActionStrategy,
|
||||
setSelectedActionKey,
|
||||
openConfirmationModal,
|
||||
closeConfirmationModal,
|
||||
openResponseModal,
|
||||
closeResponseModal,
|
||||
handleActionClick,
|
||||
selectedAction,
|
||||
allActions,
|
||||
availableActions,
|
||||
availablePivotActions,
|
||||
executeAction,
|
||||
actionResponseData: computed(() => state.actionResponseData),
|
||||
}
|
||||
}
|
||||
9
nova/resources/js/composables/useCloseOnEsc.js
Normal file
9
nova/resources/js/composables/useCloseOnEsc.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
export function useCloseOnEsc(callback) {
|
||||
return {
|
||||
closeOnEsc: useEventListener(document, 'keydown', event => {
|
||||
if (event.key === 'Escape') callback()
|
||||
}),
|
||||
}
|
||||
}
|
||||
22
nova/resources/js/composables/useDragAndDrop.js
Normal file
22
nova/resources/js/composables/useDragAndDrop.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
export function useDragAndDrop(emit) {
|
||||
const startedDrag = ref(false)
|
||||
const files = ref([])
|
||||
|
||||
const handleOnDragEnter = () => (startedDrag.value = true)
|
||||
|
||||
const handleOnDragLeave = () => (startedDrag.value = false)
|
||||
|
||||
const handleOnDrop = e => {
|
||||
files.value = e.dataTransfer.files
|
||||
emit('fileChanged', e.dataTransfer.files)
|
||||
}
|
||||
|
||||
return {
|
||||
startedDrag,
|
||||
handleOnDragEnter,
|
||||
handleOnDragLeave,
|
||||
handleOnDrop,
|
||||
}
|
||||
}
|
||||
28
nova/resources/js/composables/useFilePreviews.js
Normal file
28
nova/resources/js/composables/useFilePreviews.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
export function useFilePreviews(file) {
|
||||
const imageTypes = [
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/svg+xml',
|
||||
'image/webp',
|
||||
]
|
||||
|
||||
const type = computed(() =>
|
||||
imageTypes.includes(file.value.type) ? 'image' : 'other'
|
||||
)
|
||||
|
||||
const previewUrl = computed(() =>
|
||||
URL.createObjectURL(file.value.originalFile)
|
||||
)
|
||||
|
||||
const isImage = computed(() => type.value === 'image')
|
||||
|
||||
return {
|
||||
imageTypes,
|
||||
isImage,
|
||||
type,
|
||||
previewUrl,
|
||||
}
|
||||
}
|
||||
6
nova/resources/js/composables/useId.js
Normal file
6
nova/resources/js/composables/useId.js
Normal file
@@ -0,0 +1,6 @@
|
||||
let id = 0
|
||||
export function useId() {
|
||||
++id
|
||||
|
||||
return id
|
||||
}
|
||||
7
nova/resources/js/composables/useLocalization.js
Normal file
7
nova/resources/js/composables/useLocalization.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import __ from '../util/localization'
|
||||
|
||||
export function useLocalization() {
|
||||
return {
|
||||
__: (key, replace) => __(key, replace),
|
||||
}
|
||||
}
|
||||
392
nova/resources/js/composables/useMarkdownEditing.js
Normal file
392
nova/resources/js/composables/useMarkdownEditing.js
Normal file
@@ -0,0 +1,392 @@
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import CodeMirror from 'codemirror'
|
||||
import each from 'lodash/each'
|
||||
import isNil from 'lodash/isNil'
|
||||
import debounce from 'lodash/debounce'
|
||||
|
||||
import { useLocalization } from '@/composables/useLocalization'
|
||||
|
||||
const { __ } = useLocalization()
|
||||
|
||||
const defineMarkdownCommands = (
|
||||
editor,
|
||||
{ props, emit, isFocused, filesUploadingCount, filesUploadedCount, files }
|
||||
) => {
|
||||
const doc = editor.getDoc()
|
||||
|
||||
return {
|
||||
setValue(value) {
|
||||
doc.setValue(value)
|
||||
this.refresh()
|
||||
},
|
||||
|
||||
focus() {
|
||||
isFocused.value = true
|
||||
},
|
||||
|
||||
refresh() {
|
||||
nextTick(() => editor.refresh())
|
||||
},
|
||||
|
||||
insert(insertion) {
|
||||
let cursor = doc.getCursor()
|
||||
|
||||
doc.replaceRange(insertion, {
|
||||
line: cursor.line,
|
||||
ch: cursor.ch,
|
||||
})
|
||||
},
|
||||
|
||||
insertAround(start, end) {
|
||||
if (doc.somethingSelected()) {
|
||||
const selection = doc.getSelection()
|
||||
|
||||
doc.replaceSelection(start + selection + end)
|
||||
} else {
|
||||
let cursor = doc.getCursor()
|
||||
|
||||
doc.replaceRange(start + end, {
|
||||
line: cursor.line,
|
||||
ch: cursor.ch,
|
||||
})
|
||||
|
||||
doc.setCursor({
|
||||
line: cursor.line,
|
||||
ch: cursor.ch + start.length,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
insertBefore(insertion, cursorOffset) {
|
||||
if (doc.somethingSelected()) {
|
||||
const selects = doc.listSelections()
|
||||
selects.forEach(selection => {
|
||||
const pos = [selection.head.line, selection.anchor.line].sort()
|
||||
|
||||
for (let i = pos[0]; i <= pos[1]; i++) {
|
||||
doc.replaceRange(insertion, { line: i, ch: 0 })
|
||||
}
|
||||
|
||||
doc.setCursor({ line: pos[0], ch: cursorOffset || 0 })
|
||||
})
|
||||
} else {
|
||||
let cursor = doc.getCursor()
|
||||
|
||||
doc.replaceRange(insertion, {
|
||||
line: cursor.line,
|
||||
ch: 0,
|
||||
})
|
||||
doc.setCursor({
|
||||
line: cursor.line,
|
||||
ch: cursorOffset || 0,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
uploadAttachment(file) {
|
||||
if (!isNil(props.uploader)) {
|
||||
filesUploadingCount.value = filesUploadingCount.value + 1
|
||||
|
||||
const placeholder = `![Uploading ${file.name}…]()`
|
||||
|
||||
this.insert(placeholder)
|
||||
|
||||
props.uploader(file, {
|
||||
onCompleted: (path, url) => {
|
||||
let value = doc.getValue()
|
||||
value = value.replace(placeholder, ``)
|
||||
|
||||
doc.setValue(value)
|
||||
emit('change', value)
|
||||
|
||||
filesUploadedCount.value = filesUploadedCount.value + 1
|
||||
},
|
||||
onFailure: error => {
|
||||
filesUploadingCount.value = filesUploadingCount.value - 1
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const defineMarkdownActions = (commands, { isEditable, isFullScreen }) => {
|
||||
return {
|
||||
bold() {
|
||||
if (!isEditable) return
|
||||
|
||||
commands.insertAround('**', '**')
|
||||
},
|
||||
|
||||
italicize() {
|
||||
if (!isEditable) return
|
||||
|
||||
commands.insertAround('*', '*')
|
||||
},
|
||||
|
||||
image() {
|
||||
if (!isEditable) return
|
||||
|
||||
commands.insertBefore('', 2)
|
||||
},
|
||||
|
||||
link() {
|
||||
if (!isEditable) return
|
||||
|
||||
commands.insertAround('[', '](url)')
|
||||
},
|
||||
|
||||
toggleFullScreen() {
|
||||
isFullScreen.value = !isFullScreen.value
|
||||
|
||||
commands.refresh()
|
||||
},
|
||||
|
||||
fullScreen() {
|
||||
isFullScreen.value = true
|
||||
|
||||
commands.refresh()
|
||||
},
|
||||
|
||||
exitFullScreen() {
|
||||
isFullScreen.value = false
|
||||
|
||||
commands.refresh()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const defineMarkdownKeyMaps = (editor, actions) => {
|
||||
const keyMaps = {
|
||||
'Cmd-B': 'bold',
|
||||
'Cmd-I': 'italicize',
|
||||
'Cmd-Alt-I': 'image',
|
||||
'Cmd-K': 'link',
|
||||
F11: 'fullScreen',
|
||||
Esc: 'exitFullScreen',
|
||||
}
|
||||
|
||||
each(keyMaps, (action, map) => {
|
||||
const realMap = map.replace(
|
||||
'Cmd-',
|
||||
CodeMirror.keyMap['default'] == CodeMirror.keyMap.macDefault
|
||||
? 'Cmd-'
|
||||
: 'Ctrl-'
|
||||
)
|
||||
|
||||
editor.options.extraKeys[realMap] = actions[keyMaps[map]].bind(this)
|
||||
})
|
||||
}
|
||||
|
||||
const defineMarkdownEvents = (
|
||||
editor,
|
||||
commands,
|
||||
{ props, emit, isFocused, files, filesUploadingCount, filesUploadedCount }
|
||||
) => {
|
||||
const doc = editor.getDoc()
|
||||
|
||||
const handlePasteFromClipboard = e => {
|
||||
if (e.clipboardData && e.clipboardData.items) {
|
||||
const items = e.clipboardData.items
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].type.indexOf('image') !== -1) {
|
||||
commands.uploadAttachment(items[i].getAsFile())
|
||||
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const markdownFileRegex = /!\[[^\]]*\]\(([^\)]+)\)/gm
|
||||
|
||||
const getFileUrls = function (content) {
|
||||
return [...content.matchAll(markdownFileRegex)]
|
||||
.map(match => match[1])
|
||||
.filter(url => {
|
||||
try {
|
||||
new URL(url)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
editor.on('focus', () => (isFocused.value = true))
|
||||
editor.on('blur', () => (isFocused.value = false))
|
||||
|
||||
doc.on('change', (cm, changeObj) => {
|
||||
if (changeObj.origin === 'setValue') {
|
||||
return
|
||||
}
|
||||
|
||||
emit('change', cm.getValue())
|
||||
})
|
||||
|
||||
doc.on(
|
||||
'change',
|
||||
debounce((cm, changeObj) => {
|
||||
const newFiles = getFileUrls(cm.getValue())
|
||||
|
||||
files.value
|
||||
.filter(file => !newFiles.includes(file))
|
||||
.filter((url, index, array) => array.indexOf(url) === index)
|
||||
.forEach(file => emit('file-removed', file))
|
||||
newFiles
|
||||
.filter(url => !files.value.includes(url))
|
||||
.filter((url, index, array) => array.indexOf(url) === index)
|
||||
.forEach(file => emit('file-added', file))
|
||||
files.value = newFiles
|
||||
}, 1000)
|
||||
)
|
||||
|
||||
editor.on('paste', (cm, event) => {
|
||||
handlePasteFromClipboard(event)
|
||||
})
|
||||
|
||||
watch(isFocused, (currentValue, oldValue) => {
|
||||
if (currentValue === true && oldValue === false) {
|
||||
editor.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const bootstrap = (
|
||||
theTextarea,
|
||||
{
|
||||
emit,
|
||||
props,
|
||||
isEditable,
|
||||
isFocused,
|
||||
isFullScreen,
|
||||
filesUploadingCount,
|
||||
filesUploadedCount,
|
||||
files,
|
||||
unmountMarkdownEditor,
|
||||
}
|
||||
) => {
|
||||
const editor = CodeMirror.fromTextArea(theTextarea.value, {
|
||||
tabSize: 4,
|
||||
indentWithTabs: true,
|
||||
lineWrapping: true,
|
||||
mode: 'markdown',
|
||||
viewportMargin: Infinity,
|
||||
extraKeys: {
|
||||
Enter: 'newlineAndIndentContinueMarkdownList',
|
||||
},
|
||||
readOnly: props.readonly,
|
||||
})
|
||||
|
||||
const doc = editor.getDoc()
|
||||
|
||||
const commands = defineMarkdownCommands(editor, {
|
||||
props,
|
||||
emit,
|
||||
isFocused,
|
||||
filesUploadingCount,
|
||||
filesUploadedCount,
|
||||
files,
|
||||
})
|
||||
const actions = defineMarkdownActions(commands, { isEditable, isFullScreen })
|
||||
|
||||
defineMarkdownKeyMaps(editor, actions)
|
||||
|
||||
defineMarkdownEvents(editor, commands, {
|
||||
props,
|
||||
emit,
|
||||
isFocused,
|
||||
files,
|
||||
filesUploadingCount,
|
||||
filesUploadedCount,
|
||||
})
|
||||
|
||||
commands.refresh()
|
||||
|
||||
return {
|
||||
editor,
|
||||
unmount: () => {
|
||||
editor.toTextArea()
|
||||
unmountMarkdownEditor()
|
||||
},
|
||||
actions: {
|
||||
...commands,
|
||||
...actions,
|
||||
handle(context, action) {
|
||||
if (!props.readonly) {
|
||||
isFocused.value = true
|
||||
actions[action].call(context)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function useMarkdownEditing(emit, props) {
|
||||
const isFullScreen = ref(false)
|
||||
const isFocused = ref(false)
|
||||
const previewContent = ref('')
|
||||
const visualMode = ref('write')
|
||||
const statusContent = ref(
|
||||
__('Attach files by dragging & dropping, selecting or pasting them.')
|
||||
)
|
||||
const files = ref([])
|
||||
const filesUploadingCount = ref(0)
|
||||
const filesUploadedCount = ref(0)
|
||||
|
||||
const isEditable = computed(
|
||||
() => props.readonly && visualMode.value == 'write'
|
||||
)
|
||||
|
||||
const unmountMarkdownEditor = () => {
|
||||
isFullScreen.value = false
|
||||
isFocused.value = false
|
||||
visualMode.value = 'write'
|
||||
previewContent.value = ''
|
||||
filesUploadingCount.value = 0
|
||||
filesUploadedCount.value = 0
|
||||
files.value = []
|
||||
}
|
||||
|
||||
if (!isNil(props.uploader)) {
|
||||
watch(
|
||||
[filesUploadedCount, filesUploadingCount],
|
||||
([currentFilesUploaded, currentFilesCount]) => {
|
||||
if (currentFilesCount > currentFilesUploaded) {
|
||||
statusContent.value = __('Uploading files... (:current/:total)', {
|
||||
current: currentFilesUploaded,
|
||||
total: currentFilesCount,
|
||||
})
|
||||
} else {
|
||||
statusContent.value = __(
|
||||
'Attach files by dragging & dropping, selecting or pasting them.'
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
createMarkdownEditor: (context, theTextarea) => {
|
||||
return bootstrap.call(context, theTextarea, {
|
||||
emit,
|
||||
props,
|
||||
isEditable,
|
||||
isFocused,
|
||||
isFullScreen,
|
||||
filesUploadingCount,
|
||||
filesUploadedCount,
|
||||
files,
|
||||
unmountMarkdownEditor,
|
||||
})
|
||||
},
|
||||
isFullScreen,
|
||||
isFocused,
|
||||
isEditable,
|
||||
visualMode,
|
||||
previewContent,
|
||||
statusContent,
|
||||
files,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user