@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.
@@ -72,12 +72,13 @@ declare class ActionController {
72
72
  $actionControllerTitle: HTMLElement | null;
73
73
  $actionsContainer: HTMLElement | null;
74
74
  translations: any;
75
+ private keyboardHandler;
75
76
  constructor(parent: ILayout, actions: Action[], options: InactOptions);
76
77
  init(): void;
77
78
  openLayoutInNewTab(layoutCode: string, options: InactOptions): void;
78
79
  openLayoutInPlayer(layoutCode: string, _options: InactOptions): void;
79
80
  prevOrNextLayout(targetId: string, actionType: string): void;
80
- /** Change media in region (next/previous) */
81
+ /** Change media in region (next/previous) with wrap-around at boundaries. */
81
82
  gotoMediaInRegion(regionId: string, actionType: string): void;
82
83
  loadMediaInRegion(regionId: string, widgetId: string): void;
83
84
  /** Run action based on action data */
@@ -88,6 +89,8 @@ declare class ActionController {
88
89
  /** Dispatch an incoming webhook trigger to any matching actions on this layout. */
89
90
  handleWebhookTrigger(triggerCode: string, widgetId?: string): void;
90
91
  initKeyboardActions(): void;
92
+ /** Remove the keydown listener registered by initKeyboardActions. Call when the layout ends or is cancelled. */
93
+ removeKeyboardActions(): void;
91
94
  }
92
95
 
93
96
  declare function initRenderingDOM(targetContainer: Element | null): void;
@@ -73572,7 +73572,7 @@ function createMediaElement(mediaObject) {
73572
73572
  cssText += "\n visibility: hidden;\n opacity: 0;\n z-index: 0;\n ";
73573
73573
  }
73574
73574
  $media.style.cssText = cssText;
73575
- if (self.render === 'html' || self.mediaType === 'ticker' || self.mediaType === 'webpage') {
73575
+ if (self.mediaType !== 'spacer' && (self.render === 'html' || self.mediaType === 'ticker' || self.mediaType === 'webpage')) {
73576
73576
  self.checkIframeStatus = true;
73577
73577
  self.iframe = prepareIframe(self);
73578
73578
  } else if (self.mediaType === "image") {
@@ -73720,8 +73720,6 @@ function prepareAudioMedia(media, region) {
73720
73720
  region.html.appendChild(media.html);
73721
73721
  }
73722
73722
  function prepareHtmlMedia(media, region) {
73723
- // Set state as false ( for now )
73724
- media.ready = false;
73725
73723
  if (media.html) {
73726
73724
  var mediaId = getMediaId(media);
73727
73725
  // Clean up old copy of the media
@@ -73733,14 +73731,16 @@ function prepareHtmlMedia(media, region) {
73733
73731
  mediaId: mediaId,
73734
73732
  mediaInRegion: mediaInRegion
73735
73733
  });
73736
- // Append iframe
73737
- media.html.innerHTML = '';
73738
- media.html.appendChild(media.iframe);
73739
73734
  if (!mediaInRegion) {
73740
- // Add fresh copy of the media into the region using the direct reference
73735
+ // Append iframe and insert into region only when not already in the DOM.
73736
+ // Calling innerHTML = '' when the element is already present detaches the
73737
+ // iframe, causing the browser to reload its src unnecessarily (e.g. when
73738
+ // a media was preloaded then skipped by a navigation action).
73739
+ media.html.innerHTML = '';
73740
+ media.html.appendChild(media.iframe);
73741
73741
  region.html.appendChild(media.html);
73742
- media.ready = true;
73743
73742
  }
73743
+ media.ready = true;
73744
73744
  }
73745
73745
  }
73746
73746
  var FaultCodes;
@@ -74048,7 +74048,12 @@ var Media = /*#__PURE__*/function () {
74048
74048
  var _this$sspImpressionUr, _this$sspErrorUrls;
74049
74049
  _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);
74050
74050
  }
74051
- media.region.playNextMedia();
74051
+ // Only advance the region if this media is still the active one.
74052
+ // A user-triggered next/prev action may have already moved currMedia
74053
+ // on, in which case the timer firing here would cause a double-advance.
74054
+ if (media === media.region.currMedia) {
74055
+ media.region.playNextMedia();
74056
+ }
74052
74057
  });
74053
74058
  this.on('cancelled', function (media) {
74054
74059
  if (media.state === MediaState.CANCELLED) return;
@@ -74092,6 +74097,10 @@ var Media = /*#__PURE__*/function () {
74092
74097
  key: "startMediaTimer",
74093
74098
  value: function startMediaTimer(media) {
74094
74099
  var _this2 = this;
74100
+ // Always reset the counter so a media replayed after cancellation runs
74101
+ // for its full duration rather than the residual time left from the
74102
+ // previous play.
74103
+ this.mediaTimeCount = 0;
74095
74104
  var preloadTimeMs = 2000;
74096
74105
  var preloadTimeBufferMs = media.duration * 1000 / 2 - preloadTimeMs;
74097
74106
  var isPreparingNextMedia = false;
@@ -74967,11 +74976,19 @@ var Region = /*#__PURE__*/function () {
74967
74976
  nxtMedia: (_this$nxtMedia2 = this.nxtMedia) === null || _this$nxtMedia2 === void 0 ? void 0 : _this$nxtMedia2.containerName
74968
74977
  });
74969
74978
  if (!this.layout.isOverlay && crossedEnd) {
74979
+ var _this$currMedia8;
74970
74980
  this.finished();
74971
74981
  if (this.layout.allEnded) {
74972
74982
  console.debug('??? XLR.debug >> Region - playNextMedia - layout all ended');
74973
74983
  return;
74974
74984
  }
74985
+ // Freeze single-media HTML at its last state while waiting for other
74986
+ // regions to complete. The guard at the top only catches the second
74987
+ // call; this one catches the first completion when complete was just
74988
+ // set by finished() above.
74989
+ if (((_this$currMedia8 = this.currMedia) === null || _this$currMedia8 === void 0 ? void 0 : _this$currMedia8.render) === 'html' && this.totalMediaObjects === 1 && this.oldMedia === this.currMedia) {
74990
+ return;
74991
+ }
74975
74992
  }
74976
74993
  this.transitionNodes(this.oldMedia, this.currMedia);
74977
74994
  }
@@ -74981,11 +74998,18 @@ var Region = /*#__PURE__*/function () {
74981
74998
  if (this.currentMediaIndex <= 0 || this.ended) {
74982
74999
  return;
74983
75000
  }
75001
+ var interruptedMedia = this.currMedia;
74984
75002
  this.oldMedia = this.currMedia;
74985
75003
  this.currentMediaIndex -= 1;
74986
75004
  this.currMedia = this.mediaObjects[this.currentMediaIndex];
74987
75005
  this.nxtMedia = this.mediaObjects[(this.currentMediaIndex + 1) % this.totalMediaObjects];
74988
75006
  this.complete = false;
75007
+ // Cancel the interrupted media after advancing currMedia, using the same
75008
+ // pattern as gotoMediaInRegion — emitting after the update ensures the
75009
+ // handler's (media === currMedia) guard correctly skips playNextMedia.
75010
+ if ((interruptedMedia === null || interruptedMedia === void 0 ? void 0 : interruptedMedia.state) === MediaState.PLAYING) {
75011
+ interruptedMedia.emitter.emit('cancelled', interruptedMedia);
75012
+ }
74989
75013
  console.debug('region::playPreviousMedia', this);
74990
75014
  this.transitionNodes(this.oldMedia, this.currMedia);
74991
75015
  }
@@ -75063,6 +75087,7 @@ var ActionController = /*#__PURE__*/function () {
75063
75087
  _defineProperty(this, "$actionControllerTitle", void 0);
75064
75088
  _defineProperty(this, "$actionsContainer", void 0);
75065
75089
  _defineProperty(this, "translations", {});
75090
+ _defineProperty(this, "keyboardHandler", null);
75066
75091
  this.parent = parent;
75067
75092
  this.actions = actions;
75068
75093
  this.options = options;
@@ -75227,7 +75252,7 @@ var ActionController = /*#__PURE__*/function () {
75227
75252
  this.parent.xlr.gotoPrevLayout();
75228
75253
  }
75229
75254
  }
75230
- /** Change media in region (next/previous) */
75255
+ /** Change media in region (next/previous) with wrap-around at boundaries. */
75231
75256
  }, {
75232
75257
  key: "gotoMediaInRegion",
75233
75258
  value: function gotoMediaInRegion(regionId, actionType) {
@@ -75235,15 +75260,34 @@ var ActionController = /*#__PURE__*/function () {
75235
75260
  regionId: regionId,
75236
75261
  actionType: actionType
75237
75262
  });
75238
- // Find target region
75239
75263
  this.parent.regions.forEach(function (regionObj) {
75240
- if (regionObj.id === regionId) {
75241
- if (actionType === 'next') {
75242
- regionObj.playNextMedia();
75243
- } else {
75244
- regionObj.playPreviousMedia();
75245
- }
75264
+ if (regionObj.id !== regionId || regionObj.ended) return;
75265
+ var total = regionObj.totalMediaObjects;
75266
+ if (total === 0) return;
75267
+ // Snapshot the currently-playing media before updating currMedia so
75268
+ // we can cancel it cleanly after the region state is advanced.
75269
+ var interruptedMedia = regionObj.currMedia;
75270
+ // Compute new index with wrap-around. We do NOT delegate to
75271
+ // playNextMedia() / playPreviousMedia() here because those carry
75272
+ // normal playlist-cycle semantics (finished(), regionExpired()) that
75273
+ // must not fire during user-driven navigation.
75274
+ var newIndex = actionType === 'next' ? (regionObj.currentMediaIndex + 1) % total : (regionObj.currentMediaIndex - 1 + total) % total;
75275
+ regionObj.oldMedia = regionObj.currMedia;
75276
+ regionObj.currentMediaIndex = newIndex;
75277
+ regionObj.currMedia = regionObj.mediaObjects[newIndex];
75278
+ regionObj.nxtMedia = regionObj.mediaObjects[(newIndex + 1) % total];
75279
+ regionObj.complete = false;
75280
+ // Properly cancel the interrupted media AFTER updating currMedia.
75281
+ // Using 'cancelled' rather than bare clearInterval ensures state is
75282
+ // reset from PLAYING and mediaTimeCount is zeroed — without this,
75283
+ // returning to a cancelled media causes run() → 'start' to bail on
75284
+ // the state === PLAYING guard, leaving the region stuck indefinitely.
75285
+ // currMedia is already updated so the handler's guard
75286
+ // (media === media.region.currMedia) correctly skips playNextMedia.
75287
+ if ((interruptedMedia === null || interruptedMedia === void 0 ? void 0 : interruptedMedia.state) === MediaState.PLAYING) {
75288
+ interruptedMedia.emitter.emit('cancelled', interruptedMedia);
75246
75289
  }
75290
+ regionObj.transitionNodes(regionObj.oldMedia, regionObj.currMedia);
75247
75291
  });
75248
75292
  }
75249
75293
  }, {
@@ -75294,11 +75338,19 @@ var ActionController = /*#__PURE__*/function () {
75294
75338
  console.debug('[ActionController::loadMediaInRegion] Target media already queued, skipping duplicate insertion');
75295
75339
  return;
75296
75340
  }
75297
- // Cancel the current media's duration timer so it doesn't fire and interrupt
75298
- // the target widget mid-playback (e.g. an Interactive Zone timer still ticking).
75299
- if ((_targetRegion2 = targetRegion) !== null && _targetRegion2 !== void 0 && (_targetRegion2 = _targetRegion2.currMedia) !== null && _targetRegion2 !== void 0 && _targetRegion2.mediaTimer) {
75300
- clearInterval(targetRegion.currMedia.mediaTimer);
75301
- targetRegion.currMedia.mediaTimer = undefined;
75341
+ // Cancel the interrupted media so it doesn't double-advance when the playlist
75342
+ // returns to it. currMedia has not been advanced yet at this point (playNextMedia
75343
+ // does that below), so we cannot use emitter.emit('cancelled') the handler's
75344
+ // currMedia guard would fire and call playNextMedia a second time. Instead we
75345
+ // cancel directly: clear the timer and reset state so the 'start' handler does
75346
+ // not bail on the state === PLAYING guard when this media is replayed.
75347
+ if (((_targetRegion2 = targetRegion) === null || _targetRegion2 === void 0 || (_targetRegion2 = _targetRegion2.currMedia) === null || _targetRegion2 === void 0 ? void 0 : _targetRegion2.state) === MediaState.PLAYING) {
75348
+ var interruptedMedia = targetRegion.currMedia;
75349
+ if (interruptedMedia.mediaTimer) {
75350
+ clearInterval(interruptedMedia.mediaTimer);
75351
+ interruptedMedia.mediaTimer = undefined;
75352
+ }
75353
+ interruptedMedia.state = MediaState.CANCELLED;
75302
75354
  }
75303
75355
  // Reset complete so the HTML-media guard in playNextMedia() doesn't block
75304
75356
  // the transition (that guard is for single-media loops, not navWidget injections).
@@ -75327,6 +75379,12 @@ var ActionController = /*#__PURE__*/function () {
75327
75379
  }, {
75328
75380
  key: "runAction",
75329
75381
  value: function runAction(actionData, options) {
75382
+ // If this layout is no longer active (being cancelled or navigated away from),
75383
+ // discard the action so it doesn't interfere with the outgoing transition.
75384
+ // inLoop is set to false synchronously before finishAllRegions() in all nav paths.
75385
+ if (!this.parent.inLoop) {
75386
+ return;
75387
+ }
75330
75388
  console.debug('[ActionController::runAction] Triggering action', {
75331
75389
  actionData: actionData
75332
75390
  });
@@ -75414,31 +75472,37 @@ var ActionController = /*#__PURE__*/function () {
75414
75472
  key: "initKeyboardActions",
75415
75473
  value: function initKeyboardActions() {
75416
75474
  var self = this;
75417
- // Store actions in a map
75418
75475
  var keyActions = new Map();
75419
- this.$actionController.querySelectorAll('.action[triggerType="keyPress"]').forEach(function ($el) {
75476
+ this.$actionController.querySelectorAll('.action[triggertype="keyPress"]').forEach(function ($el) {
75420
75477
  var dataset = $el.dataset;
75421
75478
  var code = dataset.triggercode;
75422
75479
  if (code) {
75423
- // Create an empty array, if not yet set
75424
75480
  if (!keyActions.get(code)) {
75425
75481
  keyActions.set(code, []);
75426
75482
  }
75427
- // Add new action to array
75428
75483
  keyActions.get(code).push(dataset);
75429
75484
  }
75430
75485
  });
75431
- // Keyboard listener
75432
- document.addEventListener('keydown', function (ev) {
75486
+ // Nothing to do if this layout has no keyboard-triggered actions.
75487
+ if (keyActions.size === 0) return;
75488
+ this.keyboardHandler = function (ev) {
75433
75489
  var actions = keyActions.get(ev.code);
75434
- // Are there action for this key code?
75435
75490
  if (actions) {
75436
- // Run all actions associated with it
75437
75491
  actions.forEach(function (dataset) {
75438
75492
  self.runAction(dataset, self.options);
75439
75493
  });
75440
75494
  }
75441
- });
75495
+ };
75496
+ document.addEventListener('keydown', this.keyboardHandler);
75497
+ }
75498
+ /** Remove the keydown listener registered by initKeyboardActions. Call when the layout ends or is cancelled. */
75499
+ }, {
75500
+ key: "removeKeyboardActions",
75501
+ value: function removeKeyboardActions() {
75502
+ if (this.keyboardHandler) {
75503
+ document.removeEventListener('keydown', this.keyboardHandler);
75504
+ this.keyboardHandler = null;
75505
+ }
75442
75506
  }
75443
75507
  }]);
75444
75508
  }();
@@ -75673,6 +75737,7 @@ var Layout = /*#__PURE__*/function () {
75673
75737
  });
75674
75738
  this.on('end', /*#__PURE__*/function () {
75675
75739
  var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(layout) {
75740
+ var _layout$actionControl;
75676
75741
  var $layout, _$layout$parentElemen;
75677
75742
  return _regeneratorRuntime().wrap(function _callee2$(_context2) {
75678
75743
  while (1) switch (_context2.prev = _context2.next) {
@@ -75731,9 +75796,10 @@ var Layout = /*#__PURE__*/function () {
75731
75796
  }
75732
75797
  // Emit layout end event
75733
75798
  console.debug('>>>>> XLR.debug Awaited XLR::emitSync > End - Calling layoutEnd event');
75734
- _context2.next = 13;
75799
+ (_layout$actionControl = layout.actionController) === null || _layout$actionControl === void 0 || _layout$actionControl.removeKeyboardActions();
75800
+ _context2.next = 14;
75735
75801
  return layout.xlr.emitSync('layoutEnd', layout);
75736
- case 13:
75802
+ case 14:
75737
75803
  if (_this.xlr.config.platform !== ConsumerPlatform.CMS && layout.inLoop) {
75738
75804
  // Transition next layout to current layout and prepare next layout if exist
75739
75805
  _this.xlr.prepareLayouts().then( /*#__PURE__*/function () {
@@ -75763,7 +75829,7 @@ var Layout = /*#__PURE__*/function () {
75763
75829
  };
75764
75830
  }());
75765
75831
  }
75766
- case 14:
75832
+ case 15:
75767
75833
  case "end":
75768
75834
  return _context2.stop();
75769
75835
  }
@@ -75774,9 +75840,11 @@ var Layout = /*#__PURE__*/function () {
75774
75840
  };
75775
75841
  }());
75776
75842
  this.on('cancelled', function (layout) {
75843
+ var _layout$actionControl2;
75777
75844
  console.debug('>>>>> XLR.debug / Layout cancelled > Layout ID > ', layout.id);
75778
75845
  layout.state = ELayoutState.CANCELLED;
75779
75846
  layout.inLoop = false;
75847
+ (_layout$actionControl2 = layout.actionController) === null || _layout$actionControl2 === void 0 || _layout$actionControl2.removeKeyboardActions();
75780
75848
  // Dispose video handlers immediately so their stall watchdogs and error
75781
75849
  // callbacks can't fire against a layout whose DOM is about to be removed.
75782
75850
  var _iterator = _createForOfIteratorHelper(layout.regions),
@@ -75935,7 +76003,8 @@ var Layout = /*#__PURE__*/function () {
75935
76003
  _this2.regions.push(regionObj);
75936
76004
  });
75937
76005
  this.actionController.initTouchActions();
75938
- this.actionController.initKeyboardActions();
76006
+ // Keyboard actions are registered in run() so the global document listener
76007
+ // is only active while the layout is actually playing, not during background preparation.
75939
76008
  }
75940
76009
  }, {
75941
76010
  key: "run",
@@ -75954,6 +76023,7 @@ var Layout = /*#__PURE__*/function () {
75954
76023
  shouldParse: false
75955
76024
  });
75956
76025
  if ($layoutContainer) {
76026
+ var _this$actionControlle;
75957
76027
  $layoutContainer.style.setProperty('visibility', 'visible');
75958
76028
  $layoutContainer.style.setProperty('opacity', '1');
75959
76029
  $layoutContainer.style.setProperty('z-index', this.zIndex !== null ? "".concat(this.zIndex) : '1');
@@ -75962,6 +76032,9 @@ var Layout = /*#__PURE__*/function () {
75962
76032
  // Also set the background color of the player window > body
75963
76033
  document.body.style.setProperty('background-color', "".concat(this.bgColor));
75964
76034
  }
76035
+ // Register keyboard actions now that the layout is active.
76036
+ // Done here (not in parseXlf) so the global listener is scoped to playback time.
76037
+ (_this$actionControlle = this.actionController) === null || _this$actionControlle === void 0 || _this$actionControlle.initKeyboardActions();
75965
76038
  // Emit start event
75966
76039
  this.emitter.emit('start', this);
75967
76040
  // Play regions