@xibosignage/xibo-layout-renderer 1.0.28 → 1.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,12 +21,13 @@ export default class ActionController {
21
21
  $actionControllerTitle: HTMLElement | null;
22
22
  $actionsContainer: HTMLElement | null;
23
23
  translations: any;
24
+ private keyboardHandler;
24
25
  constructor(parent: ILayout, actions: Action[], options: InactOptions);
25
26
  init(): void;
26
27
  openLayoutInNewTab(layoutCode: string, options: InactOptions): void;
27
28
  openLayoutInPlayer(layoutCode: string, _options: InactOptions): void;
28
29
  prevOrNextLayout(targetId: string, actionType: string): void;
29
- /** Change media in region (next/previous) */
30
+ /** Change media in region (next/previous) with wrap-around at boundaries. */
30
31
  gotoMediaInRegion(regionId: string, actionType: string): void;
31
32
  loadMediaInRegion(regionId: string, widgetId: string): void;
32
33
  /** Run action based on action data */
@@ -37,4 +38,6 @@ export default class ActionController {
37
38
  /** Dispatch an incoming webhook trigger to any matching actions on this layout. */
38
39
  handleWebhookTrigger(triggerCode: string, widgetId?: string): void;
39
40
  initKeyboardActions(): void;
41
+ /** Remove the keydown listener registered by initKeyboardActions. Call when the layout ends or is cancelled. */
42
+ removeKeyboardActions(): void;
40
43
  }
@@ -73576,7 +73576,7 @@ function createMediaElement(mediaObject) {
73576
73576
  cssText += "\n visibility: hidden;\n opacity: 0;\n z-index: 0;\n ";
73577
73577
  }
73578
73578
  $media.style.cssText = cssText;
73579
- if (self.render === 'html' || self.mediaType === 'ticker' || self.mediaType === 'webpage') {
73579
+ if (self.mediaType !== 'spacer' && (self.render === 'html' || self.mediaType === 'ticker' || self.mediaType === 'webpage')) {
73580
73580
  self.checkIframeStatus = true;
73581
73581
  self.iframe = prepareIframe(self);
73582
73582
  } else if (self.mediaType === "image") {
@@ -73724,8 +73724,6 @@ function prepareAudioMedia(media, region) {
73724
73724
  region.html.appendChild(media.html);
73725
73725
  }
73726
73726
  function prepareHtmlMedia(media, region) {
73727
- // Set state as false ( for now )
73728
- media.ready = false;
73729
73727
  if (media.html) {
73730
73728
  var mediaId = getMediaId(media);
73731
73729
  // Clean up old copy of the media
@@ -73737,14 +73735,16 @@ function prepareHtmlMedia(media, region) {
73737
73735
  mediaId: mediaId,
73738
73736
  mediaInRegion: mediaInRegion
73739
73737
  });
73740
- // Append iframe
73741
- media.html.innerHTML = '';
73742
- media.html.appendChild(media.iframe);
73743
73738
  if (!mediaInRegion) {
73744
- // Add fresh copy of the media into the region using the direct reference
73739
+ // Append iframe and insert into region only when not already in the DOM.
73740
+ // Calling innerHTML = '' when the element is already present detaches the
73741
+ // iframe, causing the browser to reload its src unnecessarily (e.g. when
73742
+ // a media was preloaded then skipped by a navigation action).
73743
+ media.html.innerHTML = '';
73744
+ media.html.appendChild(media.iframe);
73745
73745
  region.html.appendChild(media.html);
73746
- media.ready = true;
73747
73746
  }
73747
+ media.ready = true;
73748
73748
  }
73749
73749
  }
73750
73750
  exports.FaultCodes = void 0;
@@ -74052,7 +74052,12 @@ var Media = /*#__PURE__*/function () {
74052
74052
  var _this$sspImpressionUr, _this$sspErrorUrls;
74053
74053
  _this.xlr.emitter.emit('sspWidgetEnd', (_this$sspImpressionUr = _this.sspImpressionUrls) !== null && _this$sspImpressionUr !== void 0 ? _this$sspImpressionUr : [], (_this$sspErrorUrls = _this.sspErrorUrls) !== null && _this$sspErrorUrls !== void 0 ? _this$sspErrorUrls : [], _this.sspImpressionUrls ? _this.duration : 0);
74054
74054
  }
74055
- media.region.playNextMedia();
74055
+ // Only advance the region if this media is still the active one.
74056
+ // A user-triggered next/prev action may have already moved currMedia
74057
+ // on, in which case the timer firing here would cause a double-advance.
74058
+ if (media === media.region.currMedia) {
74059
+ media.region.playNextMedia();
74060
+ }
74056
74061
  });
74057
74062
  this.on('cancelled', function (media) {
74058
74063
  if (media.state === MediaState.CANCELLED) return;
@@ -74096,6 +74101,10 @@ var Media = /*#__PURE__*/function () {
74096
74101
  key: "startMediaTimer",
74097
74102
  value: function startMediaTimer(media) {
74098
74103
  var _this2 = this;
74104
+ // Always reset the counter so a media replayed after cancellation runs
74105
+ // for its full duration rather than the residual time left from the
74106
+ // previous play.
74107
+ this.mediaTimeCount = 0;
74099
74108
  var preloadTimeMs = 2000;
74100
74109
  var preloadTimeBufferMs = media.duration * 1000 / 2 - preloadTimeMs;
74101
74110
  var isPreparingNextMedia = false;
@@ -74971,11 +74980,19 @@ var Region = /*#__PURE__*/function () {
74971
74980
  nxtMedia: (_this$nxtMedia2 = this.nxtMedia) === null || _this$nxtMedia2 === void 0 ? void 0 : _this$nxtMedia2.containerName
74972
74981
  });
74973
74982
  if (!this.layout.isOverlay && crossedEnd) {
74983
+ var _this$currMedia8;
74974
74984
  this.finished();
74975
74985
  if (this.layout.allEnded) {
74976
74986
  console.debug('??? XLR.debug >> Region - playNextMedia - layout all ended');
74977
74987
  return;
74978
74988
  }
74989
+ // Freeze single-media HTML at its last state while waiting for other
74990
+ // regions to complete. The guard at the top only catches the second
74991
+ // call; this one catches the first completion when complete was just
74992
+ // set by finished() above.
74993
+ if (((_this$currMedia8 = this.currMedia) === null || _this$currMedia8 === void 0 ? void 0 : _this$currMedia8.render) === 'html' && this.totalMediaObjects === 1 && this.oldMedia === this.currMedia) {
74994
+ return;
74995
+ }
74979
74996
  }
74980
74997
  this.transitionNodes(this.oldMedia, this.currMedia);
74981
74998
  }
@@ -74985,11 +75002,18 @@ var Region = /*#__PURE__*/function () {
74985
75002
  if (this.currentMediaIndex <= 0 || this.ended) {
74986
75003
  return;
74987
75004
  }
75005
+ var interruptedMedia = this.currMedia;
74988
75006
  this.oldMedia = this.currMedia;
74989
75007
  this.currentMediaIndex -= 1;
74990
75008
  this.currMedia = this.mediaObjects[this.currentMediaIndex];
74991
75009
  this.nxtMedia = this.mediaObjects[(this.currentMediaIndex + 1) % this.totalMediaObjects];
74992
75010
  this.complete = false;
75011
+ // Cancel the interrupted media after advancing currMedia, using the same
75012
+ // pattern as gotoMediaInRegion — emitting after the update ensures the
75013
+ // handler's (media === currMedia) guard correctly skips playNextMedia.
75014
+ if ((interruptedMedia === null || interruptedMedia === void 0 ? void 0 : interruptedMedia.state) === MediaState.PLAYING) {
75015
+ interruptedMedia.emitter.emit('cancelled', interruptedMedia);
75016
+ }
74993
75017
  console.debug('region::playPreviousMedia', this);
74994
75018
  this.transitionNodes(this.oldMedia, this.currMedia);
74995
75019
  }
@@ -75067,6 +75091,7 @@ var ActionController = /*#__PURE__*/function () {
75067
75091
  _defineProperty(this, "$actionControllerTitle", void 0);
75068
75092
  _defineProperty(this, "$actionsContainer", void 0);
75069
75093
  _defineProperty(this, "translations", {});
75094
+ _defineProperty(this, "keyboardHandler", null);
75070
75095
  this.parent = parent;
75071
75096
  this.actions = actions;
75072
75097
  this.options = options;
@@ -75231,7 +75256,7 @@ var ActionController = /*#__PURE__*/function () {
75231
75256
  this.parent.xlr.gotoPrevLayout();
75232
75257
  }
75233
75258
  }
75234
- /** Change media in region (next/previous) */
75259
+ /** Change media in region (next/previous) with wrap-around at boundaries. */
75235
75260
  }, {
75236
75261
  key: "gotoMediaInRegion",
75237
75262
  value: function gotoMediaInRegion(regionId, actionType) {
@@ -75239,15 +75264,34 @@ var ActionController = /*#__PURE__*/function () {
75239
75264
  regionId: regionId,
75240
75265
  actionType: actionType
75241
75266
  });
75242
- // Find target region
75243
75267
  this.parent.regions.forEach(function (regionObj) {
75244
- if (regionObj.id === regionId) {
75245
- if (actionType === 'next') {
75246
- regionObj.playNextMedia();
75247
- } else {
75248
- regionObj.playPreviousMedia();
75249
- }
75268
+ if (regionObj.id !== regionId || regionObj.ended) return;
75269
+ var total = regionObj.totalMediaObjects;
75270
+ if (total === 0) return;
75271
+ // Snapshot the currently-playing media before updating currMedia so
75272
+ // we can cancel it cleanly after the region state is advanced.
75273
+ var interruptedMedia = regionObj.currMedia;
75274
+ // Compute new index with wrap-around. We do NOT delegate to
75275
+ // playNextMedia() / playPreviousMedia() here because those carry
75276
+ // normal playlist-cycle semantics (finished(), regionExpired()) that
75277
+ // must not fire during user-driven navigation.
75278
+ var newIndex = actionType === 'next' ? (regionObj.currentMediaIndex + 1) % total : (regionObj.currentMediaIndex - 1 + total) % total;
75279
+ regionObj.oldMedia = regionObj.currMedia;
75280
+ regionObj.currentMediaIndex = newIndex;
75281
+ regionObj.currMedia = regionObj.mediaObjects[newIndex];
75282
+ regionObj.nxtMedia = regionObj.mediaObjects[(newIndex + 1) % total];
75283
+ regionObj.complete = false;
75284
+ // Properly cancel the interrupted media AFTER updating currMedia.
75285
+ // Using 'cancelled' rather than bare clearInterval ensures state is
75286
+ // reset from PLAYING and mediaTimeCount is zeroed — without this,
75287
+ // returning to a cancelled media causes run() → 'start' to bail on
75288
+ // the state === PLAYING guard, leaving the region stuck indefinitely.
75289
+ // currMedia is already updated so the handler's guard
75290
+ // (media === media.region.currMedia) correctly skips playNextMedia.
75291
+ if ((interruptedMedia === null || interruptedMedia === void 0 ? void 0 : interruptedMedia.state) === MediaState.PLAYING) {
75292
+ interruptedMedia.emitter.emit('cancelled', interruptedMedia);
75250
75293
  }
75294
+ regionObj.transitionNodes(regionObj.oldMedia, regionObj.currMedia);
75251
75295
  });
75252
75296
  }
75253
75297
  }, {
@@ -75298,11 +75342,19 @@ var ActionController = /*#__PURE__*/function () {
75298
75342
  console.debug('[ActionController::loadMediaInRegion] Target media already queued, skipping duplicate insertion');
75299
75343
  return;
75300
75344
  }
75301
- // Cancel the current media's duration timer so it doesn't fire and interrupt
75302
- // the target widget mid-playback (e.g. an Interactive Zone timer still ticking).
75303
- if ((_targetRegion2 = targetRegion) !== null && _targetRegion2 !== void 0 && (_targetRegion2 = _targetRegion2.currMedia) !== null && _targetRegion2 !== void 0 && _targetRegion2.mediaTimer) {
75304
- clearInterval(targetRegion.currMedia.mediaTimer);
75305
- targetRegion.currMedia.mediaTimer = undefined;
75345
+ // Cancel the interrupted media so it doesn't double-advance when the playlist
75346
+ // returns to it. currMedia has not been advanced yet at this point (playNextMedia
75347
+ // does that below), so we cannot use emitter.emit('cancelled') the handler's
75348
+ // currMedia guard would fire and call playNextMedia a second time. Instead we
75349
+ // cancel directly: clear the timer and reset state so the 'start' handler does
75350
+ // not bail on the state === PLAYING guard when this media is replayed.
75351
+ if (((_targetRegion2 = targetRegion) === null || _targetRegion2 === void 0 || (_targetRegion2 = _targetRegion2.currMedia) === null || _targetRegion2 === void 0 ? void 0 : _targetRegion2.state) === MediaState.PLAYING) {
75352
+ var interruptedMedia = targetRegion.currMedia;
75353
+ if (interruptedMedia.mediaTimer) {
75354
+ clearInterval(interruptedMedia.mediaTimer);
75355
+ interruptedMedia.mediaTimer = undefined;
75356
+ }
75357
+ interruptedMedia.state = MediaState.CANCELLED;
75306
75358
  }
75307
75359
  // Reset complete so the HTML-media guard in playNextMedia() doesn't block
75308
75360
  // the transition (that guard is for single-media loops, not navWidget injections).
@@ -75331,6 +75383,12 @@ var ActionController = /*#__PURE__*/function () {
75331
75383
  }, {
75332
75384
  key: "runAction",
75333
75385
  value: function runAction(actionData, options) {
75386
+ // If this layout is no longer active (being cancelled or navigated away from),
75387
+ // discard the action so it doesn't interfere with the outgoing transition.
75388
+ // inLoop is set to false synchronously before finishAllRegions() in all nav paths.
75389
+ if (!this.parent.inLoop) {
75390
+ return;
75391
+ }
75334
75392
  console.debug('[ActionController::runAction] Triggering action', {
75335
75393
  actionData: actionData
75336
75394
  });
@@ -75418,31 +75476,37 @@ var ActionController = /*#__PURE__*/function () {
75418
75476
  key: "initKeyboardActions",
75419
75477
  value: function initKeyboardActions() {
75420
75478
  var self = this;
75421
- // Store actions in a map
75422
75479
  var keyActions = new Map();
75423
- this.$actionController.querySelectorAll('.action[triggerType="keyPress"]').forEach(function ($el) {
75480
+ this.$actionController.querySelectorAll('.action[triggertype="keyPress"]').forEach(function ($el) {
75424
75481
  var dataset = $el.dataset;
75425
75482
  var code = dataset.triggercode;
75426
75483
  if (code) {
75427
- // Create an empty array, if not yet set
75428
75484
  if (!keyActions.get(code)) {
75429
75485
  keyActions.set(code, []);
75430
75486
  }
75431
- // Add new action to array
75432
75487
  keyActions.get(code).push(dataset);
75433
75488
  }
75434
75489
  });
75435
- // Keyboard listener
75436
- document.addEventListener('keydown', function (ev) {
75490
+ // Nothing to do if this layout has no keyboard-triggered actions.
75491
+ if (keyActions.size === 0) return;
75492
+ this.keyboardHandler = function (ev) {
75437
75493
  var actions = keyActions.get(ev.code);
75438
- // Are there action for this key code?
75439
75494
  if (actions) {
75440
- // Run all actions associated with it
75441
75495
  actions.forEach(function (dataset) {
75442
75496
  self.runAction(dataset, self.options);
75443
75497
  });
75444
75498
  }
75445
- });
75499
+ };
75500
+ document.addEventListener('keydown', this.keyboardHandler);
75501
+ }
75502
+ /** Remove the keydown listener registered by initKeyboardActions. Call when the layout ends or is cancelled. */
75503
+ }, {
75504
+ key: "removeKeyboardActions",
75505
+ value: function removeKeyboardActions() {
75506
+ if (this.keyboardHandler) {
75507
+ document.removeEventListener('keydown', this.keyboardHandler);
75508
+ this.keyboardHandler = null;
75509
+ }
75446
75510
  }
75447
75511
  }]);
75448
75512
  }();
@@ -75677,6 +75741,7 @@ var Layout = /*#__PURE__*/function () {
75677
75741
  });
75678
75742
  this.on('end', /*#__PURE__*/function () {
75679
75743
  var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(layout) {
75744
+ var _layout$actionControl;
75680
75745
  var $layout, _$layout$parentElemen;
75681
75746
  return _regeneratorRuntime().wrap(function _callee2$(_context2) {
75682
75747
  while (1) switch (_context2.prev = _context2.next) {
@@ -75735,9 +75800,10 @@ var Layout = /*#__PURE__*/function () {
75735
75800
  }
75736
75801
  // Emit layout end event
75737
75802
  console.debug('>>>>> XLR.debug Awaited XLR::emitSync > End - Calling layoutEnd event');
75738
- _context2.next = 13;
75803
+ (_layout$actionControl = layout.actionController) === null || _layout$actionControl === void 0 || _layout$actionControl.removeKeyboardActions();
75804
+ _context2.next = 14;
75739
75805
  return layout.xlr.emitSync('layoutEnd', layout);
75740
- case 13:
75806
+ case 14:
75741
75807
  if (_this.xlr.config.platform !== exports.ConsumerPlatform.CMS && layout.inLoop) {
75742
75808
  // Transition next layout to current layout and prepare next layout if exist
75743
75809
  _this.xlr.prepareLayouts().then( /*#__PURE__*/function () {
@@ -75767,7 +75833,7 @@ var Layout = /*#__PURE__*/function () {
75767
75833
  };
75768
75834
  }());
75769
75835
  }
75770
- case 14:
75836
+ case 15:
75771
75837
  case "end":
75772
75838
  return _context2.stop();
75773
75839
  }
@@ -75778,9 +75844,11 @@ var Layout = /*#__PURE__*/function () {
75778
75844
  };
75779
75845
  }());
75780
75846
  this.on('cancelled', function (layout) {
75847
+ var _layout$actionControl2;
75781
75848
  console.debug('>>>>> XLR.debug / Layout cancelled > Layout ID > ', layout.id);
75782
75849
  layout.state = exports.ELayoutState.CANCELLED;
75783
75850
  layout.inLoop = false;
75851
+ (_layout$actionControl2 = layout.actionController) === null || _layout$actionControl2 === void 0 || _layout$actionControl2.removeKeyboardActions();
75784
75852
  // Dispose video handlers immediately so their stall watchdogs and error
75785
75853
  // callbacks can't fire against a layout whose DOM is about to be removed.
75786
75854
  var _iterator = _createForOfIteratorHelper(layout.regions),
@@ -75939,7 +76007,8 @@ var Layout = /*#__PURE__*/function () {
75939
76007
  _this2.regions.push(regionObj);
75940
76008
  });
75941
76009
  this.actionController.initTouchActions();
75942
- this.actionController.initKeyboardActions();
76010
+ // Keyboard actions are registered in run() so the global document listener
76011
+ // is only active while the layout is actually playing, not during background preparation.
75943
76012
  }
75944
76013
  }, {
75945
76014
  key: "run",
@@ -75958,6 +76027,7 @@ var Layout = /*#__PURE__*/function () {
75958
76027
  shouldParse: false
75959
76028
  });
75960
76029
  if ($layoutContainer) {
76030
+ var _this$actionControlle;
75961
76031
  $layoutContainer.style.setProperty('visibility', 'visible');
75962
76032
  $layoutContainer.style.setProperty('opacity', '1');
75963
76033
  $layoutContainer.style.setProperty('z-index', this.zIndex !== null ? "".concat(this.zIndex) : '1');
@@ -75966,6 +76036,9 @@ var Layout = /*#__PURE__*/function () {
75966
76036
  // Also set the background color of the player window > body
75967
76037
  document.body.style.setProperty('background-color', "".concat(this.bgColor));
75968
76038
  }
76039
+ // Register keyboard actions now that the layout is active.
76040
+ // Done here (not in parseXlf) so the global listener is scoped to playback time.
76041
+ (_this$actionControlle = this.actionController) === null || _this$actionControlle === void 0 || _this$actionControlle.initKeyboardActions();
75969
76042
  // Emit start event
75970
76043
  this.emitter.emit('start', this);
75971
76044
  // Play regions