diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js index fc06413d44..08e1f1e439 100644 --- a/src/public/app/layouts/desktop_layout.js +++ b/src/public/app/layouts/desktop_layout.js @@ -46,6 +46,7 @@ import OpenNoteButtonWidget from "../widgets/buttons/open_note_button_widget.js" import MermaidWidget from "../widgets/mermaid.js"; import BookmarkButtons from "../widgets/bookmark_buttons.js"; import NoteWrapperWidget from "../widgets/note_wrapper.js"; +import BannerMessageWidget from "../widgets/banner_message.js"; export default class DesktopLayout { constructor(customWidgets) { @@ -57,6 +58,7 @@ export default class DesktopLayout { return new RootContainer() .setParent(appContext) + .child(new BannerMessageWidget()) .child(new FlexContainer("column") .id("launcher-pane") .css("width", "53px") diff --git a/src/public/app/services/timer.js b/src/public/app/services/timer.js new file mode 100644 index 0000000000..d966b0a985 --- /dev/null +++ b/src/public/app/services/timer.js @@ -0,0 +1,29 @@ +// https://stackoverflow.com/a/3969760 +export default class Timer { + timerId; + start; + remaining; + callback; + + constructor(callback, delay) { + this.remaining = delay; + this.callback = callback; + + this.resume() + } + + pause() { + clearTimeout(this.timerId); + this.remaining -= Date.now() - this.start; + } + + resume() { + this.start = Date.now(); + clearTimeout(this.timerId); + this.timerId = setTimeout(this.callback, this.remaining); + } + + clear() { + clearTimeout(this.timerId); + } +} diff --git a/src/public/app/widgets/banner_message.js b/src/public/app/widgets/banner_message.js new file mode 100644 index 0000000000..d7679e2b77 --- /dev/null +++ b/src/public/app/widgets/banner_message.js @@ -0,0 +1,158 @@ +import BasicWidget from "./basic_widget.js"; +import Timer from "../services/timer.js"; + +const TLP = ` +
+`; + +const AVAILABLE_TYPES = new Set([ + "error", "info", "warning", "success", "plain" +]); + +export default class BannerMessageWidget extends BasicWidget { + durationTimer; + + constructor() { + super(); + } + + doRender() { + this.$widget = $(TLP); + this.$bannerParagraph = this.$widget.find("p"); + this.$timer = this.$widget.find(".timer"); + + this.$widget.on("mouseenter", this.pauseTimer.bind(this)); + this.$widget.on("mouseleave", this.resumeTimer.bind(this)); + } + + hideBanner() { + this.$bannerParagraph.text(""); + this.$widget.removeClass(); + this.$widget.addClass("empty"); + + // In case `hideBanner` is called before the actual end, clear timer to avoid hard bugs + this.durationTimer?.clear(); + this.durationTimer = undefined; + } + + pauseTimer() { + if (this.durationTimer) { + this.$timer.css({ + animationPlayState: "paused", + }); + this.durationTimer?.pause(); + } + } + + resumeTimer() { + if (this.durationTimer) { + this.$timer.css({ + animationPlayState: "", + }); + this.durationTimer?.resume(); + } + } + + /** + * Shows a top banner. + * @param text - string: The text that should be displayed + * @param type - string: Type of the banner ("error", "info", "warning", "success", "plain") + * @param duration - number?: How long to show the banner. If `none` or `undefined`, + * the banner will not automatically be hidden. + */ + setBannerEvent({ + text, + type = "alert", + duration, + }) { + if (!text) { + this.hideBanner(); + return; + } + + const className = AVAILABLE_TYPES.has(type) ? type : "plain"; + + this.$bannerParagraph.text(text); + this.$widget.removeClass(); + this.$widget.addClass(className); + this.$timer.removeClass("indefinite"); + + // Remove old timer to avoid hard bug + this.durationTimer?.clear(); + + if (duration) { + this.durationTimer = new Timer(this.hideBanner.bind(this), duration); + this.$timer.css({ + animationDuration: `${duration}ms` + }) + } else { + this.$timer.addClass("indefinite"); + } + } + + hideBannerEvent() { + this.hideBanner(); + } +} diff --git a/src/public/app/widgets/buttons/calendar.js b/src/public/app/widgets/buttons/calendar.js index 85d541c957..a592813cc8 100644 --- a/src/public/app/widgets/buttons/calendar.js +++ b/src/public/app/widgets/buttons/calendar.js @@ -30,6 +30,13 @@ const DROPDOWN_TPL = ` export default class CalendarWidget extends RightDropdownButtonWidget { constructor() { super("bx-calendar", "Calendar", DROPDOWN_TPL); + + setTimeout(() => { + this.triggerEvent("setBanner", { + text: "Internet lost.", + type: "error" + }) + }, 200); } doRender() { diff --git a/src/public/stylesheets/theme-dark.css b/src/public/stylesheets/theme-dark.css index 3c6009b636..e70ec6e3d5 100644 --- a/src/public/stylesheets/theme-dark.css +++ b/src/public/stylesheets/theme-dark.css @@ -64,6 +64,9 @@ --link-color: lightskyblue; --mermaid-theme: dark; + + --banner-background-color: #DA321B; + --banner-color: var(--main-text-color); } body .global-menu-button { diff --git a/src/public/stylesheets/theme-light.css b/src/public/stylesheets/theme-light.css index 82fb21d9b4..ba4161b343 100644 --- a/src/public/stylesheets/theme-light.css +++ b/src/public/stylesheets/theme-light.css @@ -68,4 +68,7 @@ html { --link-color: blue; --mermaid-theme: default; + + --banner-background-color: #DA321B; + --banner-color: var(--main-text-color); }