-
+ @clear="$vfm.open(`delete-slide-${index}-fr-config`)"
+ @createConfig="createNewConfig(index, 'fr')"
+ />
import ActionModal from '@/components/helpers/action-modal.vue';
-import TocOptions from '@/components/helpers/toc-options.vue';
+import SlideTocButton from '@/components/helpers/slide-toc-button.vue';
import { Options, Prop, Vue } from 'vue-property-decorator';
import {
BasePanel,
@@ -542,12 +338,12 @@ import ConfirmationModalV from './helpers/confirmation-modal.vue';
@Options({
components: {
- TocOptions,
ActionModal,
'slide-editor': SlideEditorV,
'confirmation-modal': ConfirmationModalV,
'vue-final-modal': VueFinalModal,
- draggable
+ draggable,
+ SlideTocButton
}
})
export default class SlideTocV extends Vue {
@@ -801,21 +597,6 @@ window.addEventListener('resize', () => {
margin: 10px 0px 0px 0px !important;
}
-.slide-toc-button {
- border-radius: 3px;
- padding: 2px;
-}
-.slide-toc-button:hover {
- background-color: rgb(209, 213, 219);
-}
-
-.line-clamp-2 {
- overflow: hidden;
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 2;
-}
-
/* Hard coded height :(
TODO: Change positioning of app components so we don't need to hardcode
TODO: Change height here when any new changes cause overshoot
@@ -836,4 +617,12 @@ window.addEventListener('resize', () => {
.selected-toc-config-item {
background-color: rgb(225, 225, 225);
}
+
+.slide-toc-button {
+ border-radius: 3px;
+ padding: 2px;
+}
+.slide-toc-button:hover {
+ background-color: rgb(209, 213, 219);
+}
diff --git a/src/directives/truncate/truncate.ts b/src/directives/truncate/truncate.ts
new file mode 100644
index 00000000..c91f9f0a
--- /dev/null
+++ b/src/directives/truncate/truncate.ts
@@ -0,0 +1,108 @@
+import { useTippy } from 'vue-tippy';
+import type { TippyContent } from 'vue-tippy';
+import linkifyHtml from 'linkify-html';
+import type { Directive, DirectiveBinding } from 'vue';
+
+const TRUNCATE_ATTR = 'truncate-text';
+const TRIGGER_ATTR = 'truncate-trigger';
+
+/**
+ * The Truncate Directive
+ *
+ * It makes the text truncate as needed and adds a tooltip that shows IFF the text is actually truncated.
+ *
+ * The binding value looks like:
+ * ```
+ * {
+ * externalTrigger: boolean,
+ * options: tippyOptions
+ * }
+ * ```
+ * if externalTrigger is present you must put the attribute `truncate-trigger` on the element you wish to be the tooltip trigger (this element must be an ancestor of the element with v-truncate)
+ * if noTruncateClass is present it will prevent the 'truncate' class from being added (which can break some elements)
+ */
+export const Truncate: Directive = {
+ beforeMount(el: HTMLElement, binding: DirectiveBinding) {
+ if (!el.classList.contains('truncate-multiline') && !binding.value?.noTruncateClass) {
+ el.classList.add('truncate-multiline');
+ }
+
+ el.toggleAttribute(TRUNCATE_ATTR, true);
+ },
+ mounted(el: HTMLElement, binding: DirectiveBinding) {
+ let triggerElement;
+ if (binding.value && binding.value.externalTrigger) {
+ // el.closest gets closest ancestor that matches the selector (moves up the parent chain)
+ triggerElement = el.closest(`[${TRIGGER_ATTR}]`);
+ }
+
+ useTippy(el, {
+ content: linkifyContent(el.textContent),
+ onShow: onShow,
+ allowHTML: true,
+ placement: 'bottom-start',
+ flip: false, // can't find a replacement for Vue3
+ boundary: 'window',
+ triggerTarget: triggerElement,
+ size: 'large',
+ ...(binding.value?.options || {})
+ });
+ },
+ updated(el: HTMLElement, binding: DirectiveBinding) {
+ // update content and options
+ if ((el as any)._tippy) {
+ (el as any)._tippy.setContent(linkifyContent(el.textContent));
+ if (binding.value && binding.value.options) {
+ (el as any)._tippy.setProps(binding.value.options);
+ }
+ }
+ },
+ unmounted(el: HTMLElement) {
+ // destroy tippy instance
+ if ((el as any)._tippy) {
+ (el as any)._tippy.destroy();
+ }
+ }
+};
+
+/**
+ * The callback for the onShow lifecycle hook of tooltips
+ *
+ * @param instance tippy instance, automatically given to this on callback
+ * @returns false IFF the text is not being truncated and the tooltip should not be shown
+ */
+function onShow(instance: any) {
+ // cancel showing the tooltip if the text isn't truncated
+ // clientWidth is the visible width of the element, scrollWidth is the width of the content
+ // clientHeight is the visible height of the element, scrollHeight is the height of the content
+ const isTruncated =
+ instance.reference.clientWidth < instance.reference.scrollWidth ||
+ instance.reference.clientHeight < instance.reference.scrollHeight;
+
+ if (!isTruncated) {
+ // returning false tells tippy to cancel
+ return false;
+ }
+}
+
+/**
+ * Applies hyperlinks to any URLs in the provided content.
+ *
+ * @param the text content
+ * @returns a string with any URLs hyperlinked.
+ */
+function linkifyContent(content: string | null): TippyContent {
+ if (content === null) {
+ return '';
+ }
+
+ let res = linkifyHtml(content, {
+ target: '_blank',
+ validate: {
+ url: (value: string) => /^https?:\/\//.test(value) // only links that begin with a protocol will be hyperlinked
+ }
+ });
+ res = `
${res}
`;
+
+ return res;
+}
diff --git a/src/lang/lang.csv b/src/lang/lang.csv
index d3af0f78..b2361917 100644
--- a/src/lang/lang.csv
+++ b/src/lang/lang.csv
@@ -210,7 +210,7 @@ editor.slides.toc.copySlide,Copy Slide,1,Copier la diapositive,0
editor.slides.toc.deleteSlide,Delete Slide,1,Supprimer la diapositive,0
editor.slides.toc.newENGSlideText,New EN Slide*,1,Nouvelle diapositive AN*,0
editor.slides.toc.newFRSlideText,New FR Slide*,1,Nouvelle diapositive FR*,0
-editor.slides.toc.noENGslide,(No English Config),1,(Pas de configuration Anglais),0
+editor.slide.toc.noENGslide,(No English Config),1,(Pas de configuration Anglais),0
editor.slide.toc.noFRSlide,(No French Config),1,(Pas de configuration française),0
editor.slide.toc.untitledENG,(Untitled English slide),1,(Diapositive anglaise sans titre),0
editor.slide.toc.untitledFR,(Untitled French slide),1,(Diapositive française sans titre),0
diff --git a/src/main.ts b/src/main.ts
index f907123e..9cfc7de5 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -36,6 +36,7 @@ import StorylinesViewer from 'ramp-storylines_demo-scenarios-pcar';
import 'ramp-storylines_demo-scenarios-pcar/dist/style.css';
import { FocusContainer, FocusItem, FocusList } from '@/directives/focus-list';
+import { Truncate } from '@/directives/truncate/truncate';
const app = createApp(App);
const pinia = createPinia();
@@ -56,4 +57,5 @@ app.use(pinia)
app.directive('focus-container', FocusContainer);
app.directive('focus-list', FocusList);
app.directive('focus-item', FocusItem);
+app.directive('truncate', Truncate);
app.mount('#app');