diff --git a/app/controllers/user_initials_controller.rb b/app/controllers/user_initials_controller.rb index b2db409b..ced24aaa 100644 --- a/app/controllers/user_initials_controller.rb +++ b/app/controllers/user_initials_controller.rb @@ -4,7 +4,10 @@ class UserInitialsController < ApplicationController before_action :load_user_config authorize_resource :user_config - def edit; end + def edit + @font_config = + UserConfig.find_or_initialize_by(user: current_user, key: UserConfig::INITIALS_FONT_KEY) + end def update file = params[:file] @@ -22,6 +25,7 @@ class UserInitialsController < ApplicationController ) if @user_config.update(value: attachment.uuid) + save_font_preference(UserConfig::INITIALS_FONT_KEY) redirect_to settings_profile_index_path, notice: I18n.t('initials_has_been_saved') else redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') @@ -40,4 +44,11 @@ class UserInitialsController < ApplicationController @user_config = UserConfig.find_or_initialize_by(user: current_user, key: UserConfig::INITIALS_KEY) end + + def save_font_preference(key) + return if params[:font].blank? + + font_config = UserConfig.find_or_initialize_by(user: current_user, key:) + font_config.update(value: params[:font]) + end end diff --git a/app/controllers/user_signatures_controller.rb b/app/controllers/user_signatures_controller.rb index 1200acff..661749dd 100644 --- a/app/controllers/user_signatures_controller.rb +++ b/app/controllers/user_signatures_controller.rb @@ -4,7 +4,10 @@ class UserSignaturesController < ApplicationController before_action :load_user_config authorize_resource :user_config - def edit; end + def edit + @font_config = + UserConfig.find_or_initialize_by(user: current_user, key: UserConfig::SIGNATURE_FONT_KEY) + end def update file = params[:file] @@ -22,6 +25,7 @@ class UserSignaturesController < ApplicationController ) if @user_config.update(value: attachment.uuid) + save_font_preference(UserConfig::SIGNATURE_FONT_KEY) redirect_to settings_profile_index_path, notice: I18n.t('signature_has_been_saved') else redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') @@ -40,4 +44,11 @@ class UserSignaturesController < ApplicationController @user_config = UserConfig.find_or_initialize_by(user: current_user, key: UserConfig::SIGNATURE_KEY) end + + def save_font_preference(key) + return if params[:font].blank? + + font_config = UserConfig.find_or_initialize_by(user: current_user, key:) + font_config.update(value: params[:font]) + end end diff --git a/app/javascript/application.js b/app/javascript/application.js index 920cba82..24671b2c 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -20,6 +20,7 @@ import AutoresizeTextarea from './elements/autoresize_textarea' import SubmittersAutocomplete from './elements/submitter_autocomplete' import FolderAutocomplete from './elements/folder_autocomplete' import SignatureForm from './elements/signature_form' +import TypedSignatureForm from './elements/typed_signature_form' import SubmitForm from './elements/submit_form' import PromptPassword from './elements/prompt_password' import EmailsTextarea from './elements/emails_textarea' @@ -111,6 +112,7 @@ safeRegisterElement('autoresize-textarea', AutoresizeTextarea) safeRegisterElement('submitters-autocomplete', SubmittersAutocomplete) safeRegisterElement('folder-autocomplete', FolderAutocomplete) safeRegisterElement('signature-form', SignatureForm) +safeRegisterElement('typed-signature-form', TypedSignatureForm) safeRegisterElement('submit-form', SubmitForm) safeRegisterElement('prompt-password', PromptPassword) safeRegisterElement('emails-textarea', EmailsTextarea) diff --git a/app/javascript/elements/typed_signature_form.js b/app/javascript/elements/typed_signature_form.js new file mode 100644 index 00000000..5aa2485f --- /dev/null +++ b/app/javascript/elements/typed_signature_form.js @@ -0,0 +1,137 @@ +import { target, targetable } from '@github/catalyst/lib/targetable' +import { cropCanvasAndExportToPNG } from '../submission_form/crop_canvas' + +const SIGNATURE_FONTS = { + 'Dancing Script': 'DancingScript-Regular.otf', + 'Great Vibes': 'GreatVibes-Regular.ttf', + Pacifico: 'Pacifico-Regular.ttf', + Caveat: 'Caveat-Regular.ttf', + 'Homemade Apple': 'HomemadeApple-Regular.ttf', + 'Mrs Saint Delafield': 'MrsSaintDelafield-Regular.ttf', + 'Shadows Into Light': 'ShadowsIntoLight-Regular.ttf', + 'Alex Brush': 'AlexBrush-Regular.ttf', + Kalam: 'Kalam-Regular.ttf', + Sacramento: 'Sacramento-Regular.ttf', + 'Herr Von Muellerhoff': 'HerrVonMuellerhoff-Regular.ttf' +} + +const fontLoadPromises = {} +const scale = 3 + +export default targetable(class extends HTMLElement { + static [target.static] = ['canvas', 'textInput', 'fontSelect', 'input', 'button', 'fontHidden'] + + async connectedCallback () { + this.setCanvasSize() + + this.resizeObserver = new ResizeObserver(() => { + requestAnimationFrame(() => { + if (!this.canvas) return + + this.setCanvasSize() + this.updateCanvas() + }) + }) + + this.resizeObserver.observe(this.canvas.parentNode) + + this.textInput.addEventListener('input', () => this.updateCanvas()) + + this.fontSelect.addEventListener('change', async () => { + this.fontHidden.value = this.fontSelect.value + await this.loadFont(this.fontSelect.value) + this.updateCanvas() + }) + + this.button.addEventListener('click', (e) => { + e.preventDefault() + + if (!this.textInput.value.trim()) return + + this.button.disabled = true + this.submit() + }) + + await this.loadFont(this.fontSelect.value) + + if (this.textInput.value) { + this.updateCanvas() + } + } + + disconnectedCallback () { + if (this.resizeObserver) { + this.resizeObserver.disconnect() + } + } + + setCanvasSize () { + const width = this.canvas.parentNode.clientWidth + const height = width / 2.5 + + if (this.canvas.width !== width * scale || this.canvas.height !== height * scale) { + this.canvas.width = width * scale + this.canvas.height = height * scale + this.canvas.getContext('2d').scale(scale, scale) + } + } + + loadFont (fontName) { + const file = SIGNATURE_FONTS[fontName] + if (!file) return Promise.resolve() + + if (!fontLoadPromises[fontName]) { + const ext = file.endsWith('.otf') ? 'opentype' : 'truetype' + const font = new FontFace(fontName, `url(/fonts/${file}) format("${ext}")`) + + fontLoadPromises[fontName] = font.load().then((loadedFont) => { + document.fonts.add(loadedFont) + }).catch((error) => { + console.error('Font loading failed:', error) + }) + } + + return fontLoadPromises[fontName] + } + + updateCanvas () { + const context = this.canvas.getContext('2d') + const text = this.textInput.value + const fontFamily = this.fontSelect.value + const initialFontSize = 44 + + const setFontSize = (size) => { + context.font = `italic ${size}px "${fontFamily}"` + } + + const maxWidth = this.canvas.width / scale + let size = initialFontSize + + setFontSize(size) + + while (context.measureText(text).width > maxWidth && size > 1) { + size -= 1 + setFontSize(size) + } + + context.textAlign = 'center' + context.clearRect(0, 0, this.canvas.width / scale, this.canvas.height / scale) + context.fillText(text, this.canvas.width / 2 / scale, this.canvas.height / 2 / scale + 11) + } + + async submit () { + const blob = await cropCanvasAndExportToPNG(this.canvas) + const file = new File([blob], 'signature.png', { type: 'image/png' }) + + const dataTransfer = new DataTransfer() + dataTransfer.items.add(file) + + this.input.files = dataTransfer.files + + if (this.input.webkitEntries.length) { + this.input.dataset.file = `${dataTransfer.files[0].name}` + } + + this.closest('form').requestSubmit() + } +}) diff --git a/app/models/user_config.rb b/app/models/user_config.rb index 87849013..f0c179a6 100644 --- a/app/models/user_config.rb +++ b/app/models/user_config.rb @@ -23,6 +23,8 @@ class UserConfig < ApplicationRecord SIGNATURE_KEY = 'signature' INITIALS_KEY = 'initials' + SIGNATURE_FONT_KEY = 'signature_font' + INITIALS_FONT_KEY = 'initials_font' RECEIVE_COMPLETED_EMAIL = 'receive_completed_email' RECEIVE_DECLINED_EMAIL = 'receive_declined_email' SHOW_APP_TOUR = 'show_app_tour' diff --git a/app/views/user_initials/edit.html.erb b/app/views/user_initials/edit.html.erb index 512b81a2..195a1685 100644 --- a/app/views/user_initials/edit.html.erb +++ b/app/views/user_initials/edit.html.erb @@ -1,5 +1,7 @@ +<% signature_fonts = ['Dancing Script', 'Great Vibes', 'Pacifico', 'Caveat', 'Homemade Apple', 'Mrs Saint Delafield', 'Shadows Into Light', 'Alex Brush', 'Kalam', 'Sacramento', 'Herr Von Muellerhoff'] %> +<% saved_font = @font_config&.value || 'Dancing Script' %> <%= render 'shared/turbo_modal', title: t('update_initials') do %> - <% options = [[t('draw'), 'draw'], [t('upload'), 'upload']] %> + <% options = [[t('draw'), 'draw'], [t('type'), 'type'], [t('upload'), 'upload']] %>
<% options.each_with_index do |(label, value), index| %> @@ -24,6 +26,22 @@ <% end %>
+