diff --git a/app/assets/javascripts/wizard/addon/components/wizard-preview-base.js b/app/assets/javascripts/wizard/addon/components/wizard-preview-base.js index 8e7cd0946f9..bd8058fb042 100644 --- a/app/assets/javascripts/wizard/addon/components/wizard-preview-base.js +++ b/app/assets/javascripts/wizard/addon/components/wizard-preview-base.js @@ -1,4 +1,5 @@ import Component from "@ember/component"; +import PreloadStore from "discourse/lib/preload-store"; import { Promise } from "rsvp"; /*eslint no-bitwise:0 */ import getUrl from "discourse-common/lib/get-url"; @@ -51,24 +52,78 @@ export default Component.extend({ ctx: null, loaded: false, + loadingFontVariants: false, didInsertElement() { this._super(...arguments); + this.fontMap = PreloadStore.get("fontMap"); + this.loadedFonts = new Set(); const c = this.element.querySelector("canvas"); this.ctx = c.getContext("2d"); this.ctx.scale(scale, scale); this.reload(); }, - @observes( - "step.fieldsById.{color_scheme,body_font,heading_font,homepage_style}.value" - ) + @observes("step.fieldsById.{color_scheme,homepage_style}.value") themeChanged() { this.triggerRepaint(); }, + @observes("step.fieldsById.{body_font}.value") + themeBodyFontChanged() { + if (!this.loadingFontVariants) { + this.loadFontVariants(this.wizard.font); + } + }, + + @observes("step.fieldsById.{heading_font}.value") + themeHeadingFontChanged() { + if (!this.loadingFontVariants) { + this.loadFontVariants(this.wizard.headingFont); + } + }, + + loadFontVariants(font) { + const fontVariantData = this.fontMap[font.id]; + + // System font for example does not need to load from a remote source. + if (fontVariantData && !this.loadedFonts.has(font.id)) { + this.loadingFontVariants = true; + const fontFaces = fontVariantData.map((fontVariant) => { + return new FontFace(font.label, `url(${fontVariant.url})`, { + style: "normal", + weight: fontVariant.weight, + }); + }); + + Promise.all( + fontFaces.map((fontFace) => + fontFace.load().then((loadedFont) => { + document.fonts.add(loadedFont); + + // We use our own Set because, though document.fonts.check is available, + // it does not seem very reliable, returning false for fonts that have + // definitely been loaded. + this.loadedFonts.add(font.id); + }) + ) + ) + .then(() => { + this.triggerRepaint(); + }) + .finally(() => { + this.loadingFontVariants = false; + }); + } else if (this.loadedFonts.has(font.id)) { + this.triggerRepaint(); + } + }, + images() {}, + // NOTE: This works for fonts included in a style that is actually using the + // @font-faces on load, but for fonts that we aren't using yet we need to + // make sure they are loaded before rendering the canvas via loadFontVariants. loadFonts() { return document.fonts.ready; }, @@ -125,8 +180,8 @@ export default Component.extend({ const options = { ctx, colors, - font, - headingFont, + font: font?.label, + headingFont: headingFont?.label, width: this.width, height: this.height, }; diff --git a/app/assets/javascripts/wizard/addon/models/wizard.js b/app/assets/javascripts/wizard/addon/models/wizard.js index fbdf8f86c08..a871746eb4c 100644 --- a/app/assets/javascripts/wizard/addon/models/wizard.js +++ b/app/assets/javascripts/wizard/addon/models/wizard.js @@ -41,13 +41,13 @@ const Wizard = EmberObject.extend(Evented, { get font() { const fontChoice = this.steps.findBy("id", "styling")?.fieldsById ?.body_font; - return fontChoice.choices?.findBy("id", fontChoice.value)?.label; + return fontChoice.choices?.findBy("id", fontChoice.value); }, get headingFont() { const fontChoice = this.steps.findBy("id", "styling")?.fieldsById ?.heading_font; - return fontChoice.choices?.findBy("id", fontChoice.value)?.label; + return fontChoice.choices?.findBy("id", fontChoice.value); }, }); diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2eb57914f42..d5820de91a3 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -661,6 +661,9 @@ class ApplicationController < ActionController::Base store_preloaded("topicTrackingStates", MultiJson.dump(hash[:data])) store_preloaded("topicTrackingStateMeta", MultiJson.dump(hash[:meta])) + + # This is used in the wizard so we can preload fonts using the FontMap JS API. + store_preloaded("fontMap", MultiJson.dump(load_font_map)) if current_user.admin? end def custom_html_json @@ -1065,4 +1068,18 @@ class ApplicationController < ActionController::Base def spa_boot_request? request.get? && !(request.format && request.format.json?) && !request.xhr? end + + def load_font_map + DiscourseFonts + .fonts + .each_with_object({}) do |font, font_map| + next if !font[:variants] + font_map[font[:key]] = font[:variants].map do |v| + { + url: "#{Discourse.base_url}/fonts/#{v[:filename]}?v=#{DiscourseFonts::VERSION}", + weight: v[:weight], + } + end + end + end end diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb index 9ec28506c3a..40056a27bee 100644 --- a/spec/requests/application_controller_spec.rb +++ b/spec/requests/application_controller_spec.rb @@ -1133,4 +1133,96 @@ RSpec.describe ApplicationController do end end end + + describe "preloading data" do + def preloaded_json + JSON.parse( + Nokogiri::HTML5.fragment(response.body).css("div#data-preloaded").first["data-preloaded"], + ) + end + + context "when user is anon" do + it "preloads the relevant JSON data" do + get "/latest" + expect(response.status).to eq(200) + expect(preloaded_json.keys).to match_array( + [ + "site", + "siteSettings", + "customHTML", + "banner", + "customEmoji", + "isReadOnly", + "isStaffWritesOnly", + "activatedThemes", + "#{TopicList.new("latest", Fabricate(:anonymous), []).preload_key}", + ], + ) + end + end + + context "when user is regular user" do + fab!(:user) { Fabricate(:user) } + + before { sign_in(user) } + + it "preloads the relevant JSON data" do + get "/latest" + expect(response.status).to eq(200) + expect(preloaded_json.keys).to match_array( + [ + "site", + "siteSettings", + "customHTML", + "banner", + "customEmoji", + "isReadOnly", + "isStaffWritesOnly", + "activatedThemes", + "#{TopicList.new("latest", Fabricate(:anonymous), []).preload_key}", + "currentUser", + "topicTrackingStates", + "topicTrackingStateMeta", + ], + ) + end + end + + context "when user is admin" do + fab!(:user) { Fabricate(:admin) } + + before { sign_in(user) } + + it "preloads the relevant JSON data" do + get "/latest" + expect(response.status).to eq(200) + expect(preloaded_json.keys).to match_array( + [ + "site", + "siteSettings", + "customHTML", + "banner", + "customEmoji", + "isReadOnly", + "isStaffWritesOnly", + "activatedThemes", + "#{TopicList.new("latest", Fabricate(:anonymous), []).preload_key}", + "currentUser", + "topicTrackingStates", + "topicTrackingStateMeta", + "fontMap", + ], + ) + end + + it "generates a fontMap" do + get "/latest" + expect(response.status).to eq(200) + font_map = JSON.parse(preloaded_json["fontMap"]) + expect(font_map.keys).to match_array( + DiscourseFonts.fonts.filter { |f| f[:variants].present? }.map { |f| f[:key] }, + ) + end + end + end end