diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b34fc81df..c4955b3bb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -13,7 +13,9 @@ class ApplicationController < ActionController::Base before_action :maybe_redirect_to_setup, unless: :signed_in? before_action :authenticate_user!, unless: :devise_controller? - before_action :set_csp, if: -> { request.get? && !request.headers['HTTP_X_TURBO_REQUEST_ID'] && !turbo_infinite_scroll? } + before_action :set_csp, if: lambda { + request.get? && !request.headers['HTTP_X_TURBO_REQUEST_ID'] && !turbo_infinite_scroll? + } helper_method :button_title, :current_account, @@ -70,12 +72,22 @@ class ApplicationController < ActionController::Base request.headers['HTTP_X_TURBO_INFINITE_SCROLL'].present? end + def turbo_native_app? + return @turbo_native_app if defined?(@turbo_native_app) + + @turbo_native_app = request.user_agent.to_s.include?('Hotwire Native') + end + def turbo_native_ios? - request.user_agent.to_s.include?('Hotwire Native iOS') + return @turbo_native_ios if defined?(@turbo_native_ios) + + @turbo_native_ios = request.user_agent.to_s.include?('Hotwire Native iOS') end def turbo_native_android? - request.user_agent.to_s.include?('Hotwire Native Android') + return @turbo_native_android if defined?(@turbo_native_android) + + @turbo_native_android = request.user_agent.to_s.include?('Hotwire Native Android') end def with_locale(&) diff --git a/app/javascript/application.js b/app/javascript/application.js index 6091c1271..e95852523 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -81,6 +81,12 @@ document.addEventListener('keyup', (e) => { } }) +document.addEventListener('turbo:morph', () => { + if (document.activeElement?.closest('.dropdown')) { + document.activeElement.blur() + } +}) + document.addEventListener('turbo:before-fetch-response', (event) => { if (event.detail.fetchResponse.header('content-disposition')?.includes('attachment')) { event.preventDefault() @@ -373,7 +379,7 @@ safeRegisterElement('template-builder', class extends HTMLElement { root.querySelector('meta[name="viewport"]')?.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no') - style.textContent = 'html { overflow-x: hidden; overscroll-behavior-x: none; } html, body { max-width: 100%; }' + style.textContent = 'html { overflow-x: hidden; overscroll-behavior-x: none; } html, body { max-width: 100%; } body { -webkit-user-select: none; user-select: none; } input, textarea, [contenteditable] { -webkit-user-select: text; user-select: text; }' root.querySelector('head').appendChild(style) diff --git a/app/javascript/elements/native_action.js b/app/javascript/elements/native_action.js index 256c9000a..97d3b5225 100644 --- a/app/javascript/elements/native_action.js +++ b/app/javascript/elements/native_action.js @@ -1,3 +1,5 @@ +import { nativeIcon } from '../native' + export default class extends HTMLElement { connectedCallback () { const bridge = window.webkit?.messageHandlers?.native @@ -18,13 +20,13 @@ export default class extends HTMLElement { op: 'add', id: this.nativeId, title: this.dataset.label || target?.textContent.trim(), - icon: this.dataset.icon, + icon: nativeIcon(this.dataset.icon), placement: this.dataset.placement || 'menu', side: this.dataset.side, view: this.dataset.view, section: this.dataset.section, menu: this.dataset.menu, - menuIcon: this.dataset.menuIcon, + menuIcon: nativeIcon(this.dataset.menuIcon), native: this.dataset.native, url: this.dataset.url, accept: this.dataset.accept, diff --git a/app/javascript/elements/native_menu.js b/app/javascript/elements/native_menu.js index 34c8c4ea7..d9c2f7b86 100644 --- a/app/javascript/elements/native_menu.js +++ b/app/javascript/elements/native_menu.js @@ -1,3 +1,5 @@ +import { nativeIcon } from '../native' + export default class extends HTMLElement { connectedCallback () { if (window.webkit?.messageHandlers?.native) { @@ -31,7 +33,7 @@ export default class extends HTMLElement { return { id: target.dataset.nativeId, title: action.dataset.label || target.textContent.trim(), - icon: action.dataset.icon, + icon: nativeIcon(action.dataset.icon), destructive: action.dataset.destructive === 'true', selected: action.dataset.selected === 'true', haptic: action.dataset.haptic === 'true' diff --git a/app/javascript/native.js b/app/javascript/native.js index 3fe988c1d..d2898f582 100644 --- a/app/javascript/native.js +++ b/app/javascript/native.js @@ -1,3 +1,54 @@ +const ICONS = { + ios: { + link: 'link', + archive: 'archivebox', + folder: 'folder', + trash: 'trash', + rotate: 'arrow.counterclockwise', + copy: 'doc.on.doc', + pencil: 'pencil', + logs: 'list.bullet.rectangle', + external_link: 'arrow.up.right.square', + download: 'arrow.down.circle', + mail_forward: 'paperplane', + send: 'paperplane', + writing: 'signature', + adjustments_horizontal: 'slider.horizontal.3', + file_text: 'doc.text', + files: 'doc.on.doc', + plus: 'plus', + eye: 'eye', + check: 'checkmark', + code: 'chevron.left.forwardslash.chevron.right', + history: 'clock.arrow.circlepath', + user: 'person', + users: 'person.2', + users_plus: 'person.badge.plus', + photo: 'photo', + settings: 'gearshape', + logout: 'rectangle.portrait.and.arrow.right', + upload: 'arrow.up.doc', + scan: 'doc.viewfinder', + pencil_plus: 'square.and.pencil', + arrow_sort: 'arrow.up.arrow.down', + sort_descending_numbers: 'calendar', + sort_descending_small_big: 'clock.arrow.circlepath', + sort_ascending_letters: 'textformat.abc', + calendar: 'calendar', + calendar_check: 'calendar.badge.checkmark', + info_circle: 'info.circle', + list: 'list.bullet', + clock: 'clock', + circle_check: 'checkmark.circle' + } +} + +const platform = navigator.userAgent.includes('Hotwire Native Android') ? 'android' : 'ios' + +export function nativeIcon (name) { + return ICONS[platform]?.[name] || name +} + const handlers = window.webkit?.messageHandlers if (handlers?.native && window === window.top) { @@ -46,7 +97,7 @@ if (handlers?.native && window === window.top) { const actions = link.closest('[data-native-actions]')?.dataset.nativeActions - handlers.native.postMessage({ type: 'title', url: link.href, title, actions: actions ? actions.split(',') : null }) + handlers.native.postMessage({ type: 'title', url: link.href, title, actions: actions ? actions.split(',').map(nativeIcon) : null }) }, true) document.addEventListener('click', (event) => { diff --git a/app/javascript/template_builder/area_title.vue b/app/javascript/template_builder/area_title.vue index 8ca938669..9b039b39f 100644 --- a/app/javascript/template_builder/area_title.vue +++ b/app/javascript/template_builder/area_title.vue @@ -90,7 +90,7 @@ class="cursor-pointer flex items-center" :class="{ 'pr-0.5': isMobile }" style="height: 24px" - @focus="isSettingsFocus = true" + @focus="onSettingsFocus" @blur="maybeBlurSettings" > @@ -423,6 +423,19 @@ export default { this.isSettingsFocus = false } }, + onSettingsFocus () { + this.isSettingsFocus = true + + if (this.isMobile) { + this.$nextTick(() => { + const dropdown = this.$refs.settingsDropdown + const padding = parseFloat(getComputedStyle(dropdown).paddingLeft) + 6 + const left = dropdown.parentElement.getBoundingClientRect().right - dropdown.offsetWidth - padding + + dropdown.style.right = left < 0 ? `${left}px` : '' + }) + } + }, onNameFocus (e) { this.selectedAreasRef.value = [this.area] diff --git a/app/views/layouts/_head_tags.html.erb b/app/views/layouts/_head_tags.html.erb index 292d8379e..9c8757efb 100644 --- a/app/views/layouts/_head_tags.html.erb +++ b/app/views/layouts/_head_tags.html.erb @@ -4,5 +4,5 @@ <%= render 'shared/meta' %> <% if turbo_native_app? %> - + <%= render 'layouts/native_styles' %> <% end %> diff --git a/app/views/layouts/_native_path_configuration.json.erb b/app/views/layouts/_native_path_configuration.json.erb index 1f6650794..f3517034a 100644 --- a/app/views/layouts/_native_path_configuration.json.erb +++ b/app/views/layouts/_native_path_configuration.json.erb @@ -3,6 +3,7 @@ "strings": { "search": <%== t('search').to_json %>, "create": <%== t('create').to_json %>, + "new": <%== t('new').to_json %>, "documents": <%== t('documents').to_json %>, "fields": <%== t('fields').to_json %>, "more": <%== t('more').to_json %>, diff --git a/app/views/layouts/_native_styles.html.erb b/app/views/layouts/_native_styles.html.erb new file mode 100644 index 000000000..2eb34a5c2 --- /dev/null +++ b/app/views/layouts/_native_styles.html.erb @@ -0,0 +1,10 @@ + diff --git a/app/views/templates/_submission.html.erb b/app/views/templates/_submission.html.erb index 32116b0a3..af228822b 100644 --- a/app/views/templates/_submission.html.erb +++ b/app/views/templates/_submission.html.erb @@ -35,13 +35,13 @@ <% if submission.expired? && !submitter.completed_at? && !submitter.declined_at? %>
- + <%= t('expired') %>
<% else %> - + <%= t(submitter.status) %> @@ -67,7 +67,7 @@ <% if submitter.completed_at? %>
- + <%= svg_icon('download', class: 'w-5 h-5 max-sm:w-4 max-sm:h-4 stroke-2') %> <%= t('download') %> @@ -82,7 +82,7 @@ <% elsif !submission.archived_at? && !template&.archived_at? && !submission.expired? && !submitter.declined_at? %> <% if current_user.email == submitter.email %> <% else %>
- <%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug, host: form_link_host), class: 'btn btn-sm max-sm:normal-case btn-neutral text-white md:w-36 flex z-[1]', icon_class: 'w-6 h-6 max-sm:w-[18px] max-sm:h-[18px] text-white', copy_title: t('copy_link').length < 10 ? t('copy_link') : t('copy'), copy_title_md: t('link'), copied_title_md: t('copied') %> + <%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug, host: form_link_host), class: 'btn btn-sm max-sm:text-[13px] btn-neutral text-white md:w-36 flex z-[1]', icon_class: 'w-6 h-6 max-sm:w-[18px] max-sm:h-[18px] text-white', copy_title: t('copy_link').length < 10 ? t('copy_link') : t('copy'), copy_title_md: t('link'), copied_title_md: t('copied') %>
<% end %> <% end %>
@@ -119,13 +119,13 @@ <% if is_submission_completed %> <% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %> - + <%= t(latest_submitter.status) %> <% elsif submission.expired? %>
- + <%= t('expired') %>
@@ -158,7 +158,7 @@
<% if submitter.completed_at? && !is_submission_completed %> - + <%= svg_icon('download', class: 'w-4 h-4 stroke-2') %> @@ -171,14 +171,14 @@ <% elsif !template&.archived_at? && !submission.archived_at? && !is_submission_completed && !submission.expired? && !submitter.declined_at? %>
<% if current_user.email == submitter.email %> - + <%= svg_icon('writing_sign', class: "w-4 h-4 stroke-2 #{'md:hidden' if t('sign_now').length >= 12}") %> <% else %> - <%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug, host: form_link_host), class: 'relative btn btn-xs max-sm:normal-case text-xs btn-neutral text-white w-12 md:w-36 flex z-[1]', icon_class: 'w-4 h-4 text-white', copy_title: t('copy_link') %> + <%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug, host: form_link_host), class: 'relative btn btn-xs text-xs btn-neutral text-white w-12 md:w-36 flex z-[1]', icon_class: 'w-4 h-4 text-white', copy_title: t('copy_link') %> <% end %>
<% end %> @@ -192,7 +192,7 @@ <% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
- + <%= svg_icon('download', class: 'w-5 h-5 max-sm:w-4 max-sm:h-4 stroke-2') %> <%= t('download') %> @@ -206,7 +206,7 @@
<% elsif !template&.archived_at? && !submission.archived_at? && !submission.expired? && (sign_now_submitter = submitters.find { |item| !item.completed_at? && !item.declined_at? && current_user.email == item.email }) %> <% end %> diff --git a/app/views/templates/_template.html.erb b/app/views/templates/_template.html.erb index b3667f6d2..d677facb4 100644 --- a/app/views/templates/_template.html.erb +++ b/app/views/templates/_template.html.erb @@ -28,48 +28,50 @@

-