[{"data":1,"prerenderedAt":1404},["ShallowReactive",2],{"blog-\u002Fblog\u002F2023\u002F12\u002Fdashboard-0-10-0":3},{"id":4,"title":5,"body":6,"description":1377,"extension":1378,"meta":1379,"navigation":144,"path":1400,"seo":1401,"stem":1402,"__hash__":1403},"blog\u002Fblog\u002F2023\u002F12\u002Fdashboard-0-10-0.md","Building a Custom Video Player in Dashboard 2.0",{"type":7,"value":8,"toc":1367},"minimark",[9,23,42,45,48,60,78,83,86,97,286,289,345,350,356,397,404,408,412,415,522,525,570,577,584,588,591,596,618,840,850,856,860,884,1135,1139,1146,1298,1301,1304,1320,1324,1331,1340,1348,1364],[10,11,12,13,17,18,22],"p",{},"Dashboard 2.0 just got ",[14,15,16],"em",{},"a lot"," more powerful with our new updates to the ",[19,20,21],"code",{},"ui-template"," node. New features added to the node include:",[24,25,26,30,37],"ul",{},[27,28,29],"li",{},"Support for a full Vue component  to be defined using the VueJS Options API.",[27,31,32,33,36],{},"Running of raw JavaScript within ",[19,34,35],{},"\u003Cscript \u002F>"," tags",[27,38,39,40,36],{},"Loading of external dependencies through ",[19,41,35],{},[10,43,44],{},"In this article we're going to deepdive into an example of how you can use this new functionality to build a custom video player.",[10,46,47],{},"We're going to aim for 3 key features:",[49,50,51,54,57],"ol",{},[27,52,53],{},"Emit events into Node-RED when a user plays\u002Fpauses the video",[27,55,56],{},"Allow for the video to be played\u002Fpaused from within Node-RED",[27,58,59],{},"Allow the user to seek to a specific point in the video from within Node-RED",[61,62,64,65,69,70,73,74,77],"div",{"style":63},"background-color: #fff4b9; border:1px solid #ffc400; color: #a27110; padding: 12px; border-radius: 6px; font-style: italic;","Reminder: all new releases of Dashboard are now under the ",[19,66,68],{"style":67},"background-color: transparent;","@flowfuse"," namespace, so you'll need to update to use ",[19,71,72],{"style":67},"@flowfuse\u002Fnode-red-dashboard",", and not ",[19,75,76],{"style":67},"@flowforge",".",[79,80,82],"h2",{"id":81},"building-a-vue-component","Building a Vue Component",[10,84,85],{},"With Dashboard 2.0, we switched over our underlying front-end framework to VueJS. We're aware that not everyone coming into Dashboard 2.0 will be familiar with VueJS.",[10,87,88,89,96],{},"We have a more detailed guide ",[90,91,95],"a",{"href":92,"rel":93},"https:\u002F\u002Fdashboard.flowfuse.com\u002Fnodes\u002Fwidgets\u002Fui-template.html#building-full-vue-components",[94],"nofollow","here",", but we'll also give a quick overview of the elements from Vue \"component\" that we'll use here:",[98,99,104],"pre",{"className":100,"code":101,"language":102,"meta":103,"style":103},"language-html shiki shiki-themes github-light github-dark","\u003Ctemplate>\n    \u003C!-- Our HTML content will go here -->\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nexport default {\n  name: 'MyComponent',\n  methods: {\n    \u002F\u002F JS methods we want to use across our component will go here\n  },\n  mounted () {\n    \u002F\u002F Code we want to run when our component is loaded will go here\n  },\n  unmounted () {\n    \u002F\u002F Code we want to run when our component is unloaded will go here\n  }\n}\n\u003C\u002Fscript>\n\n\u003Cstyle>\n    \u002F* We can define custom CSS here too *\u002F\n\u003C\u002Fstyle>\n","html","",[19,105,106,122,129,139,146,156,169,182,188,194,200,210,216,221,229,235,241,247,256,261,271,277],{"__ignoreMap":103},[107,108,111,115,119],"span",{"class":109,"line":110},"line",1,[107,112,114],{"class":113},"sVt8B","\u003C",[107,116,118],{"class":117},"s9eBZ","template",[107,120,121],{"class":113},">\n",[107,123,125],{"class":109,"line":124},2,[107,126,128],{"class":127},"sJ8bj","    \u003C!-- Our HTML content will go here -->\n",[107,130,132,135,137],{"class":109,"line":131},3,[107,133,134],{"class":113},"\u003C\u002F",[107,136,118],{"class":117},[107,138,121],{"class":113},[107,140,142],{"class":109,"line":141},4,[107,143,145],{"emptyLinePlaceholder":144},true,"\n",[107,147,149,151,154],{"class":109,"line":148},5,[107,150,114],{"class":113},[107,152,153],{"class":117},"script",[107,155,121],{"class":113},[107,157,159,163,166],{"class":109,"line":158},6,[107,160,162],{"class":161},"szBVR","export",[107,164,165],{"class":161}," default",[107,167,168],{"class":113}," {\n",[107,170,172,175,179],{"class":109,"line":171},7,[107,173,174],{"class":113},"  name: ",[107,176,178],{"class":177},"sZZnC","'MyComponent'",[107,180,181],{"class":113},",\n",[107,183,185],{"class":109,"line":184},8,[107,186,187],{"class":113},"  methods: {\n",[107,189,191],{"class":109,"line":190},9,[107,192,193],{"class":127},"    \u002F\u002F JS methods we want to use across our component will go here\n",[107,195,197],{"class":109,"line":196},10,[107,198,199],{"class":113},"  },\n",[107,201,203,207],{"class":109,"line":202},11,[107,204,206],{"class":205},"sScJk","  mounted",[107,208,209],{"class":113}," () {\n",[107,211,213],{"class":109,"line":212},12,[107,214,215],{"class":127},"    \u002F\u002F Code we want to run when our component is loaded will go here\n",[107,217,219],{"class":109,"line":218},13,[107,220,199],{"class":113},[107,222,224,227],{"class":109,"line":223},14,[107,225,226],{"class":205},"  unmounted",[107,228,209],{"class":113},[107,230,232],{"class":109,"line":231},15,[107,233,234],{"class":127},"    \u002F\u002F Code we want to run when our component is unloaded will go here\n",[107,236,238],{"class":109,"line":237},16,[107,239,240],{"class":113},"  }\n",[107,242,244],{"class":109,"line":243},17,[107,245,246],{"class":113},"}\n",[107,248,250,252,254],{"class":109,"line":249},18,[107,251,134],{"class":113},[107,253,153],{"class":117},[107,255,121],{"class":113},[107,257,259],{"class":109,"line":258},19,[107,260,145],{"emptyLinePlaceholder":144},[107,262,264,266,269],{"class":109,"line":263},20,[107,265,114],{"class":113},[107,267,268],{"class":117},"style",[107,270,121],{"class":113},[107,272,274],{"class":109,"line":273},21,[107,275,276],{"class":127},"    \u002F* We can define custom CSS here too *\u002F\n",[107,278,280,282,284],{"class":109,"line":279},22,[107,281,134],{"class":113},[107,283,268],{"class":117},[107,285,121],{"class":113},[10,287,288],{},"Some quick gotchas to note:",[24,290,291,297,303,309,315,333],{},[27,292,293,296],{},[19,294,295],{},"\u003Cdiv>{{ msg }}\u003C\u002Fdiv>"," - is an example of how you render variables into the HTML.",[27,298,299,302],{},[19,300,301],{},"\u003Cdiv v-if=\"myVar\">\u003C\u002Fdiv>"," - lets you conditionally show\u002Fhide content based on a variable.",[27,304,305,308],{},[19,306,307],{},"\u003Cdiv v-for=\"item in items\">\u003C\u002Fdiv>"," - lets you loop over an array of items and render them into the HTML.",[27,310,311,314],{},[19,312,313],{},"\u003Cdiv @click=\"myMethod\">\u003C\u002Fdiv>"," - lets you bind a method to an event, in this case, when the user clicks on the div.",[27,316,317,320,321,324,325,328,329,332],{},[19,318,319],{},"\u003Cdiv :class=\"{ 'my-class': isActive }\">\u003C\u002Fdiv>"," - ",[19,322,323],{},":"," is a way to define a \"bound\" property. In this case, the class ",[19,326,327],{},"my-class"," will be applied when ",[19,330,331],{},"isActive"," is true.",[27,334,335,338,339,341,342,77],{},[19,336,337],{},"console.log(this.myVar)"," - when you're writing code inside the ",[19,340,35],{}," tags, you can access Component variables and methods using ",[19,343,344],{},"this",[346,347,349],"h3",{"id":348},"built-in-extras","Built-in Extras",[10,351,352,353,355],{},"In addition to building a component from scratch, we'll also utilize some built-in features of ",[19,354,21],{}," too. These will be:",[24,357,358,384],{},[27,359,360,364],{},[361,362,363],"strong",{},"Variables:",[24,365,366,372,378],{},[27,367,368,371],{},[19,369,370],{},"id"," - The unique ID for this node in Node-RED",[27,373,374,377],{},[19,375,376],{},"msg"," - The message that was most recently received into the node",[27,379,380,383],{},[19,381,382],{},"$socket"," - The underlying SocketIO connection to Node-RED. Use this to listen to any incoming events, and send new ones back.",[27,385,386,389],{},[361,387,388],{},"Functions:",[24,390,391],{},[27,392,393,396],{},[19,394,395],{},"send(payload)"," - Send a message back to Node-RED",[10,398,399,400,77],{},"As above, we have more detailed documentation on these features ",[90,401,95],{"href":402,"rel":403},"https:\u002F\u002Fdashboard.flowfuse.com\u002Fnodes\u002Fwidgets\u002Fui-template.html#built-in-functionality",[94],[79,405,407],{"id":406},"building-the-video-player","Building the Video Player",[346,409,411],{"id":410},"defining-the-content-html","Defining the Content (HTML)",[10,413,414],{},"We're going to start by adding a basic HTML video player:",[98,416,418],{"className":100,"code":417,"language":102,"meta":103,"style":103},"\u003Ctemplate>\n    \u003Cvideo ref=\"my-video\" style=\"width: 100%\" controls @play=\"onPlay\" @pause=\"onPause\">\n        \u003Csource src=\"http:\u002F\u002Fcommondatastorage.googleapis.com\u002Fgtv-videos-bucket\u002Fsample\u002FBigBuckBunny.mp4\" type=\"video\u002Fmp4\">\n        Your browser does not support the video tag.\n    \u003C\u002Fvideo>\n\u003C\u002Ftemplate>\n",[19,419,420,428,474,500,505,514],{"__ignoreMap":103},[107,421,422,424,426],{"class":109,"line":110},[107,423,114],{"class":113},[107,425,118],{"class":117},[107,427,121],{"class":113},[107,429,430,433,436,439,442,445,448,450,453,456,459,461,464,467,469,472],{"class":109,"line":124},[107,431,432],{"class":113},"    \u003C",[107,434,435],{"class":117},"video",[107,437,438],{"class":205}," ref",[107,440,441],{"class":113},"=",[107,443,444],{"class":177},"\"my-video\"",[107,446,447],{"class":205}," style",[107,449,441],{"class":113},[107,451,452],{"class":177},"\"width: 100%\"",[107,454,455],{"class":205}," controls",[107,457,458],{"class":205}," @play",[107,460,441],{"class":113},[107,462,463],{"class":177},"\"onPlay\"",[107,465,466],{"class":205}," @pause",[107,468,441],{"class":113},[107,470,471],{"class":177},"\"onPause\"",[107,473,121],{"class":113},[107,475,476,479,482,485,487,490,493,495,498],{"class":109,"line":131},[107,477,478],{"class":113},"        \u003C",[107,480,481],{"class":117},"source",[107,483,484],{"class":205}," src",[107,486,441],{"class":113},[107,488,489],{"class":177},"\"http:\u002F\u002Fcommondatastorage.googleapis.com\u002Fgtv-videos-bucket\u002Fsample\u002FBigBuckBunny.mp4\"",[107,491,492],{"class":205}," type",[107,494,441],{"class":113},[107,496,497],{"class":177},"\"video\u002Fmp4\"",[107,499,121],{"class":113},[107,501,502],{"class":109,"line":141},[107,503,504],{"class":113},"        Your browser does not support the video tag.\n",[107,506,507,510,512],{"class":109,"line":148},[107,508,509],{"class":113},"    \u003C\u002F",[107,511,435],{"class":117},[107,513,121],{"class":113},[107,515,516,518,520],{"class":109,"line":158},[107,517,134],{"class":113},[107,519,118],{"class":117},[107,521,121],{"class":113},[10,523,524],{},"A few things of importance to note here:",[24,526,527,541,547,561],{},[27,528,529,532,533,536,537,540],{},[19,530,531],{},"ref"," is Vue's replacement for ",[19,534,535],{},"document.getElementById()",". This is copied to each instance of the component, meaning we can call ",[19,538,539],{},"this.$refs['my-video']"," to access the video element, and this doesn't break when duplicating the widget multiple times in Dashboard.",[27,542,543,546],{},[19,544,545],{},"style=\"\""," is required here to ensure the video fills the group\u002Fwrapper that it is contained within.",[27,548,549,552,553,556,557,560],{},[19,550,551],{},"@play="," is Vue's way of binding onto the standard ",[19,554,555],{},"onplay"," event listener available on HTML video players. We'll define the ",[19,558,559],{},"onPlay"," method in the next section.",[27,562,563,566,567,569],{},[19,564,565],{},"@pause="," is our event listener for when the video is paused by the user. As with ",[19,568,559],{},", we'll define this shortly.",[10,571,572,573,576],{},"With ",[14,574,575],{},"just"," the above defined, we end up with a standard video player rendered:",[10,578,579],{},[580,581],"img",{"alt":582,"src":583},"HTML5 Video Player rendered in Dashboard","\u002Fblog\u002F2023\u002F12\u002Fimages\u002Fdashboard-video-1.png",[346,585,587],{"id":586},"defining-the-behaviors-vuejs","Defining the Behaviors (VueJS)",[10,589,590],{},"Now we begin to build our Vue component. Referring back to our earlier set of features, we'll tackle these one at a time.",[592,593,595],"h4",{"id":594},"_1-emitting-events-to-node-red-on-playpause","1. Emitting Events to Node-RED on Play\u002FPause",[10,597,598,599,602,603,605,606,609,610,613,614,617],{},"We can use ",[19,600,601],{},"methods"," to define our ",[19,604,559],{}," and ",[19,607,608],{},"onPause"," functions that are called ",[19,611,612],{},"@play","\u002F",[19,615,616],{},"@pause"," respectively.",[98,619,621],{"className":100,"code":620,"language":102,"meta":103,"style":103},"\u003Cscript>\nexport default {\n  name: 'MyVideoPlayer',\n  methods: {\n    capture (eventType) {\n        \u002F\u002F let's define our own function that can be called onPlay\u002FonPause\n        \u002F\u002F this prevents duplicated code across the two methods\n\n        \u002F\u002F get the Video's DOM element\n        const video = this.$refs['my-video']\n\n        \u002F\u002F send a msg to Node-RED using built-in \"send\" fcn\n        this.send({\n            \u002F\u002F specify which action is taking place\n            event: eventType,\n            \u002F\u002F use Vue's $refs to get the video's currentTime\n            time: video.currentTime\n        })\n    },\n    onPlay () {\n        this.capture('play')\n    },\n    onPause () {\n        this.capture('pause')\n    }\n  }\n}\n\u003C\u002Fscript>\n",[19,622,623,631,639,648,652,667,672,677,681,686,710,714,719,732,737,742,747,752,757,762,769,787,791,799,815,821,826,831],{"__ignoreMap":103},[107,624,625,627,629],{"class":109,"line":110},[107,626,114],{"class":113},[107,628,153],{"class":117},[107,630,121],{"class":113},[107,632,633,635,637],{"class":109,"line":124},[107,634,162],{"class":161},[107,636,165],{"class":161},[107,638,168],{"class":113},[107,640,641,643,646],{"class":109,"line":131},[107,642,174],{"class":113},[107,644,645],{"class":177},"'MyVideoPlayer'",[107,647,181],{"class":113},[107,649,650],{"class":109,"line":141},[107,651,187],{"class":113},[107,653,654,657,660,664],{"class":109,"line":148},[107,655,656],{"class":205},"    capture",[107,658,659],{"class":113}," (",[107,661,663],{"class":662},"s4XuR","eventType",[107,665,666],{"class":113},") {\n",[107,668,669],{"class":109,"line":158},[107,670,671],{"class":127},"        \u002F\u002F let's define our own function that can be called onPlay\u002FonPause\n",[107,673,674],{"class":109,"line":171},[107,675,676],{"class":127},"        \u002F\u002F this prevents duplicated code across the two methods\n",[107,678,679],{"class":109,"line":184},[107,680,145],{"emptyLinePlaceholder":144},[107,682,683],{"class":109,"line":190},[107,684,685],{"class":127},"        \u002F\u002F get the Video's DOM element\n",[107,687,688,691,695,698,701,704,707],{"class":109,"line":196},[107,689,690],{"class":161},"        const",[107,692,694],{"class":693},"sj4cs"," video",[107,696,697],{"class":161}," =",[107,699,700],{"class":693}," this",[107,702,703],{"class":113},".$refs[",[107,705,706],{"class":177},"'my-video'",[107,708,709],{"class":113},"]\n",[107,711,712],{"class":109,"line":202},[107,713,145],{"emptyLinePlaceholder":144},[107,715,716],{"class":109,"line":212},[107,717,718],{"class":127},"        \u002F\u002F send a msg to Node-RED using built-in \"send\" fcn\n",[107,720,721,724,726,729],{"class":109,"line":218},[107,722,723],{"class":693},"        this",[107,725,77],{"class":113},[107,727,728],{"class":205},"send",[107,730,731],{"class":113},"({\n",[107,733,734],{"class":109,"line":223},[107,735,736],{"class":127},"            \u002F\u002F specify which action is taking place\n",[107,738,739],{"class":109,"line":231},[107,740,741],{"class":113},"            event: eventType,\n",[107,743,744],{"class":109,"line":237},[107,745,746],{"class":127},"            \u002F\u002F use Vue's $refs to get the video's currentTime\n",[107,748,749],{"class":109,"line":243},[107,750,751],{"class":113},"            time: video.currentTime\n",[107,753,754],{"class":109,"line":249},[107,755,756],{"class":113},"        })\n",[107,758,759],{"class":109,"line":258},[107,760,761],{"class":113},"    },\n",[107,763,764,767],{"class":109,"line":263},[107,765,766],{"class":205},"    onPlay",[107,768,209],{"class":113},[107,770,771,773,775,778,781,784],{"class":109,"line":273},[107,772,723],{"class":693},[107,774,77],{"class":113},[107,776,777],{"class":205},"capture",[107,779,780],{"class":113},"(",[107,782,783],{"class":177},"'play'",[107,785,786],{"class":113},")\n",[107,788,789],{"class":109,"line":279},[107,790,761],{"class":113},[107,792,794,797],{"class":109,"line":793},23,[107,795,796],{"class":205},"    onPause",[107,798,209],{"class":113},[107,800,802,804,806,808,810,813],{"class":109,"line":801},24,[107,803,723],{"class":693},[107,805,77],{"class":113},[107,807,777],{"class":205},[107,809,780],{"class":113},[107,811,812],{"class":177},"'pause'",[107,814,786],{"class":113},[107,816,818],{"class":109,"line":817},25,[107,819,820],{"class":113},"    }\n",[107,822,824],{"class":109,"line":823},26,[107,825,240],{"class":113},[107,827,829],{"class":109,"line":828},27,[107,830,246],{"class":113},[107,832,834,836,838],{"class":109,"line":833},28,[107,835,134],{"class":113},[107,837,153],{"class":117},[107,839,121],{"class":113},[10,841,842,843,845,846,849],{},"With this functionality in place, we can wire the ",[19,844,21],{}," node to a ",[19,847,848],{},"debug"," node, and see the following when we play\u002Fpause the video:",[10,851,852],{},[580,853],{"alt":854,"src":855},"Example debug output when our custom build video player is played\u002Fpaused","\u002Fblog\u002F2023\u002F12\u002Fimages\u002Fdashboard-video-2.png",[592,857,859],{"id":858},"_2-remote-control-of-playpause-from-node-red","2. Remote control of play\u002Fpause from Node-RED",[10,861,862,863,865,866,868,869,872,873,605,876,879,880,883],{},"We can use the built-in ",[19,864,382],{}," variable to listen for incoming events from Node-RED. When Dashboard 2.0's nodes receive a ",[19,867,376],{}," inside Node-RED, they send a ",[19,870,871],{},"msg-input:\u003Cnode-id>"," event to the Dashboard client. We can listen for this event and then call the ",[19,874,875],{},"play()",[19,877,878],{},"pause()"," methods on the video element, depending on any properties of that message, in this case, the ",[19,881,882],{},"msg.payload.event"," value.",[98,885,887],{"className":100,"code":886,"language":102,"meta":103,"style":103},"\u003Cscript>\nexport default {\n  name: 'MyVideoPlayer',\n  methods: {\n    \u002F\u002F ...\n  },\n  mounted () {\n    \u002F\u002F listen for incoming msg's from Node-RED\n    \u002F\u002F note our topic is \"msg-input\" + the node's unique ID\n    this.$socket.on('msg-input:' + this.id, (msg) => {\n        \u002F\u002F get the Video's DOM element\n        const video = this.$refs['my-video']\n\n        \u002F\u002F if the event is \"play\", call the video's play() method\n        if (msg.payload?.event === 'play') {\n            video.play()\n        }\n\n        \u002F\u002F if the event is \"pause\", call the video's pause() method\n        if (msg.payload?.event === 'pause') {\n            video.pause()\n        }\n    })\n  },\n  unmounted () {\n    \u002F\u002F make sure we remove our listeners when the widget is destroyed\n    this.$socket.off(`msg-input:${this.id}`)\n  }\n}\n\u003C\u002Fscript>\n",[19,888,889,897,905,913,917,922,926,932,937,942,976,980,996,1000,1005,1021,1032,1037,1041,1046,1059,1068,1072,1077,1081,1087,1092,1117,1121,1126],{"__ignoreMap":103},[107,890,891,893,895],{"class":109,"line":110},[107,892,114],{"class":113},[107,894,153],{"class":117},[107,896,121],{"class":113},[107,898,899,901,903],{"class":109,"line":124},[107,900,162],{"class":161},[107,902,165],{"class":161},[107,904,168],{"class":113},[107,906,907,909,911],{"class":109,"line":131},[107,908,174],{"class":113},[107,910,645],{"class":177},[107,912,181],{"class":113},[107,914,915],{"class":109,"line":141},[107,916,187],{"class":113},[107,918,919],{"class":109,"line":148},[107,920,921],{"class":127},"    \u002F\u002F ...\n",[107,923,924],{"class":109,"line":158},[107,925,199],{"class":113},[107,927,928,930],{"class":109,"line":171},[107,929,206],{"class":205},[107,931,209],{"class":113},[107,933,934],{"class":109,"line":184},[107,935,936],{"class":127},"    \u002F\u002F listen for incoming msg's from Node-RED\n",[107,938,939],{"class":109,"line":190},[107,940,941],{"class":127},"    \u002F\u002F note our topic is \"msg-input\" + the node's unique ID\n",[107,943,944,947,950,953,955,958,961,963,966,968,971,974],{"class":109,"line":196},[107,945,946],{"class":693},"    this",[107,948,949],{"class":113},".$socket.",[107,951,952],{"class":205},"on",[107,954,780],{"class":113},[107,956,957],{"class":177},"'msg-input:'",[107,959,960],{"class":161}," +",[107,962,700],{"class":693},[107,964,965],{"class":113},".id, (",[107,967,376],{"class":662},[107,969,970],{"class":113},") ",[107,972,973],{"class":161},"=>",[107,975,168],{"class":113},[107,977,978],{"class":109,"line":202},[107,979,685],{"class":127},[107,981,982,984,986,988,990,992,994],{"class":109,"line":212},[107,983,690],{"class":161},[107,985,694],{"class":693},[107,987,697],{"class":161},[107,989,700],{"class":693},[107,991,703],{"class":113},[107,993,706],{"class":177},[107,995,709],{"class":113},[107,997,998],{"class":109,"line":218},[107,999,145],{"emptyLinePlaceholder":144},[107,1001,1002],{"class":109,"line":223},[107,1003,1004],{"class":127},"        \u002F\u002F if the event is \"play\", call the video's play() method\n",[107,1006,1007,1010,1013,1016,1019],{"class":109,"line":231},[107,1008,1009],{"class":161},"        if",[107,1011,1012],{"class":113}," (msg.payload?.event ",[107,1014,1015],{"class":161},"===",[107,1017,1018],{"class":177}," 'play'",[107,1020,666],{"class":113},[107,1022,1023,1026,1029],{"class":109,"line":237},[107,1024,1025],{"class":113},"            video.",[107,1027,1028],{"class":205},"play",[107,1030,1031],{"class":113},"()\n",[107,1033,1034],{"class":109,"line":243},[107,1035,1036],{"class":113},"        }\n",[107,1038,1039],{"class":109,"line":249},[107,1040,145],{"emptyLinePlaceholder":144},[107,1042,1043],{"class":109,"line":258},[107,1044,1045],{"class":127},"        \u002F\u002F if the event is \"pause\", call the video's pause() method\n",[107,1047,1048,1050,1052,1054,1057],{"class":109,"line":263},[107,1049,1009],{"class":161},[107,1051,1012],{"class":113},[107,1053,1015],{"class":161},[107,1055,1056],{"class":177}," 'pause'",[107,1058,666],{"class":113},[107,1060,1061,1063,1066],{"class":109,"line":273},[107,1062,1025],{"class":113},[107,1064,1065],{"class":205},"pause",[107,1067,1031],{"class":113},[107,1069,1070],{"class":109,"line":279},[107,1071,1036],{"class":113},[107,1073,1074],{"class":109,"line":793},[107,1075,1076],{"class":113},"    })\n",[107,1078,1079],{"class":109,"line":801},[107,1080,199],{"class":113},[107,1082,1083,1085],{"class":109,"line":817},[107,1084,226],{"class":205},[107,1086,209],{"class":113},[107,1088,1089],{"class":109,"line":823},[107,1090,1091],{"class":127},"    \u002F\u002F make sure we remove our listeners when the widget is destroyed\n",[107,1093,1094,1096,1098,1101,1103,1106,1108,1110,1112,1115],{"class":109,"line":828},[107,1095,946],{"class":693},[107,1097,949],{"class":113},[107,1099,1100],{"class":205},"off",[107,1102,780],{"class":113},[107,1104,1105],{"class":177},"`msg-input:${",[107,1107,344],{"class":693},[107,1109,77],{"class":177},[107,1111,370],{"class":113},[107,1113,1114],{"class":177},"}`",[107,1116,786],{"class":113},[107,1118,1119],{"class":109,"line":833},[107,1120,240],{"class":113},[107,1122,1124],{"class":109,"line":1123},29,[107,1125,246],{"class":113},[107,1127,1129,1131,1133],{"class":109,"line":1128},30,[107,1130,134],{"class":113},[107,1132,153],{"class":117},[107,1134,121],{"class":113},[592,1136,1138],{"id":1137},"_3-seeking-to-a-specific-point-in-the-video-from-within-node-red","3. Seeking to a specific point in the video from within Node-RED",[10,1140,1141,1142,1145],{},"With the ",[19,1143,1144],{},"on('msg-input')"," listener in place, we can now extend our handler to handle seeking to a specific point in the video.",[98,1147,1149],{"className":100,"code":1148,"language":102,"meta":103,"style":103},"\u003Cscript>\nexport default {\n  name: 'MyVideoPlayer',\n  methods: {\n    \u002F\u002F ...\n  },\n  mounted () {\n    \u002F\u002F ...\n    this.$socket.on('msg-input:' + this.id, (msg) => {\n        \u002F\u002F ... other handlers\n\n        \u002F\u002F if the event is \"seek\", call the video's currentTime() method\n        if (msg.payload?.event === 'seek') {\n            video.currentTime = msg.payload.currentTime\n        }\n    })\n  },\n  unmounted () {\n    \u002F\u002F ...\n  }\n}\n\u003C\u002Fscript>\n",[19,1150,1151,1159,1167,1175,1179,1183,1187,1193,1197,1223,1228,1232,1237,1250,1260,1264,1268,1272,1278,1282,1286,1290],{"__ignoreMap":103},[107,1152,1153,1155,1157],{"class":109,"line":110},[107,1154,114],{"class":113},[107,1156,153],{"class":117},[107,1158,121],{"class":113},[107,1160,1161,1163,1165],{"class":109,"line":124},[107,1162,162],{"class":161},[107,1164,165],{"class":161},[107,1166,168],{"class":113},[107,1168,1169,1171,1173],{"class":109,"line":131},[107,1170,174],{"class":113},[107,1172,645],{"class":177},[107,1174,181],{"class":113},[107,1176,1177],{"class":109,"line":141},[107,1178,187],{"class":113},[107,1180,1181],{"class":109,"line":148},[107,1182,921],{"class":127},[107,1184,1185],{"class":109,"line":158},[107,1186,199],{"class":113},[107,1188,1189,1191],{"class":109,"line":171},[107,1190,206],{"class":205},[107,1192,209],{"class":113},[107,1194,1195],{"class":109,"line":184},[107,1196,921],{"class":127},[107,1198,1199,1201,1203,1205,1207,1209,1211,1213,1215,1217,1219,1221],{"class":109,"line":190},[107,1200,946],{"class":693},[107,1202,949],{"class":113},[107,1204,952],{"class":205},[107,1206,780],{"class":113},[107,1208,957],{"class":177},[107,1210,960],{"class":161},[107,1212,700],{"class":693},[107,1214,965],{"class":113},[107,1216,376],{"class":662},[107,1218,970],{"class":113},[107,1220,973],{"class":161},[107,1222,168],{"class":113},[107,1224,1225],{"class":109,"line":196},[107,1226,1227],{"class":127},"        \u002F\u002F ... other handlers\n",[107,1229,1230],{"class":109,"line":202},[107,1231,145],{"emptyLinePlaceholder":144},[107,1233,1234],{"class":109,"line":212},[107,1235,1236],{"class":127},"        \u002F\u002F if the event is \"seek\", call the video's currentTime() method\n",[107,1238,1239,1241,1243,1245,1248],{"class":109,"line":218},[107,1240,1009],{"class":161},[107,1242,1012],{"class":113},[107,1244,1015],{"class":161},[107,1246,1247],{"class":177}," 'seek'",[107,1249,666],{"class":113},[107,1251,1252,1255,1257],{"class":109,"line":223},[107,1253,1254],{"class":113},"            video.currentTime ",[107,1256,441],{"class":161},[107,1258,1259],{"class":113}," msg.payload.currentTime\n",[107,1261,1262],{"class":109,"line":231},[107,1263,1036],{"class":113},[107,1265,1266],{"class":109,"line":237},[107,1267,1076],{"class":113},[107,1269,1270],{"class":109,"line":243},[107,1271,199],{"class":113},[107,1273,1274,1276],{"class":109,"line":249},[107,1275,226],{"class":205},[107,1277,209],{"class":113},[107,1279,1280],{"class":109,"line":258},[107,1281,921],{"class":127},[107,1283,1284],{"class":109,"line":263},[107,1285,240],{"class":113},[107,1287,1288],{"class":109,"line":273},[107,1289,246],{"class":113},[107,1291,1292,1294,1296],{"class":109,"line":279},[107,1293,134],{"class":113},[107,1295,153],{"class":117},[107,1297,121],{"class":113},[10,1299,1300],{},"and with that, we now have a Dashboard 2.0 widget to display a video, that can be controlled from Node-RED, and logs details of user activity back into Node-RED.",[10,1302,1303],{},"Other features available with the UI Template are detailed in the online documentation, and include:",[24,1305,1306,1313],{},[27,1307,1308],{},[90,1309,1312],{"href":1310,"rel":1311},"https:\u002F\u002Fdashboard.flowfuse.com\u002Fnodes\u002Fwidgets\u002Fui-template.html#loading-external-dependencies",[94],"Loading External Dependencies",[27,1314,1315],{},[90,1316,1319],{"href":1317,"rel":1318},"https:\u002F\u002Fdashboard.flowfuse.com\u002Fnodes\u002Fwidgets\u002Fui-template.html#writing-raw-javascript",[94],"Running raw JavaScript",[79,1321,1323],{"id":1322},"follow-our-progress","Follow our Progress",[10,1325,1326,1327,1330],{},"You can also read the more comprehensive release notes for ",[19,1328,1329],{},"v0.10.0"," release here:",[24,1332,1333],{},[27,1334,1335],{},[90,1336,1339],{"href":1337,"rel":1338},"https:\u002F\u002Fgithub.com\u002FFlowFuse\u002Fnode-red-dashboard\u002Freleases\u002Ftag\u002Fv0.10.0",[94],"0.10.0 Release Notes",[10,1341,1342,1343,77],{},"As always, thanks for reading and your interest in Dashboard 2.0. If you have any feature requests, bugs\u002Fcomplaints or general feedback, please do reach out, and raise issues on our relevant ",[90,1344,1347],{"href":1345,"rel":1346},"https:\u002F\u002Fgithub.com\u002FFlowFuse\u002Fnode-red-dashboard",[94],"GitHub repository",[24,1349,1350,1357],{},[27,1351,1352],{},[90,1353,1356],{"href":1354,"rel":1355},"https:\u002F\u002Fgithub.com\u002Forgs\u002FFlowFuse\u002Fprojects\u002F15\u002Fviews\u002F1",[94],"Dashboard 2.0 Activity Tracker",[27,1358,1359],{},[90,1360,1363],{"href":1361,"rel":1362},"https:\u002F\u002Fgithub.com\u002Forgs\u002FFlowFuse\u002Fprojects\u002F15\u002Fviews\u002F4",[94],"Dashboard 2.0 Planning Board",[268,1365,1366],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":103,"searchDepth":124,"depth":124,"links":1368},[1369,1372,1376],{"id":81,"depth":124,"text":82,"children":1370},[1371],{"id":348,"depth":131,"text":349},{"id":406,"depth":124,"text":407,"children":1373},[1374,1375],{"id":410,"depth":131,"text":411},{"id":586,"depth":131,"text":587},{"id":1322,"depth":124,"text":1323},"Dashboard 2.0 just got a lot more powerful with our new updates to the ui-template node. New features added to the node include:","md",{"navTitle":5,"excerpt":1380},{"type":7,"value":1381},[1382,1388],[10,1383,12,1384,17,1386,22],{},[14,1385,16],{},[19,1387,21],{},[24,1389,1390,1392,1396],{},[27,1391,29],{},[27,1393,32,1394,36],{},[19,1395,35],{},[27,1397,39,1398,36],{},[19,1399,35],{},"\u002Fblog\u002F2023\u002F12\u002Fdashboard-0-10-0",{"title":5,"description":1377},"blog\u002F2023\u002F12\u002Fdashboard-0-10-0","UPS_eUaHVxGlk2alpsegjSG4EanjJCejmThXIWbBsM0",1780070551171]