Merge branch 'master' into feat/book-series-info

This commit is contained in:
Greg Lorenzen 2025-01-30 17:21:44 -08:00
commit 56c62d4ea4
53 changed files with 846 additions and 314 deletions

View file

@ -19,6 +19,14 @@
</div>
<div v-else-if="!totalShelves && initialized" class="w-full py-16">
<p class="text-xl text-center">{{ emptyMessage }}</p>
<div v-if="entityName === 'collections' || entityName === 'playlists'" class="flex justify-center mt-4">
{{ emptyMessageHelp }}
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/collections" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
</div>
<!-- Clear filter only available on Library bookshelf -->
<div v-if="entityName === 'items'" class="flex justify-center mt-2">
<ui-btn v-if="hasFilter" color="primary" @click="clearFilter">{{ $strings.ButtonClearFilter }}</ui-btn>
@ -109,6 +117,11 @@ export default {
}
return this.$strings.MessageNoResults
},
emptyMessageHelp() {
if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollectionsHelp
if (this.page === 'playlists') return this.$strings.MessageNoUserPlaylistsHelp
return ''
},
entityName() {
if (!this.page) return 'items'
return this.page

View file

@ -394,7 +394,8 @@ export default {
{
src: this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png', true)
}
]
],
chapterInfo
})
console.log('Set media session metadata', navigator.mediaSession.metadata)

View file

@ -24,7 +24,7 @@
</div>
</div>
<div class="w-full max-h-12 overflow-hidden">
<p class="text-gray-500 text-xs">{{ book.description }}</p>
<p class="text-gray-500 text-xs">{{ book.descriptionPlain }}</p>
</div>
</div>
<div v-else class="px-4 flex-grow">

View file

@ -39,7 +39,7 @@
</div>
<!-- No progress shown for podcasts (unless showing podcast episode) -->
<div cy-id="progressBar" v-if="!isPodcast || episodeProgress" class="absolute bottom-0 left-0 h-1e shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
<div cy-id="progressBar" v-if="!isPodcast || episodeProgress" class="absolute bottom-0 left-0 h-1e max-w-full z-20 rounded-b box-shadow-progressbar" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
<!-- Overlay is not shown if collapsing series in library -->
<div cy-id="overlay" v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded md:block" :class="overlayWrapperClasslist">

View file

@ -10,7 +10,7 @@
<p :style="{ fontSize: 0.8 + 'em' }" role="status" :aria-label="$strings.LabelNumberOfBooks">{{ books.length }}</p>
</div>
<div cy-id="seriesProgressBar" v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1e shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
<div cy-id="seriesProgressBar" v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1e shadow-sm max-w-full z-10 rounded-b w-full box-shadow-progressbar" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
<div cy-id="hoveringDisplayTitle" v-if="hasValidCovers" aria-hidden="true" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: '1em' }">
<p :style="{ fontSize: 1.2 + 'em' }">{{ displayTitle }}</p>

View file

@ -1,7 +1,7 @@
<template>
<div ref="wrapper" class="relative ml-4 sm:ml-8" v-click-outside="clickOutside">
<div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)">
<span class="text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base">x</span></span>
<span class="text-gray-200 text-sm sm:text-base">{{ playbackRateDisplay }}<span class="text-base">x</span></span>
</div>
<div v-show="showMenu" class="absolute -top-[5.5rem] z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
<div class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }">
@ -19,7 +19,7 @@
<div class="w-full py-1 px-1">
<div class="flex items-center justify-between">
<ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" />
<p class="px-2 text-2xl sm:text-3xl">{{ playbackRate }}<span class="text-2xl">x</span></p>
<p class="px-2 text-2xl sm:text-3xl">{{ playbackRateDisplay }}<span class="text-2xl">x</span></p>
<ui-icon-btn :disabled="!canIncrement" icon="add" @click="increment" />
</div>
</div>
@ -33,6 +33,10 @@ export default {
value: {
type: [String, Number],
default: 1
},
playbackRateIncrementDecrement: {
type: Number,
default: 0.1
}
},
data() {
@ -58,10 +62,17 @@ export default {
return [0.5, 1, 1.2, 1.5, 2]
},
canIncrement() {
return this.playbackRate + 0.1 <= this.MAX_SPEED
return this.playbackRate + this.playbackRateIncrementDecrement <= this.MAX_SPEED
},
canDecrement() {
return this.playbackRate - 0.1 >= this.MIN_SPEED
return this.playbackRate - this.playbackRateIncrementDecrement >= this.MIN_SPEED
},
playbackRateDisplay() {
if (this.playbackRateIncrementDecrement == 0.05) return this.playbackRate.toFixed(2)
// For 0.1 increment: Only show 2 decimal places if the playback rate is 2 decimals
const numDecimals = String(this.playbackRate).split('.')[1]?.length || 0
if (numDecimals <= 1) return this.playbackRate.toFixed(1)
return this.playbackRate.toFixed(2)
}
},
methods: {
@ -73,14 +84,14 @@ export default {
this.$nextTick(() => this.setShowMenu(false))
},
increment() {
if (this.playbackRate + 0.1 > this.MAX_SPEED) return
var newPlaybackRate = this.playbackRate + 0.1
this.playbackRate = Number(newPlaybackRate.toFixed(1))
if (this.playbackRate + this.playbackRateIncrementDecrement > this.MAX_SPEED) return
var newPlaybackRate = this.playbackRate + this.playbackRateIncrementDecrement
this.playbackRate = Number(newPlaybackRate.toFixed(2))
},
decrement() {
if (this.playbackRate - 0.1 < this.MIN_SPEED) return
var newPlaybackRate = this.playbackRate - 0.1
this.playbackRate = Number(newPlaybackRate.toFixed(1))
if (this.playbackRate - this.playbackRateIncrementDecrement < this.MIN_SPEED) return
var newPlaybackRate = this.playbackRate - this.playbackRateIncrementDecrement
this.playbackRate = Number(newPlaybackRate.toFixed(2))
},
updateMenuPositions() {
if (!this.$refs.wrapper) return

View file

@ -90,8 +90,8 @@
<div class="relative">
<ui-textarea-with-label :value="prettyFfprobeData" readonly :rows="30" class="text-xs" />
<button class="absolute top-4 right-4" :class="copiedToClipboard ? 'text-success' : 'text-white/50 hover:text-white/80'" @click.stop="copyFfprobeData">
<span class="material-symbols">{{ copiedToClipboard ? 'check' : 'content_copy' }}</span>
<button class="absolute top-4 right-4" :class="hasCopied ? 'text-success' : 'text-gray-400 hover:text-white'" @click.stop="copyToClipboard">
<span class="material-symbols">{{ hasCopied ? 'done' : 'content_copy' }}</span>
</button>
</div>
</div>
@ -113,14 +113,13 @@ export default {
return {
probingFile: false,
ffprobeData: null,
copiedToClipboard: false
hasCopied: null
}
},
watch: {
show(newVal) {
if (newVal) {
this.ffprobeData = null
this.copiedToClipboard = false
this.probingFile = false
}
}
@ -165,8 +164,13 @@ export default {
this.probingFile = false
})
},
async copyFfprobeData() {
this.copiedToClipboard = await this.$copyToClipboard(this.prettyFfprobeData)
copyToClipboard() {
clearTimeout(this.hasCopied)
this.$copyToClipboard(this.prettyFfprobeData).then((success) => {
this.hasCopied = setTimeout(() => {
this.hasCopied = null
}, 2000)
})
}
},
mounted() {}

View file

@ -11,9 +11,12 @@
<div class="flex items-center mb-4">
<ui-select-input v-model="jumpForwardAmount" :label="$strings.LabelJumpForwardAmount" menuMaxHeight="250px" :items="jumpValues" @input="setJumpForwardAmount" />
</div>
<div class="flex items-center">
<div class="flex items-center mb-4">
<ui-select-input v-model="jumpBackwardAmount" :label="$strings.LabelJumpBackwardAmount" menuMaxHeight="250px" :items="jumpValues" @input="setJumpBackwardAmount" />
</div>
<div class="flex items-center mb-4">
<ui-select-input v-model="playbackRateIncrementDecrement" :label="$strings.LabelPlaybackRateIncrementDecrement" menuMaxHeight="250px" :items="playbackRateIncrementDecrementValues" @input="setPlaybackRateIncrementDecrementAmount" />
</div>
</div>
</modals-modal>
</template>
@ -35,7 +38,9 @@ export default {
{ text: this.$getString('LabelTimeDurationXMinutes', ['5']), value: 300 }
],
jumpForwardAmount: 10,
jumpBackwardAmount: 10
jumpBackwardAmount: 10,
playbackRateIncrementDecrementValues: [0.1, 0.05],
playbackRateIncrementDecrement: 0.1
}
},
computed: {
@ -60,10 +65,15 @@ export default {
this.jumpBackwardAmount = val
this.$store.dispatch('user/updateUserSettings', { jumpBackwardAmount: val })
},
setPlaybackRateIncrementDecrementAmount(val) {
this.playbackRateIncrementDecrement = val
this.$store.dispatch('user/updateUserSettings', { playbackRateIncrementDecrement: val })
},
settingsUpdated() {
this.useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack')
this.jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount')
this.jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount')
this.playbackRateIncrementDecrement = this.$store.getters['user/getUserSetting']('playbackRateIncrementDecrement')
}
},
mounted() {

View file

@ -16,7 +16,7 @@
<template v-if="currentShare">
<div class="w-full py-2">
<label class="px-1 text-sm font-semibold block">{{ $strings.LabelShareURL }}</label>
<ui-text-input v-model="currentShareUrl" show-copy readonly class="text-base h-10" />
<ui-text-input v-model="currentShareUrl" show-copy readonly />
</div>
<div class="w-full py-2 px-1">
<p v-if="currentShare.isDownloadable" class="text-sm mb-2">{{ $strings.LabelDownloadable }}</p>

View file

@ -19,9 +19,20 @@
</template>
</transition-group>
</div>
<div v-if="!collections.length" class="flex h-32 items-center justify-center">
<p class="text-xl">{{ $strings.MessageNoCollections }}</p>
<div v-if="!collections.length" class="flex h-32 items-center justify-center text-center px-2">
<div>
<p class="text-xl mb-2">{{ $strings.MessageNoCollections }}</p>
<div class="text-sm flex items-center justify-center text-gray-200">
<p>{{ $strings.MessageBookshelfNoCollectionsHelp }}</p>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/collections" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
</div>
</div>
</div>
<div class="w-full h-px bg-white bg-opacity-10" />
<form @submit.prevent="submitCreateCollection">
<div class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">

View file

@ -94,9 +94,9 @@
<div v-if="selectedMatchOrig.description" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.description" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
<ui-rich-text-editor v-model="selectedMatch.description" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</a>
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.descriptionPlain.substr(0, 100) + (mediaMetadata.descriptionPlain.length > 100 ? '...' : '') }}</a>
</p>
</div>
</div>

View file

@ -19,8 +19,18 @@
</template>
</transition-group>
</div>
<div v-if="!playlists.length" class="flex h-32 items-center justify-center">
<p class="text-xl">{{ $strings.MessageNoUserPlaylists }}</p>
<div v-if="!playlists.length" class="flex h-32 items-center justify-center text-center px-2">
<div>
<p class="text-xl mb-2">{{ $strings.MessageNoUserPlaylists }}</p>
<div class="text-sm flex items-center justify-center text-gray-200">
<p>{{ $strings.MessageNoUserPlaylistsHelp }}</p>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/collections" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
</div>
</div>
</div>
<div class="w-full h-px bg-white bg-opacity-10" />
<form @submit.prevent="submitCreatePlaylist">

View file

@ -16,7 +16,7 @@
</div>
</div>
<p dir="auto" class="text-lg font-semibold mb-6">{{ title }}</p>
<div v-if="description" dir="auto" class="default-style" v-html="description" />
<div v-if="description" dir="auto" class="default-style less-spacing" v-html="description" />
<p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p>
<div class="w-full h-px bg-white/5 my-4" />

View file

@ -10,9 +10,7 @@
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</p>
<div class="w-full relative">
<ui-text-input :value="feedUrl" readonly />
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 transform hover:scale-125 cursor-pointer" :class="copiedToClipboard ? 'text-success' : 'text-gray-300 hover:text-white'" @click="copyToClipboard(feedUrl)">{{ copiedToClipboard ? 'check' : 'content_copy' }}</span>
<ui-text-input :value="feedUrl" readonly show-copy />
</div>
<div v-if="currentFeed.meta" class="mt-5">
@ -68,8 +66,7 @@ export default {
preventIndexing: true,
ownerName: '',
ownerEmail: ''
},
copiedToClipboard: false
}
}
},
watch: {
@ -161,12 +158,6 @@ export default {
this.processing = false
})
},
async copyToClipboard(str) {
this.copiedToClipboard = await this.$copyToClipboard(str)
setTimeout(() => {
this.copiedToClipboard = false
}, 2000)
},
closeFeed() {
this.processing = true
this.$axios

View file

@ -5,8 +5,7 @@
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedGeneral }}</p>
<div class="w-full relative">
<ui-text-input :value="feedUrl" readonly />
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 transform hover:scale-125 cursor-pointer" :class="copiedToClipboard ? 'text-success' : 'text-gray-300 hover:text-white'" @click="copyToClipboard(feedUrl)">{{ copiedToClipboard ? 'check' : 'content_copy' }}</span>
<ui-text-input :value="feedUrl" readonly show-copy />
</div>
<div v-if="feed.meta" class="mt-5">
@ -56,8 +55,7 @@ export default {
},
data() {
return {
processing: false,
copiedToClipboard: false
processing: false
}
},
computed: {
@ -75,16 +73,7 @@ export default {
feedUrl() {
return this.feed ? `${window.origin}${this.$config.routerBasePath}${this.feed.feedUrl}` : ''
}
},
methods: {
async copyToClipboard(str) {
this.copiedToClipboard = await this.$copyToClipboard(str)
setTimeout(() => {
this.copiedToClipboard = false
}, 2000)
}
},
mounted() {}
}
}
</script>

View file

@ -2,7 +2,7 @@
<div class="w-full -mt-6">
<div class="w-full relative mb-1">
<div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full">
<controls-playback-speed-control v-model="playbackRate" @input="setPlaybackRate" @change="playbackRateChanged" class="mx-2 block" />
<controls-playback-speed-control v-model="playbackRate" @input="setPlaybackRate" @change="playbackRateChanged" :playbackRateIncrementDecrement="playbackRateIncrementDecrement" class="mx-2 block" />
<ui-tooltip direction="bottom" :text="$strings.LabelVolume">
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden sm:block" />
@ -180,6 +180,9 @@ export default {
useChapterTrack() {
const _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
return this.chapters.length ? _useChapterTrack : false
},
playbackRateIncrementDecrement() {
return this.$store.getters['user/getUserSetting']('playbackRateIncrementDecrement')
}
},
methods: {
@ -223,12 +226,12 @@ export default {
},
increasePlaybackRate() {
if (this.playbackRate >= 10) return
this.playbackRate = Number((this.playbackRate + 0.1).toFixed(1))
this.playbackRate = Number((this.playbackRate + this.playbackRateIncrementDecrement || 0.1).toFixed(2))
this.setPlaybackRate(this.playbackRate)
},
decreasePlaybackRate() {
if (this.playbackRate <= 0.5) return
this.playbackRate = Number((this.playbackRate - 0.1).toFixed(1))
this.playbackRate = Number((this.playbackRate - this.playbackRateIncrementDecrement || 0.1).toFixed(2))
this.setPlaybackRate(this.playbackRate)
},
playbackRateChanged(playbackRate) {

View file

@ -1,9 +1,9 @@
<template>
<div class="default-style">
<p v-if="label" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
<p v-if="label" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }" style="margin-top: 0; margin-bottom: 0.125em">
{{ label }}
</p>
<ui-vue-trix v-model="content" :config="config" :disabled-editor="disabled" @trix-file-accept="trixFileAccept" />
<ui-vue-trix ref="input" v-model="content" :disabled-editor="disabled" @trix-file-accept="trixFileAccept" />
</div>
</template>
@ -12,7 +12,10 @@ export default {
props: {
value: String,
label: String,
disabled: Boolean
disabled: {
type: Boolean,
default: false
}
},
data() {
return {}
@ -25,49 +28,19 @@ export default {
set(val) {
this.$emit('input', val)
}
},
config() {
return {
toolbar: {
getDefaultHTML: () => `<div class="trix-button-row">
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" title="${this.$strings.LabelFontBold}" tabindex="-1">${this.$strings.LabelFontBold}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" title="${this.$strings.LabelFontItalic}" tabindex="-1">${this.$strings.LabelFontItalic}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" title="${this.$strings.LabelFontStrikethrough}" tabindex="-1">${this.$strings.LabelFontStrikethrough}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="${this.$strings.LabelTextEditorLink}" tabindex="-1">${this.$strings.LabelTextEditorLink}</button>
</span>
<span class="trix-button-group trix-button-group--block-tools" data-trix-button-group="block-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" title="${this.$strings.LabelTextEditorBulletedList}" tabindex="-1">${this.$strings.LabelTextEditorBulletedList}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" title="${this.$strings.LabelTextEditorNumberedList}" tabindex="-1">${this.$strings.LabelTextEditorNumberedList}</button>
</span>
<span class="trix-button-group-spacer"></span>
<span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" title="${this.$strings.LabelUndo}" tabindex="-1">${this.$strings.LabelUndo}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" title="${this.$strings.LabelRedo}" tabindex="-1">${this.$strings.LabelRedo}</button>
</span>
</div>
<div class="trix-dialogs" data-trix-dialogs>
<div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
<div class="trix-dialog__link-fields">
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="" aria-label="URL" required data-trix-input>
<div class="trix-button-group">
<input type="button" class="trix-button trix-button--dialog" value="${this.$strings.LabelTextEditorLink}" data-trix-method="setAttribute">
<input type="button" class="trix-button trix-button--dialog" value="${this.$strings.LabelTextEditorUnlink}" data-trix-method="removeAttribute">
</div>
</div>
</div>
</div>`
}
}
}
},
methods: {
trixFileAccept(e) {
e.preventDefault()
},
blur() {
if (this.$refs.input && this.$refs.input.blur) {
this.$refs.input.blur()
}
}
},
mounted() {},
beforeDestroy() {}
}
</script>
</script>

View file

@ -7,8 +7,8 @@
<div v-if="type === 'password' && isHovering" class="absolute top-0 right-0 h-full px-4 flex items-center justify-center">
<span class="material-symbols text-gray-400 cursor-pointer text-lg" @click.stop.prevent="showPassword = !showPassword">{{ !showPassword ? 'visibility' : 'visibility_off' }}</span>
</div>
<div v-else-if="showCopy" class="absolute top-0 right-0 h-full px-4 flex items-center justify-center">
<span class="material-symbols text-gray-400 cursor-pointer text-lg" @click.stop.prevent="copyToClipboard">{{ !hasCopied ? 'content_copy' : 'done' }}</span>
<div v-else-if="showCopy" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
<span class="material-symbols cursor-pointer text-lg" :class="hasCopied ? 'text-success' : 'text-gray-400 hover:text-white'" @click.stop.prevent="copyToClipboard">{{ !hasCopied ? 'content_copy' : 'done' }}</span>
</div>
</div>
</template>
@ -47,7 +47,7 @@ export default {
showPassword: false,
isHovering: false,
isFocused: false,
hasCopied: false,
hasCopied: null,
isInvalidDate: false
}
},
@ -62,7 +62,12 @@ export default {
},
classList() {
var _list = []
_list.push(`px-${this.paddingX}`)
if (this.showCopy) {
_list.push('pl-3', 'pr-8')
} else {
_list.push(`px-${this.paddingX}`)
}
_list.push(`py-${this.paddingY}`)
if (this.noSpinner) _list.push('no-spinner')
if (this.textCenter) _list.push('text-center')
@ -80,11 +85,10 @@ export default {
},
methods: {
copyToClipboard() {
if (this.hasCopied) return
clearTimeout(this.hasCopied)
this.$copyToClipboard(this.inputValue).then((success) => {
this.hasCopied = success
setTimeout(() => {
this.hasCopied = false
this.hasCopied = setTimeout(() => {
this.hasCopied = null
}, 2000)
})
},

View file

@ -6,7 +6,7 @@
<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
</label>
</slot>
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" :class="inputClass" @blur="inputBlurred" />
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :show-copy="showCopy" class="w-full" :class="inputClass" @blur="inputBlurred" />
</div>
</template>
@ -23,7 +23,8 @@ export default {
},
readonly: Boolean,
disabled: Boolean,
inputClass: String
inputClass: String,
showCopy: Boolean
},
data() {
return {}

View file

@ -1,6 +1,37 @@
<template>
<div>
<trix-editor :contenteditable="!disabledEditor" :class="['trix-content']" ref="trix" :input="computedId" :placeholder="placeholder" @trix-change="handleContentChange" @trix-initialize="handleInitialize" @trix-focus="processTrixFocus" @trix-blur="processTrixBlur" />
<trix-toolbar :id="toolbarId">
<div v-show="!disabledEditor" class="trix-button-row">
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" :title="$strings.LabelFontBold" tabindex="-1">{{ $strings.LabelFontBold }}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" :title="$strings.LabelFontItalic" tabindex="-1">{{ $strings.LabelFontItalic }}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" :title="$strings.LabelFontStrikethrough" tabindex="-1">{{ $strings.LabelFontStrikethrough }}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" :title="$strings.LabelTextEditorLink" tabindex="-1">{{ $strings.LabelTextEditorLink }}</button>
</span>
<span class="trix-button-group trix-button-group--block-tools" data-trix-button-group="block-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" :title="$strings.LabelTextEditorBulletedList" tabindex="-1">{{ $strings.LabelTextEditorBulletedList }}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" :title="$strings.LabelTextEditorNumberedList" tabindex="-1">{{ $strings.LabelTextEditorNumberedList }}</button>
</span>
<span class="trix-button-group-spacer"></span>
<span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" :title="$strings.LabelUndo" tabindex="-1">{{ $strings.LabelUndo }}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" :title="$strings.LabelRedo" tabindex="-1">{{ $strings.LabelRedo }}</button>
</span>
</div>
<div class="trix-dialogs" data-trix-dialogs>
<div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
<div class="trix-dialog__link-fields">
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="" aria-label="URL" required data-trix-input />
<div class="trix-button-group">
<input type="button" class="trix-button trix-button--dialog" :value="$strings.LabelTextEditorLink" data-trix-method="setAttribute" />
<input type="button" class="trix-button trix-button--dialog" :value="$strings.LabelTextEditorUnlink" data-trix-method="removeAttribute" />
</div>
</div>
</div>
</div>
</trix-toolbar>
<trix-editor :toolbar="toolbarId" :contenteditable="!disabledEditor" :class="['trix-content']" ref="trix" :input="computedId" :placeholder="placeholder" @trix-change="handleContentChange" @trix-initialize="handleInitialize" @trix-focus="processTrixFocus" @trix-blur="processTrixBlur" />
<input type="hidden" :name="inputName" :id="computedId" :value="editorContent" />
</div>
</template>
@ -14,6 +45,30 @@
import Trix from 'trix'
import '@/assets/trix.css'
function enableBreakParagraphOnReturn() {
// Trix works with divs by default, we want paragraphs instead
Trix.config.blockAttributes.default.tagName = 'p'
// Enable break paragraph on Enter (Shift + Enter will still create a line break)
Trix.config.blockAttributes.default.breakOnReturn = true
// Hack to fix buggy paragraph breaks
// Copied from https://github.com/basecamp/trix/issues/680#issuecomment-735742942
Trix.Block.prototype.breaksOnReturn = function () {
const attr = this.getLastAttribute()
const config = Trix.getBlockConfig(attr ? attr : 'default')
return config ? config.breakOnReturn : false
}
Trix.LineBreakInsertion.prototype.shouldInsertBlockBreak = function () {
if (this.block.hasAttributes() && this.block.isListItem() && !this.block.isEmpty()) {
return this.startLocation.offset > 0
} else {
return !this.shouldBreakFormattedBlock() ? this.breaksOnReturn : false
}
}
}
enableBreakParagraphOnReturn()
export default {
name: 'vue-trix',
model: {
@ -134,6 +189,9 @@ export default {
* Compute a random id of hidden input
* when it haven't been specified.
*/
toolbarId() {
return `trix-toolbar-${this.generateId}`
},
generateId() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
var r = (Math.random() * 16) | 0
@ -223,13 +281,17 @@ export default {
decorateDisabledEditor(editorState) {
/** Disable toolbar and editor by pointer events styling */
if (editorState) {
this.$refs.trix.toolbarElement.style['pointer-events'] = 'none'
this.$refs.trix.disabled = true
this.$refs.trix.contentEditable = false
this.$refs.trix.style['background'] = '#e9ecef'
this.$refs.trix.style['pointer-events'] = 'none'
this.$refs.trix.style['background-color'] = '#444'
this.$refs.trix.style['color'] = '#bbb'
} else {
this.$refs.trix.toolbarElement.style['pointer-events'] = 'unset'
this.$refs.trix.disabled = false
this.$refs.trix.contentEditable = true
this.$refs.trix.style['pointer-events'] = 'unset'
this.$refs.trix.style['background'] = 'transparent'
this.$refs.trix.style['background-color'] = ''
this.$refs.trix.style['color'] = ''
}
},
overrideConfig(config) {
@ -250,32 +312,15 @@ export default {
}
return target
},
enableBreakParagraphOnReturn() {
// Trix works with divs by default, we want paragraphs instead
Trix.config.blockAttributes.default.tagName = 'p'
// Enable break paragraph on Enter (Shift + Enter will still create a line break)
Trix.config.blockAttributes.default.breakOnReturn = true
// Hack to fix buggy paragraph breaks
// Copied from https://github.com/basecamp/trix/issues/680#issuecomment-735742942
Trix.Block.prototype.breaksOnReturn = function () {
const attr = this.getLastAttribute()
const config = Trix.getBlockConfig(attr ? attr : 'default')
return config ? config.breakOnReturn : false
}
Trix.LineBreakInsertion.prototype.shouldInsertBlockBreak = function () {
if (this.block.hasAttributes() && this.block.isListItem() && !this.block.isEmpty()) {
return this.startLocation.offset > 0
} else {
return !this.shouldBreakFormattedBlock() ? this.breaksOnReturn : false
}
blur() {
if (this.$refs.trix && this.$refs.trix.blur) {
this.$refs.trix.blur()
}
}
},
mounted() {
/** Override editor configuration */
this.overrideConfig(this.config)
this.enableBreakParagraphOnReturn()
/** Check if editor read-only mode is required */
this.decorateDisabledEditor(this.disabledEditor)
this.$nextTick(() => {
@ -305,4 +350,12 @@ export default {
.trix_container .trix-content {
background-color: white;
}
trix-editor {
max-height: calc(4 * 1lh);
overflow-y: auto;
}
trix-editor * {
pointer-events: inherit;
}
</style>

View file

@ -26,7 +26,7 @@
</div>
</div>
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
<ui-rich-text-editor ref="descriptionInput" v-model="details.description" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
<div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-1/2 px-1">