1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2024 The Android Open Source Project 2*6dbdd20aSAndroid Build Coastguard Worker// 3*6dbdd20aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License"); 4*6dbdd20aSAndroid Build Coastguard Worker// you may not use this file except in compliance with the License. 5*6dbdd20aSAndroid Build Coastguard Worker// You may obtain a copy of the License at 6*6dbdd20aSAndroid Build Coastguard Worker// 7*6dbdd20aSAndroid Build Coastguard Worker// http://www.apache.org/licenses/LICENSE-2.0 8*6dbdd20aSAndroid Build Coastguard Worker// 9*6dbdd20aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software 10*6dbdd20aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS, 11*6dbdd20aSAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*6dbdd20aSAndroid Build Coastguard Worker// See the License for the specific language governing permissions and 13*6dbdd20aSAndroid Build Coastguard Worker// limitations under the License. 14*6dbdd20aSAndroid Build Coastguard Worker 15*6dbdd20aSAndroid Build Coastguard Workerimport m from 'mithril'; 16*6dbdd20aSAndroid Build Coastguard Workerimport {classNames} from '../base/classnames'; 17*6dbdd20aSAndroid Build Coastguard Workerimport {currentTargetOffset} from '../base/dom_utils'; 18*6dbdd20aSAndroid Build Coastguard Workerimport {Bounds2D, Point2D, Vector2D} from '../base/geom'; 19*6dbdd20aSAndroid Build Coastguard Workerimport {Icons} from '../base/semantic_icons'; 20*6dbdd20aSAndroid Build Coastguard Workerimport {ButtonBar} from './button'; 21*6dbdd20aSAndroid Build Coastguard Workerimport {Chip, ChipBar} from './chip'; 22*6dbdd20aSAndroid Build Coastguard Workerimport {Icon} from './icon'; 23*6dbdd20aSAndroid Build Coastguard Workerimport {MiddleEllipsis} from './middle_ellipsis'; 24*6dbdd20aSAndroid Build Coastguard Workerimport {clamp} from '../base/math_utils'; 25*6dbdd20aSAndroid Build Coastguard Worker 26*6dbdd20aSAndroid Build Coastguard Worker/** 27*6dbdd20aSAndroid Build Coastguard Worker * The TrackWidget defines the look and style of a track. 28*6dbdd20aSAndroid Build Coastguard Worker * 29*6dbdd20aSAndroid Build Coastguard Worker * ┌──────────────────────────────────────────────────────────────────┐ 30*6dbdd20aSAndroid Build Coastguard Worker * │pf-track (grid) │ 31*6dbdd20aSAndroid Build Coastguard Worker * │┌─────────────────────────────────────────┐┌─────────────────────┐│ 32*6dbdd20aSAndroid Build Coastguard Worker * ││pf-track-shell ││pf-track-content ││ 33*6dbdd20aSAndroid Build Coastguard Worker * ││┌───────────────────────────────────────┐││ ││ 34*6dbdd20aSAndroid Build Coastguard Worker * │││pf-track-menubar (sticky) │││ ││ 35*6dbdd20aSAndroid Build Coastguard Worker * │││┌───────────────┐┌────────────────────┐│││ ││ 36*6dbdd20aSAndroid Build Coastguard Worker * ││││pf-track-title ││pf-track-buttons ││││ ││ 37*6dbdd20aSAndroid Build Coastguard Worker * │││└───────────────┘└────────────────────┘│││ ││ 38*6dbdd20aSAndroid Build Coastguard Worker * ││└───────────────────────────────────────┘││ ││ 39*6dbdd20aSAndroid Build Coastguard Worker * │└─────────────────────────────────────────┘└─────────────────────┘│ 40*6dbdd20aSAndroid Build Coastguard Worker * └──────────────────────────────────────────────────────────────────┘ 41*6dbdd20aSAndroid Build Coastguard Worker */ 42*6dbdd20aSAndroid Build Coastguard Worker 43*6dbdd20aSAndroid Build Coastguard Workerexport interface TrackComponentAttrs { 44*6dbdd20aSAndroid Build Coastguard Worker // The title of this track. 45*6dbdd20aSAndroid Build Coastguard Worker readonly title: string; 46*6dbdd20aSAndroid Build Coastguard Worker 47*6dbdd20aSAndroid Build Coastguard Worker // The full path to this track. 48*6dbdd20aSAndroid Build Coastguard Worker readonly path?: string; 49*6dbdd20aSAndroid Build Coastguard Worker 50*6dbdd20aSAndroid Build Coastguard Worker // Show dropdown arrow and make clickable. Defaults to false. 51*6dbdd20aSAndroid Build Coastguard Worker readonly collapsible?: boolean; 52*6dbdd20aSAndroid Build Coastguard Worker 53*6dbdd20aSAndroid Build Coastguard Worker // Show an up or down dropdown arrow. 54*6dbdd20aSAndroid Build Coastguard Worker readonly collapsed: boolean; 55*6dbdd20aSAndroid Build Coastguard Worker 56*6dbdd20aSAndroid Build Coastguard Worker // Height of the track in pixels. All tracks have a fixed height. 57*6dbdd20aSAndroid Build Coastguard Worker readonly heightPx: number; 58*6dbdd20aSAndroid Build Coastguard Worker 59*6dbdd20aSAndroid Build Coastguard Worker // Optional buttons to place on the RHS of the track shell. 60*6dbdd20aSAndroid Build Coastguard Worker readonly buttons?: m.Children; 61*6dbdd20aSAndroid Build Coastguard Worker 62*6dbdd20aSAndroid Build Coastguard Worker // Optional list of chips to display after the track title. 63*6dbdd20aSAndroid Build Coastguard Worker readonly chips?: ReadonlyArray<string>; 64*6dbdd20aSAndroid Build Coastguard Worker 65*6dbdd20aSAndroid Build Coastguard Worker // Render this track in error colours. 66*6dbdd20aSAndroid Build Coastguard Worker readonly error?: boolean; 67*6dbdd20aSAndroid Build Coastguard Worker 68*6dbdd20aSAndroid Build Coastguard Worker // The integer indentation level of this track. If omitted, defaults to 0. 69*6dbdd20aSAndroid Build Coastguard Worker readonly indentationLevel?: number; 70*6dbdd20aSAndroid Build Coastguard Worker 71*6dbdd20aSAndroid Build Coastguard Worker // Track titles are sticky. This is the offset in pixels from the top of the 72*6dbdd20aSAndroid Build Coastguard Worker // scrolling parent. Defaults to 0. 73*6dbdd20aSAndroid Build Coastguard Worker readonly topOffsetPx?: number; 74*6dbdd20aSAndroid Build Coastguard Worker 75*6dbdd20aSAndroid Build Coastguard Worker // Issues a scrollTo() on this DOM element at creation time. Default: false. 76*6dbdd20aSAndroid Build Coastguard Worker readonly revealOnCreate?: boolean; 77*6dbdd20aSAndroid Build Coastguard Worker 78*6dbdd20aSAndroid Build Coastguard Worker // Called when arrow clicked. 79*6dbdd20aSAndroid Build Coastguard Worker readonly onToggleCollapsed?: () => void; 80*6dbdd20aSAndroid Build Coastguard Worker 81*6dbdd20aSAndroid Build Coastguard Worker // Style the component differently if it has children. 82*6dbdd20aSAndroid Build Coastguard Worker readonly isSummary?: boolean; 83*6dbdd20aSAndroid Build Coastguard Worker 84*6dbdd20aSAndroid Build Coastguard Worker // HTML id applied to the root element. 85*6dbdd20aSAndroid Build Coastguard Worker readonly id: string; 86*6dbdd20aSAndroid Build Coastguard Worker 87*6dbdd20aSAndroid Build Coastguard Worker // Whether to highlight the track or not. 88*6dbdd20aSAndroid Build Coastguard Worker readonly highlight?: boolean; 89*6dbdd20aSAndroid Build Coastguard Worker 90*6dbdd20aSAndroid Build Coastguard Worker // Whether the shell should be draggable and emit drag/drop events. 91*6dbdd20aSAndroid Build Coastguard Worker readonly reorderable?: boolean; 92*6dbdd20aSAndroid Build Coastguard Worker 93*6dbdd20aSAndroid Build Coastguard Worker // Mouse events. 94*6dbdd20aSAndroid Build Coastguard Worker readonly onTrackContentMouseMove?: ( 95*6dbdd20aSAndroid Build Coastguard Worker pos: Point2D, 96*6dbdd20aSAndroid Build Coastguard Worker contentSize: Bounds2D, 97*6dbdd20aSAndroid Build Coastguard Worker ) => void; 98*6dbdd20aSAndroid Build Coastguard Worker readonly onTrackContentMouseOut?: () => void; 99*6dbdd20aSAndroid Build Coastguard Worker readonly onTrackContentClick?: ( 100*6dbdd20aSAndroid Build Coastguard Worker pos: Point2D, 101*6dbdd20aSAndroid Build Coastguard Worker contentSize: Bounds2D, 102*6dbdd20aSAndroid Build Coastguard Worker ) => boolean; 103*6dbdd20aSAndroid Build Coastguard Worker 104*6dbdd20aSAndroid Build Coastguard Worker // If reorderable, these functions will be called when track shells are 105*6dbdd20aSAndroid Build Coastguard Worker // dragged and dropped. 106*6dbdd20aSAndroid Build Coastguard Worker readonly onMoveBefore?: (nodeId: string) => void; 107*6dbdd20aSAndroid Build Coastguard Worker readonly onMoveAfter?: (nodeId: string) => void; 108*6dbdd20aSAndroid Build Coastguard Worker} 109*6dbdd20aSAndroid Build Coastguard Worker 110*6dbdd20aSAndroid Build Coastguard Workerconst TRACK_HEIGHT_MIN_PX = 18; 111*6dbdd20aSAndroid Build Coastguard Workerconst INDENTATION_LEVEL_MAX = 16; 112*6dbdd20aSAndroid Build Coastguard Worker 113*6dbdd20aSAndroid Build Coastguard Workerexport class TrackWidget implements m.ClassComponent<TrackComponentAttrs> { 114*6dbdd20aSAndroid Build Coastguard Worker view({attrs}: m.CVnode<TrackComponentAttrs>) { 115*6dbdd20aSAndroid Build Coastguard Worker const { 116*6dbdd20aSAndroid Build Coastguard Worker indentationLevel = 0, 117*6dbdd20aSAndroid Build Coastguard Worker collapsible, 118*6dbdd20aSAndroid Build Coastguard Worker collapsed, 119*6dbdd20aSAndroid Build Coastguard Worker highlight, 120*6dbdd20aSAndroid Build Coastguard Worker heightPx, 121*6dbdd20aSAndroid Build Coastguard Worker id, 122*6dbdd20aSAndroid Build Coastguard Worker isSummary, 123*6dbdd20aSAndroid Build Coastguard Worker } = attrs; 124*6dbdd20aSAndroid Build Coastguard Worker 125*6dbdd20aSAndroid Build Coastguard Worker const trackHeight = Math.max(heightPx, TRACK_HEIGHT_MIN_PX); 126*6dbdd20aSAndroid Build Coastguard Worker const expanded = collapsible && !collapsed; 127*6dbdd20aSAndroid Build Coastguard Worker 128*6dbdd20aSAndroid Build Coastguard Worker return m( 129*6dbdd20aSAndroid Build Coastguard Worker '.pf-track', 130*6dbdd20aSAndroid Build Coastguard Worker { 131*6dbdd20aSAndroid Build Coastguard Worker id, 132*6dbdd20aSAndroid Build Coastguard Worker className: classNames( 133*6dbdd20aSAndroid Build Coastguard Worker expanded && 'pf-expanded', 134*6dbdd20aSAndroid Build Coastguard Worker highlight && 'pf-highlight', 135*6dbdd20aSAndroid Build Coastguard Worker isSummary && 'pf-is-summary', 136*6dbdd20aSAndroid Build Coastguard Worker ), 137*6dbdd20aSAndroid Build Coastguard Worker style: { 138*6dbdd20aSAndroid Build Coastguard Worker // Note: Sub-pixel track heights can mess with sticky elements. 139*6dbdd20aSAndroid Build Coastguard Worker // Round up to the nearest integer number of pixels. 140*6dbdd20aSAndroid Build Coastguard Worker '--indent': clamp(indentationLevel, 0, INDENTATION_LEVEL_MAX), 141*6dbdd20aSAndroid Build Coastguard Worker 'height': `${Math.ceil(trackHeight)}px`, 142*6dbdd20aSAndroid Build Coastguard Worker }, 143*6dbdd20aSAndroid Build Coastguard Worker }, 144*6dbdd20aSAndroid Build Coastguard Worker this.renderShell(attrs), 145*6dbdd20aSAndroid Build Coastguard Worker this.renderContent(attrs), 146*6dbdd20aSAndroid Build Coastguard Worker ); 147*6dbdd20aSAndroid Build Coastguard Worker } 148*6dbdd20aSAndroid Build Coastguard Worker 149*6dbdd20aSAndroid Build Coastguard Worker oncreate(vnode: m.VnodeDOM<TrackComponentAttrs>) { 150*6dbdd20aSAndroid Build Coastguard Worker this.onupdate(vnode); 151*6dbdd20aSAndroid Build Coastguard Worker 152*6dbdd20aSAndroid Build Coastguard Worker if (vnode.attrs.revealOnCreate) { 153*6dbdd20aSAndroid Build Coastguard Worker vnode.dom.scrollIntoView({behavior: 'smooth', block: 'nearest'}); 154*6dbdd20aSAndroid Build Coastguard Worker } 155*6dbdd20aSAndroid Build Coastguard Worker } 156*6dbdd20aSAndroid Build Coastguard Worker 157*6dbdd20aSAndroid Build Coastguard Worker onupdate(vnode: m.VnodeDOM<TrackComponentAttrs>) { 158*6dbdd20aSAndroid Build Coastguard Worker this.decidePopupRequired(vnode.dom); 159*6dbdd20aSAndroid Build Coastguard Worker } 160*6dbdd20aSAndroid Build Coastguard Worker 161*6dbdd20aSAndroid Build Coastguard Worker // Works out whether to display a title popup on hover, based on whether the 162*6dbdd20aSAndroid Build Coastguard Worker // current title is truncated. 163*6dbdd20aSAndroid Build Coastguard Worker private decidePopupRequired(dom: Element) { 164*6dbdd20aSAndroid Build Coastguard Worker const popupTitleElement = dom.querySelector( 165*6dbdd20aSAndroid Build Coastguard Worker '.pf-track-title-popup', 166*6dbdd20aSAndroid Build Coastguard Worker ) as HTMLElement; 167*6dbdd20aSAndroid Build Coastguard Worker const truncatedTitleElement = dom.querySelector( 168*6dbdd20aSAndroid Build Coastguard Worker '.pf-middle-ellipsis', 169*6dbdd20aSAndroid Build Coastguard Worker ) as HTMLElement; 170*6dbdd20aSAndroid Build Coastguard Worker 171*6dbdd20aSAndroid Build Coastguard Worker if (popupTitleElement.clientWidth > truncatedTitleElement.clientWidth) { 172*6dbdd20aSAndroid Build Coastguard Worker popupTitleElement.classList.add('pf-visible'); 173*6dbdd20aSAndroid Build Coastguard Worker } else { 174*6dbdd20aSAndroid Build Coastguard Worker popupTitleElement.classList.remove('pf-visible'); 175*6dbdd20aSAndroid Build Coastguard Worker } 176*6dbdd20aSAndroid Build Coastguard Worker } 177*6dbdd20aSAndroid Build Coastguard Worker 178*6dbdd20aSAndroid Build Coastguard Worker private renderShell(attrs: TrackComponentAttrs): m.Children { 179*6dbdd20aSAndroid Build Coastguard Worker const chips = 180*6dbdd20aSAndroid Build Coastguard Worker attrs.chips && 181*6dbdd20aSAndroid Build Coastguard Worker m( 182*6dbdd20aSAndroid Build Coastguard Worker ChipBar, 183*6dbdd20aSAndroid Build Coastguard Worker attrs.chips.map((chip) => 184*6dbdd20aSAndroid Build Coastguard Worker m(Chip, {label: chip, compact: true, rounded: true}), 185*6dbdd20aSAndroid Build Coastguard Worker ), 186*6dbdd20aSAndroid Build Coastguard Worker ); 187*6dbdd20aSAndroid Build Coastguard Worker 188*6dbdd20aSAndroid Build Coastguard Worker const { 189*6dbdd20aSAndroid Build Coastguard Worker id, 190*6dbdd20aSAndroid Build Coastguard Worker topOffsetPx = 0, 191*6dbdd20aSAndroid Build Coastguard Worker collapsible, 192*6dbdd20aSAndroid Build Coastguard Worker collapsed, 193*6dbdd20aSAndroid Build Coastguard Worker reorderable = false, 194*6dbdd20aSAndroid Build Coastguard Worker onMoveAfter = () => {}, 195*6dbdd20aSAndroid Build Coastguard Worker onMoveBefore = () => {}, 196*6dbdd20aSAndroid Build Coastguard Worker } = attrs; 197*6dbdd20aSAndroid Build Coastguard Worker 198*6dbdd20aSAndroid Build Coastguard Worker return m( 199*6dbdd20aSAndroid Build Coastguard Worker `.pf-track-shell[data-track-node-id=${id}]`, 200*6dbdd20aSAndroid Build Coastguard Worker { 201*6dbdd20aSAndroid Build Coastguard Worker className: classNames(collapsible && 'pf-clickable'), 202*6dbdd20aSAndroid Build Coastguard Worker onclick: (e: MouseEvent) => { 203*6dbdd20aSAndroid Build Coastguard Worker // Block all clicks on the shell from propagating through to the 204*6dbdd20aSAndroid Build Coastguard Worker // canvas 205*6dbdd20aSAndroid Build Coastguard Worker e.stopPropagation(); 206*6dbdd20aSAndroid Build Coastguard Worker if (collapsible) { 207*6dbdd20aSAndroid Build Coastguard Worker attrs.onToggleCollapsed?.(); 208*6dbdd20aSAndroid Build Coastguard Worker } 209*6dbdd20aSAndroid Build Coastguard Worker }, 210*6dbdd20aSAndroid Build Coastguard Worker draggable: reorderable, 211*6dbdd20aSAndroid Build Coastguard Worker ondragstart: (e: DragEvent) => { 212*6dbdd20aSAndroid Build Coastguard Worker e.dataTransfer?.setData('text/plain', id); 213*6dbdd20aSAndroid Build Coastguard Worker }, 214*6dbdd20aSAndroid Build Coastguard Worker ondragover: (e: DragEvent) => { 215*6dbdd20aSAndroid Build Coastguard Worker if (!reorderable) { 216*6dbdd20aSAndroid Build Coastguard Worker return; 217*6dbdd20aSAndroid Build Coastguard Worker } 218*6dbdd20aSAndroid Build Coastguard Worker const target = e.currentTarget as HTMLElement; 219*6dbdd20aSAndroid Build Coastguard Worker const threshold = target.offsetHeight / 2; 220*6dbdd20aSAndroid Build Coastguard Worker if (e.offsetY > threshold) { 221*6dbdd20aSAndroid Build Coastguard Worker target.classList.remove('pf-drag-before'); 222*6dbdd20aSAndroid Build Coastguard Worker target.classList.add('pf-drag-after'); 223*6dbdd20aSAndroid Build Coastguard Worker } else { 224*6dbdd20aSAndroid Build Coastguard Worker target.classList.remove('pf-drag-after'); 225*6dbdd20aSAndroid Build Coastguard Worker target.classList.add('pf-drag-before'); 226*6dbdd20aSAndroid Build Coastguard Worker } 227*6dbdd20aSAndroid Build Coastguard Worker }, 228*6dbdd20aSAndroid Build Coastguard Worker ondragleave: (e: DragEvent) => { 229*6dbdd20aSAndroid Build Coastguard Worker if (!reorderable) { 230*6dbdd20aSAndroid Build Coastguard Worker return; 231*6dbdd20aSAndroid Build Coastguard Worker } 232*6dbdd20aSAndroid Build Coastguard Worker const target = e.currentTarget as HTMLElement; 233*6dbdd20aSAndroid Build Coastguard Worker const related = e.relatedTarget as HTMLElement | null; 234*6dbdd20aSAndroid Build Coastguard Worker if (related && !target.contains(related)) { 235*6dbdd20aSAndroid Build Coastguard Worker target.classList.remove('pf-drag-after'); 236*6dbdd20aSAndroid Build Coastguard Worker target.classList.remove('pf-drag-before'); 237*6dbdd20aSAndroid Build Coastguard Worker } 238*6dbdd20aSAndroid Build Coastguard Worker }, 239*6dbdd20aSAndroid Build Coastguard Worker ondrop: (e: DragEvent) => { 240*6dbdd20aSAndroid Build Coastguard Worker if (!reorderable) { 241*6dbdd20aSAndroid Build Coastguard Worker return; 242*6dbdd20aSAndroid Build Coastguard Worker } 243*6dbdd20aSAndroid Build Coastguard Worker const id = e.dataTransfer?.getData('text/plain'); 244*6dbdd20aSAndroid Build Coastguard Worker const target = e.currentTarget as HTMLElement; 245*6dbdd20aSAndroid Build Coastguard Worker const threshold = target.offsetHeight / 2; 246*6dbdd20aSAndroid Build Coastguard Worker if (id !== undefined) { 247*6dbdd20aSAndroid Build Coastguard Worker if (e.offsetY > threshold) { 248*6dbdd20aSAndroid Build Coastguard Worker onMoveAfter(id); 249*6dbdd20aSAndroid Build Coastguard Worker } else { 250*6dbdd20aSAndroid Build Coastguard Worker onMoveBefore(id); 251*6dbdd20aSAndroid Build Coastguard Worker } 252*6dbdd20aSAndroid Build Coastguard Worker } 253*6dbdd20aSAndroid Build Coastguard Worker target.classList.remove('pf-drag-after'); 254*6dbdd20aSAndroid Build Coastguard Worker target.classList.remove('pf-drag-before'); 255*6dbdd20aSAndroid Build Coastguard Worker }, 256*6dbdd20aSAndroid Build Coastguard Worker }, 257*6dbdd20aSAndroid Build Coastguard Worker m( 258*6dbdd20aSAndroid Build Coastguard Worker '.pf-track-menubar', 259*6dbdd20aSAndroid Build Coastguard Worker { 260*6dbdd20aSAndroid Build Coastguard Worker style: { 261*6dbdd20aSAndroid Build Coastguard Worker position: 'sticky', 262*6dbdd20aSAndroid Build Coastguard Worker top: `${topOffsetPx}px`, 263*6dbdd20aSAndroid Build Coastguard Worker }, 264*6dbdd20aSAndroid Build Coastguard Worker }, 265*6dbdd20aSAndroid Build Coastguard Worker m( 266*6dbdd20aSAndroid Build Coastguard Worker 'h1.pf-track-title', 267*6dbdd20aSAndroid Build Coastguard Worker { 268*6dbdd20aSAndroid Build Coastguard Worker ref: attrs.path, // TODO(stevegolton): Replace with aria tags? 269*6dbdd20aSAndroid Build Coastguard Worker }, 270*6dbdd20aSAndroid Build Coastguard Worker collapsible && 271*6dbdd20aSAndroid Build Coastguard Worker m(Icon, {icon: collapsed ? Icons.ExpandDown : Icons.ExpandUp}), 272*6dbdd20aSAndroid Build Coastguard Worker m( 273*6dbdd20aSAndroid Build Coastguard Worker MiddleEllipsis, 274*6dbdd20aSAndroid Build Coastguard Worker {text: attrs.title}, 275*6dbdd20aSAndroid Build Coastguard Worker m('.pf-track-title-popup', attrs.title), 276*6dbdd20aSAndroid Build Coastguard Worker ), 277*6dbdd20aSAndroid Build Coastguard Worker chips, 278*6dbdd20aSAndroid Build Coastguard Worker ), 279*6dbdd20aSAndroid Build Coastguard Worker m( 280*6dbdd20aSAndroid Build Coastguard Worker ButtonBar, 281*6dbdd20aSAndroid Build Coastguard Worker { 282*6dbdd20aSAndroid Build Coastguard Worker className: 'pf-track-buttons', 283*6dbdd20aSAndroid Build Coastguard Worker // Block button clicks from hitting the shell's on click event 284*6dbdd20aSAndroid Build Coastguard Worker onclick: (e: MouseEvent) => e.stopPropagation(), 285*6dbdd20aSAndroid Build Coastguard Worker }, 286*6dbdd20aSAndroid Build Coastguard Worker attrs.buttons, 287*6dbdd20aSAndroid Build Coastguard Worker ), 288*6dbdd20aSAndroid Build Coastguard Worker ), 289*6dbdd20aSAndroid Build Coastguard Worker ); 290*6dbdd20aSAndroid Build Coastguard Worker } 291*6dbdd20aSAndroid Build Coastguard Worker 292*6dbdd20aSAndroid Build Coastguard Worker private mouseDownPos?: Vector2D; 293*6dbdd20aSAndroid Build Coastguard Worker private selectionOccurred = false; 294*6dbdd20aSAndroid Build Coastguard Worker 295*6dbdd20aSAndroid Build Coastguard Worker private renderContent(attrs: TrackComponentAttrs): m.Children { 296*6dbdd20aSAndroid Build Coastguard Worker const { 297*6dbdd20aSAndroid Build Coastguard Worker heightPx, 298*6dbdd20aSAndroid Build Coastguard Worker onTrackContentMouseMove, 299*6dbdd20aSAndroid Build Coastguard Worker onTrackContentMouseOut, 300*6dbdd20aSAndroid Build Coastguard Worker onTrackContentClick, 301*6dbdd20aSAndroid Build Coastguard Worker } = attrs; 302*6dbdd20aSAndroid Build Coastguard Worker const trackHeight = Math.max(heightPx, TRACK_HEIGHT_MIN_PX); 303*6dbdd20aSAndroid Build Coastguard Worker 304*6dbdd20aSAndroid Build Coastguard Worker return m('.pf-track-content', { 305*6dbdd20aSAndroid Build Coastguard Worker style: { 306*6dbdd20aSAndroid Build Coastguard Worker height: `${trackHeight}px`, 307*6dbdd20aSAndroid Build Coastguard Worker }, 308*6dbdd20aSAndroid Build Coastguard Worker className: classNames(attrs.error && 'pf-track-content-error'), 309*6dbdd20aSAndroid Build Coastguard Worker onmousemove: (e: MouseEvent) => { 310*6dbdd20aSAndroid Build Coastguard Worker onTrackContentMouseMove?.( 311*6dbdd20aSAndroid Build Coastguard Worker currentTargetOffset(e), 312*6dbdd20aSAndroid Build Coastguard Worker getTargetContainerSize(e), 313*6dbdd20aSAndroid Build Coastguard Worker ); 314*6dbdd20aSAndroid Build Coastguard Worker }, 315*6dbdd20aSAndroid Build Coastguard Worker onmouseout: () => { 316*6dbdd20aSAndroid Build Coastguard Worker onTrackContentMouseOut?.(); 317*6dbdd20aSAndroid Build Coastguard Worker }, 318*6dbdd20aSAndroid Build Coastguard Worker onmousedown: (e: MouseEvent) => { 319*6dbdd20aSAndroid Build Coastguard Worker this.mouseDownPos = currentTargetOffset(e); 320*6dbdd20aSAndroid Build Coastguard Worker }, 321*6dbdd20aSAndroid Build Coastguard Worker onmouseup: (e: MouseEvent) => { 322*6dbdd20aSAndroid Build Coastguard Worker if (!this.mouseDownPos) return; 323*6dbdd20aSAndroid Build Coastguard Worker if ( 324*6dbdd20aSAndroid Build Coastguard Worker this.mouseDownPos.sub(currentTargetOffset(e)).manhattanDistance > 1 325*6dbdd20aSAndroid Build Coastguard Worker ) { 326*6dbdd20aSAndroid Build Coastguard Worker this.selectionOccurred = true; 327*6dbdd20aSAndroid Build Coastguard Worker } 328*6dbdd20aSAndroid Build Coastguard Worker this.mouseDownPos = undefined; 329*6dbdd20aSAndroid Build Coastguard Worker }, 330*6dbdd20aSAndroid Build Coastguard Worker onclick: (e: MouseEvent) => { 331*6dbdd20aSAndroid Build Coastguard Worker // This click event occurs after any selection mouse up/drag events 332*6dbdd20aSAndroid Build Coastguard Worker // so we have to look if the mouse moved during this click to know 333*6dbdd20aSAndroid Build Coastguard Worker // if a selection occurred. 334*6dbdd20aSAndroid Build Coastguard Worker if (this.selectionOccurred) { 335*6dbdd20aSAndroid Build Coastguard Worker this.selectionOccurred = false; 336*6dbdd20aSAndroid Build Coastguard Worker return; 337*6dbdd20aSAndroid Build Coastguard Worker } 338*6dbdd20aSAndroid Build Coastguard Worker 339*6dbdd20aSAndroid Build Coastguard Worker // Returns true if something was selected, so stop propagation. 340*6dbdd20aSAndroid Build Coastguard Worker if ( 341*6dbdd20aSAndroid Build Coastguard Worker onTrackContentClick?.( 342*6dbdd20aSAndroid Build Coastguard Worker currentTargetOffset(e), 343*6dbdd20aSAndroid Build Coastguard Worker getTargetContainerSize(e), 344*6dbdd20aSAndroid Build Coastguard Worker ) 345*6dbdd20aSAndroid Build Coastguard Worker ) { 346*6dbdd20aSAndroid Build Coastguard Worker e.stopPropagation(); 347*6dbdd20aSAndroid Build Coastguard Worker } 348*6dbdd20aSAndroid Build Coastguard Worker }, 349*6dbdd20aSAndroid Build Coastguard Worker }); 350*6dbdd20aSAndroid Build Coastguard Worker } 351*6dbdd20aSAndroid Build Coastguard Worker} 352*6dbdd20aSAndroid Build Coastguard Worker 353*6dbdd20aSAndroid Build Coastguard Workerfunction getTargetContainerSize(event: MouseEvent): Bounds2D { 354*6dbdd20aSAndroid Build Coastguard Worker const target = event.target as HTMLElement; 355*6dbdd20aSAndroid Build Coastguard Worker return target.getBoundingClientRect(); 356*6dbdd20aSAndroid Build Coastguard Worker} 357