44-->
55
66<script setup lang="ts">
7+ import type { Ref } from ' vue'
78import type { ReferenceWidgetObject } from ' ./../../functions/reference/widgets.ts'
89
910import { useElementSize , useIntersectionObserver } from ' @vueuse/core'
1011import { computed , inject , nextTick , onBeforeUnmount , ref , useTemplateRef , watch } from ' vue'
1112import { routerKey , RouterLink } from ' vue-router'
1213import NcButton from ' ../../components/NcButton/NcButton.vue'
1314import { t } from ' ../../l10n.ts'
14- import { destroyWidget , hasFullWidth , hasInteractiveView , isWidgetRegistered , renderWidget } from ' ./../../functions/reference/widgets.ts'
15+ import { createElementId } from ' ../../utils/createElementId.ts'
16+ import { destroyWidget , hasFullWidth , hasInteractiveView , isResizable , isWidgetRegistered , renderWidget } from ' ./../../functions/reference/widgets.ts'
1517import { getRoute } from ' ./autolink.ts'
1618
1719const props = withDefaults (defineProps <{
@@ -34,13 +36,18 @@ const props = withDefaults(defineProps<{
3436
3537/* 3 minutes outside of viewport before widget is removed from the DOM */
3638const IDLE_TIMEOUT = 3 * 60 * 1000
39+ const RESIZE_KEYBOARD_STEP = 10
3740
3841const router = inject (routerKey , null )
3942
43+ const widgetId = ` nc-reference-widget-${createElementId ()} `
44+
4045const isVisible = ref (false )
4146const customWidget = useTemplateRef (' customWidget' )
47+ const customWidgetContent = useTemplateRef (' customWidgetContent' )
4248const widgetRoot = useTemplateRef (' widgetRoot' )
4349const { width } = useElementSize (widgetRoot )
50+ const { height : customWidgetHeight } = useElementSize (customWidget )
4451
4552useIntersectionObserver (widgetRoot , ([entry ]) => {
4653 nextTick (() => {
@@ -51,6 +58,12 @@ useIntersectionObserver(widgetRoot, ([entry]) => {
5158const showInteractive = ref (false )
5259const rendered = ref (false )
5360let idleTimeout: NodeJS .Timeout | null = null
61+ const isResizing = ref (false )
62+ const resizedHeight: Ref <number | null > = ref (null )
63+ const resizeStartY = ref (0 )
64+ const resizeStartHeight = ref (0 )
65+ const resizeMinHeight = ref (100 )
66+ const resizeMaxHeight = ref (window .innerHeight - 120 )
5467
5568const isInteractive = computed (() => {
5669 return (! props .interactiveOptIn && props .interactive ) || showInteractive .value
@@ -60,10 +73,25 @@ const referenceHasFullWidth = computed(() => {
6073 return hasFullWidth (props .reference .richObjectType )
6174})
6275
76+ const isWidgetResizable = computed (() => {
77+ return isResizable (props .reference .richObjectType )
78+ })
79+
6380const hasCustomWidget = computed (() => {
6481 return isWidgetRegistered (props .reference .richObjectType )
6582})
6683
84+ const customWidgetStyle = computed (() => {
85+ if (! resizedHeight .value ) {
86+ return null
87+ }
88+ return { height: ` ${resizedHeight .value }px !important ` }
89+ })
90+
91+ const ariaValueNow = computed (() => {
92+ return Math .round (resizedHeight .value ?? customWidgetHeight .value )
93+ })
94+
6795const referenceHasInteractiveView = computed (() => {
6896 return hasCustomWidget .value && hasInteractiveView (props .reference .richObjectType )
6997})
@@ -142,6 +170,11 @@ watch(isVisible, (val) => {
142170}, { immediate: true })
143171
144172onBeforeUnmount (() => {
173+ if (idleTimeout ) {
174+ clearTimeout (idleTimeout )
175+ idleTimeout = null
176+ }
177+ stopResize ()
145178 destroyReferenceWidget ()
146179})
147180
@@ -157,20 +190,20 @@ function enableInteractive() {
157190 * Render the reference widget
158191 */
159192function renderReferenceWidget() {
160- if (! customWidget .value ) {
193+ if (! customWidgetContent .value ) {
161194 return
162195 }
163196
164197 if (props .reference .richObjectType === ' open-graph' ) {
165198 return
166199 }
167200
168- customWidget .value .innerHTML = ' '
201+ customWidgetContent .value .innerHTML = ' '
169202
170203 // create a separate element so we can rerender on the ref again
171204 const widget = document .createElement (' div' )
172205 widget .style .width = ' 100%'
173- customWidget .value .appendChild (widget )
206+ customWidgetContent .value .appendChild (widget )
174207 nextTick (() => {
175208 // Waiting for the ref to become available
176209 renderWidget (widget , {
@@ -190,15 +223,115 @@ function destroyReferenceWidget() {
190223 rendered .value = false
191224 }
192225}
226+
227+ /**
228+ * Initialize resize limits
229+ */
230+ function initResizeLimits() {
231+ // Use min/max height from rendered widget element if set
232+ const widgetEl = customWidgetContent .value ?.firstElementChild
233+ if (widgetEl ) {
234+ const computedStyle = window .getComputedStyle (widgetEl )
235+ const parsedMin = parseFloat (computedStyle .minHeight )
236+ const parsedMax = parseFloat (computedStyle .maxHeight )
237+ resizeMinHeight .value = parsedMin > 0 ? parsedMin : 100
238+ resizeMaxHeight .value = Number .isFinite (parsedMax ) && parsedMax > 0 ? parsedMax : (window .innerHeight - 120 )
239+ } else {
240+ resizeMinHeight .value = 100
241+ resizeMaxHeight .value = window .innerHeight - 120
242+ }
243+ }
244+
245+ /**
246+ * Start resize via dragging
247+ *
248+ * @param event - the pointer event
249+ */
250+ function startResize(event : PointerEvent ) {
251+ if (! isWidgetResizable .value || ! customWidget .value ) {
252+ return
253+ }
254+
255+ initResizeLimits ()
256+ isResizing .value = true
257+ resizeStartY .value = event .clientY
258+ resizeStartHeight .value = customWidget .value .getBoundingClientRect ().height
259+
260+ window .addEventListener (' pointermove' , onResize )
261+ window .addEventListener (' pointerup' , stopResize )
262+ }
263+
264+ /**
265+ * Resize via keydown
266+ *
267+ * @param delta - the resize delta
268+ */
269+ function onResizeKeydown(delta : number ) {
270+ if (! isWidgetResizable .value || ! customWidget .value ) {
271+ return
272+ }
273+
274+ // Establish limits once per interaction
275+ initResizeLimits ()
276+
277+ const currentHeight = resizedHeight .value ?? resizeStartHeight .value
278+ const next = currentHeight + delta
279+ resizedHeight .value = Math .min (resizeMaxHeight .value , Math .max (resizeMinHeight .value , next ))
280+ }
281+
282+ /**
283+ * Resize via dragging
284+ *
285+ * @param event - the pointer event
286+ */
287+ function onResize(event : PointerEvent ) {
288+ if (! isResizing .value || ! customWidget .value ) {
289+ return
290+ }
291+
292+ const deltaY = event .clientY - resizeStartY .value
293+ resizedHeight .value = Math .min (resizeMaxHeight .value , Math .max (resizeMinHeight .value , resizeStartHeight .value + deltaY ))
294+ }
295+
296+ /**
297+ * Stop resize via dragging
298+ */
299+ function stopResize() {
300+ if (! isResizing .value ) {
301+ return
302+ }
303+
304+ isResizing .value = false
305+ window .removeEventListener (' pointermove' , onResize )
306+ window .removeEventListener (' pointerup' , stopResize )
307+ }
193308 </script >
194309
195310<template >
196311 <div ref =" widgetRoot" :class =" { 'toggle-interactive': referenceHasInteractiveView && !isInteractive }" >
197312 <div
198313 v-if =" reference && hasCustomWidget"
314+ :id =" widgetId"
199315 ref =" customWidget"
200316 class =" widget-custom"
201- :class =" { 'full-width': referenceHasFullWidth }" />
317+ :class =" { 'full-width': referenceHasFullWidth }"
318+ :style =" customWidgetStyle" >
319+ <div ref =" customWidgetContent" class =" widget-custom__content" />
320+ <div
321+ v-if =" isWidgetResizable"
322+ class =" widget-custom__resize-handle"
323+ role =" slider"
324+ tabindex =" 0"
325+ aria-orientation =" vertical"
326+ :aria-label =" t('Widget height')"
327+ :aria-valuenow =" ariaValueNow"
328+ :aria-valuemin =" resizeMinHeight"
329+ :aria-valuemax =" resizeMaxHeight"
330+ :aria-controls =" widgetId"
331+ @pointerdown.stop.prevent =" startResize"
332+ @keydown.up.stop.prevent =" onResizeKeydown(-RESIZE_KEYBOARD_STEP)"
333+ @keydown.down.stop.prevent =" onResizeKeydown(RESIZE_KEYBOARD_STEP)" />
334+ </div >
202335
203336 <component
204337 :is =" referenceWidgetLinkComponent"
@@ -241,12 +374,52 @@ function destroyReferenceWidget() {
241374
242375.widget-custom {
243376 @include widget ;
377+ position : relative ;
244378
245379 & .full-width {
246380 width : var (--widget-full-width , 100% ) !important ;
247381 inset-inline-start : calc ( (var (--widget-full-width , 100% ) - 100% ) / 2 * -1 );
248382 position : relative ;
249383 }
384+
385+ & __content {
386+ width : 100% ;
387+ display : flex ;
388+ flex-direction : column ;
389+ }
390+
391+ & __resize-handle {
392+ position : absolute ;
393+ bottom : 0 ;
394+ inset-inline-start : 0 ;
395+ width : 100% ;
396+ height : calc (var (--default-grid-baseline ) * 2 );
397+ border-top : 1px solid var (--color-border );
398+ margin-top : -1px ;
399+ cursor : row-resize ;
400+ touch-action : none ;
401+
402+ & ::before , & ::after {
403+ content : ' ' ;
404+ position : absolute ;
405+ top : 50% ;
406+ left : 50% ;
407+ background-color : var (--color-border );
408+ margin-top : 1px ;
409+ transform : translateX (-50% );
410+ width : 30px ;
411+ height : 1px ;
412+ }
413+
414+ & ::before {
415+ margin-top : -2px ;
416+ }
417+
418+ & :focus-visible {
419+ outline : 2px solid var (--color-primary-element );
420+ outline-offset : -4px ;
421+ }
422+ }
250423}
251424
252425.widget-access {
0 commit comments