diff --git a/.gitignore b/.gitignore index a5e0e2d..e17c36b 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,8 @@ /public/assets +/dkim + # Ignore master key for decrypting credentials and more. /config/master.key /config/credentials.yml.enc diff --git a/Gemfile b/Gemfile index 628b25e..b05c0df 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,10 @@ gem 'kaminari' gem 'ancestry' gem 'acts_as_list' +gem 'date_validator' + +gem 'premailer-rails' +gem 'dkim' # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] # gem "kredis" diff --git a/Gemfile.lock b/Gemfile.lock index afcb1ee..e807050 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,10 +100,16 @@ GEM concurrent-ruby (1.2.3) connection_pool (2.4.1) crass (1.0.6) + css_parser (1.17.1) + addressable date (3.3.4) + date_validator (0.12.0) + activemodel (>= 3) + activesupport (>= 3) debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) + dkim (1.1.0) dotenv (3.1.0) dotenv-rails (3.1.0) dotenv (= 3.1.0) @@ -113,6 +119,7 @@ GEM ffi (1.16.3) globalid (1.2.1) activesupport (>= 6.1) + htmlentities (4.3.4) i18n (1.14.4) concurrent-ruby (~> 1.0) image_processing (1.12.2) @@ -185,6 +192,14 @@ GEM pg_search (2.3.6) activerecord (>= 5.2) activesupport (>= 5.2) + premailer (1.23.0) + addressable + css_parser (>= 1.12.0) + htmlentities (>= 4.0.0) + premailer-rails (1.12.0) + actionmailer (>= 3) + net-smtp + premailer (~> 1.7, >= 1.7.9) propshaft (0.8.0) actionpack (>= 7.0.0) activesupport (>= 7.0.0) @@ -241,7 +256,7 @@ GEM redis-client (0.22.1) connection_pool regexp_parser (2.9.0) - reline (0.5.2) + reline (0.5.3) io-console (~> 0.5) request_store (1.6.0) rack (>= 1.4) @@ -298,7 +313,9 @@ DEPENDENCIES bcrypt (~> 3.1.7) bootsnap capybara + date_validator debug + dkim dotenv-rails image_processing (~> 1.2) importmap-rails @@ -307,6 +324,7 @@ DEPENDENCIES mobility (~> 1.3.0.rc1) pg (~> 1.1) pg_search + premailer-rails propshaft puma (>= 5.0) rails (~> 7.1.3, >= 7.1.3.2) diff --git a/app/assets/fonts/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].woff2 b/app/assets/fonts/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].woff2 new file mode 100644 index 0000000..e92882f Binary files /dev/null and b/app/assets/fonts/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].woff2 differ diff --git a/app/assets/images/ico-h1.svg b/app/assets/images/ico-h1.svg new file mode 100644 index 0000000..923f83b --- /dev/null +++ b/app/assets/images/ico-h1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/ico-h2.svg b/app/assets/images/ico-h2.svg new file mode 100644 index 0000000..ae5e530 --- /dev/null +++ b/app/assets/images/ico-h2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/ico-h3.svg b/app/assets/images/ico-h3.svg new file mode 100644 index 0000000..4f46f42 --- /dev/null +++ b/app/assets/images/ico-h3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/ikea-foundation-white.svg b/app/assets/images/ikea-foundation-white.svg new file mode 100644 index 0000000..d0d99a0 --- /dev/null +++ b/app/assets/images/ikea-foundation-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/ikea-foundation.svg b/app/assets/images/ikea-foundation.svg new file mode 100644 index 0000000..e28bb85 --- /dev/null +++ b/app/assets/images/ikea-foundation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/stylesheets/admin.css b/app/assets/stylesheets/admin.css new file mode 100644 index 0000000..f2d4d04 --- /dev/null +++ b/app/assets/stylesheets/admin.css @@ -0,0 +1,274 @@ + + +@font-face { + font-family: 'Material Symbols Outlined'; + font-style: normal; + src: url('MaterialSymbolsRounded[FILL,GRAD,opsz,wght].woff2') format('woff2'); +} + + + +:root { + + --icon: #777; + --icon-active: #f1f1f1; + + --white: #e6edf3; + --black: #2d2d2d; + + --error: #b24226; + + --bg: #fff; + + --audit: #888; + + --inactive: #888; + --secondary: #888; + + --action: #3c85c6; + --call-to-action: #D65535; + --enabled: rgb(26, 134, 58); + --disabled: #b24226; + + --border: #ccc; + --hover: #f6f6f6; + + --popup-bg: #282a2c; + + --clr-black: #2d2d2d; + + --clr-grey-100: #f4f4f5; + --clr-grey-200: #e4e4e7; + --clr-grey-300: #d4d4d8; + --clr-grey-400: #a1a1aa; + --clr-grey-500: #71717a; + --clr-grey-600: #52525b; + --clr-grey-700: #3f3f46; + --clr-grey-750: #2e2f31; + + --font-icons: 'Material Symbols Outlined'; + --font-base: system-ui, sans-serif; + --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace; + + font: 10px/1.3 var(--font-base); + +} + +/*.material-symbols-outlined { + font-family: 'Material Symbols Outlined'; + font-weight: normal; + font-style: normal; + display: inline-block; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; +}*/ + +@keyframes rotate_animation { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +body { + margin: 0; + background: var(--bg, #fff); + color: var(--black, #000); + -webkit-font-smoothing: antialiased; + font-optical-sizing: auto; +} + + +.icon, .node__flags { + font-family: var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + + &.action { + color: var(--action); + } + + &.enabled { + color: var(--enabled); + } + + &.disabled { + color: var(--disabled); + } + + &.size--small { + font-size: 18px; + } + + &.size--medium { + font-size: 20px; + } +} + + +a.icon { + font-family: var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + font-weight: 400; + display: inline-flex; + justify-content: center; + align-items: center; + text-decoration: none; + width: 36px; + height: 36px; + border-radius: 50%; + color: var(--clr-grey-600); + + &:hover { + color: var(--black); + background-color: var(--clr-grey-200); + } +} + +button { + cursor: pointer; +} + +#navbar { + background: var(--clr-grey-750); + position: fixed; + z-index: 999; + width: 80px; + inset: 0 auto 0 0; + padding: 10px 0; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + gap: 20px; + + + &>div { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + } + + +} + +.navbar-link { + background: none; + border: none; + color: var(--icon); + text-decoration: none; + + & .icon { + font-size: 32px; + } + + &.open, + &.current { + .icon { + color: var(--icon-active); + } + } +} + +turbo-frame { + display: block; +} + +#main { + margin: 0 0 0 80px; + padding: 30px 40px; +} + + +h1, +.node-path { + font-size: 2.4rem; + line-height: 2; + font-weight: 400; + margin: 0 0 0 0; +} + +.list-title-link { + text-decoration: none; + color: var(--black); + padding: 0 0.6em; + border-radius: 999px; + display: inline-block; + appearance: none; + background-color: transparent; + border: none; + font-size: inherit; + line-height: inherit; +} + +a.list-title-link:hover { + background-color: var(--clr-grey-200); +} + + +#flash { + position: fixed; + left: 80px; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + gap: 0px; + font-size: 1.6rem; + z-index: 100; +} + +.flash__message { + color: var(--white, #fff); + background-color: rgba(119, 119, 119, 0.8); + animation: appear-then-fade 2s both; + padding: 10px 20px; + display: flex; + align-items: center; + gap: 8px; + + + & .icon { + color: #f1f1f1; + font-size: 3.2rem; + } +} + +@keyframes appear-then-fade { + + 0%, + 100% { + opacity: 0; + } + + 2%, + 90% { + opacity: 1; + } +} + +.logo { + display: block; + + transform-origin: center center; + width: 40px; + aspect-ratio: 40/368; + + + & svg { + rotate: 90deg; + display: block; + transform-origin: 0 0; + transform: translateY(-100%); + height: 40px; + } + +} + diff --git a/app/assets/stylesheets/assets.css b/app/assets/stylesheets/assets.css new file mode 100644 index 0000000..9ce584d --- /dev/null +++ b/app/assets/stylesheets/assets.css @@ -0,0 +1,134 @@ +#assets { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 10px; +} + +.asset { + text-align: center; +} + +.asset__thumbnail { + position: relative; + width: 100%; + margin-bottom: 12px; + + &::after { + content: ""; + display: block; + padding-bottom: 100%; + } + + & div { + position: absolute; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + } + + & img { + display: block; + max-width: 100%; + max-height: 100%; + box-shadow: rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 3px 1px -2px, rgba(0, 0, 0, 0.2) 0px 1px 5px 0px; + } +} + +.asset__title { + font-size: 1.4rem; + margin-bottom: 2px; + text-overflow: ellipsis; + white-space: nowrap; + word-break: normal; + overflow: hidden; +} + +.asset__mimetypes { + font-size: 1.2rem; + font-family: var(--font-mono); + color: var(--action); +} + +.assets-sort { + font-size: 1.4rem; + display: flex; + justify-content: flex-end; + gap: 1rem; + align-items: center; + + .popup-menu { + min-width: auto + } + + & a { + font-size: 1.4rem; + white-space: nowrap; + } +} + +.asset { + position: relative; + padding: 10px 10px 40px 10px; + border: 2px solid transparent; + + &:hover { + border-color: var(--clr-grey-200); + border-radius: 10px; + & .asset-ctrls { + bottom: 6px; + position: absolute; + left: 10px; + right: 10px; + display: flex; + justify-content: space-between; + } + } + +} + + + +.icon-cb-round { + position: absolute; + inset: 0 0 0 0; + cursor: pointer; + display: none; + + & span { + position: absolute; + right: 8px; + top: 8px; + width: 16px; + height: 16px; + outline: 2px solid var(--border); + outline-offset: 2px; + border-radius: 50%; + } + + & input { + position: absolute; + height: 0; + width: 0; + clip: rect(0,0,0,0); + + &:checked~span { + outline: 2px solid var(--action); + background-color: var(--action); + } + } +} + +#overlay { + & .icon-cb-round { + display: block; + } + + & .asset-ctrls { + display: none; + } + +} diff --git a/app/assets/stylesheets/attachments.css b/app/assets/stylesheets/attachments.css new file mode 100644 index 0000000..cb9f423 --- /dev/null +++ b/app/assets/stylesheets/attachments.css @@ -0,0 +1,142 @@ +#attachments { + display: flex; + flex-direction: column; + gap: 3.2rem; +} + +.attachment { + position: relative; + padding-top: 14px; + & label { + font-size: 1.4rem; + line-height: 1.2; + color: #555; + display: block; + margin-bottom: 8px; + } + + & trix-editor { + min-height: 15em; + /* max-height: 320px; + overflow: auto; */ + } + + + .handle { + appearance: none; + position: absolute; + top: 4px; + right: 40px; + border: none; + border-radius: 50%; + display: flex; + width: 32px; + height: 32px; + justify-content: center; + align-items: center; + font-family: var(--font-icons); + background-color: transparent; + font-size: 2.2rem; + color: var(--clr-grey-400); + + &:hover { + background-color: var(--clr-grey-200); + color: var(--clr-grey-700); + } + } +} + +.attachment__content:has(.attachment__asset) { + display: grid; + grid-template-columns: 120px 1fr; + gap: 0 20px; + + & .trix-button--icon-decrease-nesting-level, + .trix-button--icon-increase-nesting-level, + .trix-button--icon-bullet-list, + .trix-button--icon-number-list { + display: none; + } + + & trix-editor { + min-height: 8em; + } + + .attachment__content-two { + grid-column-start: 2; + } + .attachment__content-three { + grid-column-start: 2; + } + +} + +.attachment-site:not(:has(.attachment__asset)) { + & .attachment__content-three { + &>*:nth-child(3), + &>*:nth-child(4) { + display: none; + } + } +} + +.attachment-site:has(.attachment__asset) { + & .attachment__content-three { + &>*:nth-child(1), + &>*:nth-child(2) { + display: none; + } + } +} + +.attachment-tile { + & .attachment__content-three { + &>*:nth-child(4) { + display: none; + } + } +} + +#newsletter-attachments .attachment__content .field:nth-child(2) { + display: none; +} + +#newsletter-attachments .attachment__content:has(.attachment__asset) { + & .attachment__content-three { + display: none; + } +} + +.attachment__content .field { + display: flex; + flex-direction: column; + padding-bottom: 0; + + &>div:nth-child(2) { + flex-basis: 100%; + flex-grow: 1; + width: 100%; + } +} + +.attachment__content-two { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} +.attachment__content-three { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 0 20px; +} + + +.attachment__asset { + margin-top: 3em; + text-align: center; +} + + +.trix__field { + width: 100%; +} \ No newline at end of file diff --git a/app/assets/stylesheets/form.css b/app/assets/stylesheets/form.css new file mode 100644 index 0000000..3396e9a --- /dev/null +++ b/app/assets/stylesheets/form.css @@ -0,0 +1,158 @@ +.subscribe__container { + min-height: 30svh; +} + +.field { + display: flex; + flex-direction: column; + gap: 0.4em; + &>div:nth-child(1) { + & label { + font-size: 1.6rem; + line-height: 1.2; + color: var(--clr-grey-600); + } + } + + + & .field_with_errors { + --border: var(--clr-error); + } + + & + .field, + & + .input__fields-two { + margin-top: 1.2em; + } + + + &:has(input[type=submit]) { + margin: 2em 0; + display: block; + } +} + +input[type=submit] { + font-family: var(--ff-base); + font-size: 1.6rem; + background-color: var(--clr-grey-800); + color: var(--clr-white-200); + padding: 1em 1.2em; + border: none; + appearance: none; + border-radius: 2px; + cursor: pointer; +} + +.material__input { + display: flex; + font-family: var(--ff-base); + border: 1px solid var(--clr-border); + font-size: 1.6rem; + letter-spacing: 0.00625em; + border: none !important; + background-color: transparent; + width: 100%; + height: 100%; + appearance: none; + padding: 0; + box-shadow: none; + + + &:focus { + outline: none; + } + +} + +textarea.material__input { + resize: none; +} + +.input-box { + display: flex; + height: 46px; + width: 100%; + box-sizing: border-box; + align-items: center; + padding: 0 16px; + margin: 0; + overflow: visible; + -webkit-box-align: baseline; + --outline: var(--clr-action); + border-radius: 4px; + border: 1px solid var(--clr-border); + + & .field_with_errors { + display: flex; + height: 100%; + width: 100%; + } + + &:has(textarea) { + padding: 0 0 0 8px; + height: auto; + + & textarea { + line-height: 1.2; + margin: 1px 0; + padding: 8px 16px 8px 0; + } + } + + &:hover { + border-color: var(--clr-grey-500); + } + + &:focus-within { + outline: 2px solid var(--outline); + outline-offset: -2px; + } + + &:has(.field_with_errors) { + --outline: var(--clr-error); + + &:hover { + border-color: var(--clr-error); + } + } +} + +p[role="alert"] { + margin: 4px 0 0 0; + color: var(--clr-error); + font-size: 1.2rem; +} + +ul.errors { + color: var(--clr-error); + margin: 0 0 1.2em 0; +} + +p[role="tooltip"] { + margin: 4px 0 0 0; + color: var(--clr-grey-500); + font-size: 1.2rem; +} + + +.form__errors { + margin: 0 0 1.2em 0; + color: var(--clr-error); + padding: 0; + list-style: none; +} + + +.input__fields-two { + display: grid; + gap: 2em; + grid-template-columns: 1fr 2fr; + + & .field + .field { + margin-top: 0; + } +} + +.input__fields-two + .field { + margin-top: 1.2em; +} diff --git a/app/assets/stylesheets/forms.css b/app/assets/stylesheets/forms.css new file mode 100644 index 0000000..744c184 --- /dev/null +++ b/app/assets/stylesheets/forms.css @@ -0,0 +1,1061 @@ +#main:has(.attachment-container) { + padding: 0 0; + +} + + +.form-section, +.form-ctrls { + border-top: 1px solid var(--border); + margin: 1em 0; + padding: 3em 0 1em 0; +} + +.form-ctrls-overlay { + margin: 0; + padding: 1em 0; +} + +.form-header { + display: flex; + justify-content: space-between; + align-items: baseline; + + & h1 { + margin-top: 0.2em; + } +} + +.form-header__buttons { + display: flex; + gap: 8px; + +} + +.form-header__titled { + display: flex; + align-items: center; + gap: 30px; + + & .title-box { + flex-grow: 1; + } + & [data-icon] { + display: flex; + gap: 6px; + align-items: flex-start; + + &::before { + color: var(--data-icon-color, #717274); + font-family: var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + font-size: 3.2rem; + line-height: 1; + margin-top: 6px; + content: attr(data-icon); + } + } +} + +.form-header__attachment { + + & .title-box { + display: block; + margin: 1em 4px 1em -8px; + } + +} + + + + +.field { + max-width: 990px; + display: flex; + align-items: start; + padding-bottom: 2.4rem; + &>div:nth-child(1) { + padding-top: 12px; + flex-basis: 33%; + & label { + font-size: 1.4rem; + line-height: 1.2; + color: #555; + } + } + + & .button-container { + margin-top: 8px; + margin-left: -8px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + } + + &>div:nth-child(2) { + flex-grow: 0; + flex-basis: 66%; + } + + + + & .field_with_errors { + --border: var(--error); + } + + +} + +.i18n__input label, +.i18n__label { + &::after { + content: 'flag'; + font-family: var(--font-icons); + color: #EC5F59; + margin-left: 0.2em; + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + } +} + +.attachment label { + font-size: 1.4rem; + line-height: 1.2; + color: #555; +} + +.material__input { + display: flex; + font-family: var(--font-base); + border: 1px solid var(--border); + font-size: 1.6rem; + letter-spacing: 0.00625em; + border: none !important; + background-color: transparent; + width: 100%; + height: 100%; + appearance: none; + padding: 0; + box-shadow: none; + + &:focus { + outline: none; + } +} + + textarea.material__input { + resize: none; + } + +.material__input-select { + appearance: none; + position: relative; + padding-right: 24px; + padding-left: 4px; + background-image: url(data:image/svg+xml,%3Csvg%20width%3D%2210px%22%20height%3D%225px%22%20viewBox%3D%227%2010%2010%205%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%0A%20%20%20%20%3Cpolygon%20id%3D%22Shape%22%20stroke%3D%22none%22%20fill%3D%22%230%22%20fill-rule%3D%22evenodd%22%20opacity%3D%220.54%22%20points%3D%227%2010%2012%2015%2017%2010%22%3E%3C%2Fpolygon%3E%0A%3C%2Fsvg%3E); + background-repeat: no-repeat; + background-position: 100% 50%; + +} + +.date-fields { + display: grid; + grid-template-columns: 1fr 1fr 1fr 2fr; + gap: 20px; + + + & .field { + display: block; + padding: 0; + + &>div:nth-child(1), &>div:nth-child(2) { + padding-top: 0; + flex-basis: 1; + flex-grow: 1; + width: 100%; + } + + &>div:nth-child(1) { + margin-bottom: 2px; + } + } + + &[data-n='0'] { + &>div:nth-child(2), &>div:nth-child(3) { + & label { + color: var(--clr-grey-200); + } + } + } + &[data-n='1'] { + &>div:nth-child(3) { + & label { + color: var(--clr-grey-200); + } + } + } +} + + +.title-box { + + & div { + display: flex; + width: 100%; + flex-direction: column; + } + + + & input[type="text"] { + display: flex; + flex: 4; + + font-family: var(--font-base); + border: 1px solid var(--border); + font-size: 2.4rem; + line-height: 2; + font-weight: 400; + letter-spacing: 0.00625em; + border: none !important; + background-color: transparent; + + height: 100%; + appearance: none; + padding: 8px; + border-radius: 4px; + box-shadow: none; + + &:focus { + outline: 2px solid var(--action); + outline-offset: -2px; + } + } +} + +.input-box { + display: flex; + height: 46px; + width: 100%; + box-sizing: border-box; + align-items: center; + padding: 0 16px; + margin: 0; + overflow: visible; + -webkit-box-align: baseline; + --outline: var(--action); + border-radius: 4px; + border: 1px solid var(--border); + + & .field_with_errors { + display: flex; + height: 100%; + width: 100%; + } + + &:has(textarea) { + padding: 0 0 0 8px; + height: auto; + + & textarea { + line-height: 1.2; + margin: 1px 0; + padding: 8px 16px 8px 0; + } + } + + &:hover { + border-color: var(--inactive); + } + + &:focus-within { + outline: 2px solid var(--outline); + outline-offset: -2px; + } + + &:has(.field_with_errors) { + --outline: var(--error); + + &:hover { + border-color: var(--error); + } + } +} + +.input-box-url { + + & .base__url { + flex-shrink: 0; + } + + & span { + font-size: 1.6rem; + letter-spacing: 0.00625em; + color: var(--clr-grey-400); + + } +} + +.i18n__input, +.title-box .i18n__input { + display: none; +} + +form[data-locale='da'] .i18n__input-da, +form[data-locale='en'] .i18n__input-en, +form[data-locale='de'] .i18n__input-de { + display: flex; +} + + +.icon-cb, +.icon-rb { + display: block; + position: relative; + cursor: pointer; + user-select: none; + + & svg rect { + fill: #ccc !important; + } + + & input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; + + &:checked~.cm { + color: #ccc; + } + + &:checked~svg rect { + fill: #585857 !important; + } + } + + &:focus-within { + .cm { + outline: 2px solid var(--action); + outline-offset: -4px; + } + } + + & .cm { + color: #ccc; + display: block; + height: 28px; + width: 28px; + font: 28px/1 var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + } + + &.toogle { + & .cm>*:nth-child(2) { + display: none; + } + + & .cm { + color: var(--border); + } + + & input { + &:checked~.cm { + color: var(--action); + + &>*:nth-child(1) { + display: none; + } + + &>*:nth-child(2) { + display: inline; + } + } + &:disabled~.cm { + color: var(--clr-grey-500); + } + } + } +} + +p[role="alert"] { + margin: 4px 0 0 0; + color: var(--error); + font-size: 1.2rem; +} + +.icon-rb { + & input { + &:checked~.cm { + color: #000; + background: #ccc; + border-radius: 50%; + } + } + + & .cm { + color: #ccc; + display: block; + text-align: center; + text-transform: uppercase; + height: 24px; + width: 24px; + font: 400 1rem/24px "Roboto Mono", monospace; + } +} + +.form-ctrls input[type="submit"], +.form-ctrls button { + font-size: 1.5rem; + letter-spacing: 0.00625em; + padding: 0.6em 1.4em; + background: var(--action); + border-radius: 4px; + color: var(--icon-active); + text-decoration: none; + font-family: var(--base); + border: none; + cursor: pointer; + box-sizing: border-box; +} + +.btn { + display: inline-flex; + font-size: 1.4rem; + letter-spacing: 0.00625em; + padding: 0 8px 0 8px; + border-radius: 8px; + text-decoration: none; + color: var(--action); + position: relative; + align-items: center; + line-height: 32px; + + &:hover { + background-color: var(--clr-grey-100); + } +} + + + +.add-btn, .icon-only-btn, .action-btn { + font-size: 1.6rem; + letter-spacing: 0.00625em; + padding: 0 14px 0 32px; + border-radius: 12px; + text-decoration: none; + color: var(--black); + position: relative; + align-items: center; + line-height: 48px; + box-shadow: 0px 0px 1.5px rgba(0, 0, 0, 0.105), 0px 1px 3px rgba(0, 0, 0, 0.21); + background-color: transparent; + border: none; + &:hover { + background-color: var(--clr-grey-200); + } +} + +.icon-btn { + display: inline-flex; + font-size: 1.4rem; + letter-spacing: 0.00625em; + padding: 0 8px 0 24px; + color: var(--black); + position: relative; + align-items: center; + line-height: 32px; + background-color: transparent; + border: none; + & span::before { + position: absolute; + content: attr(data-icon); + left: 2px; + font-family: var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + font-size: 2.0rem; + line-height: 32px; + } +} + +.icon-only-btn { + display: inline-flex; + font-size: 1.4rem; + letter-spacing: 0.00625em; + padding: 0 8px; + color: var(--black); + position: relative; + align-items: center; + font-family: var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + font-size: 3.2rem; + line-height: 32px; + + &:disabled { + color: var(--clr-grey-400); + cursor: not-allowed; + &:hover { + background-color: transparent; + } + } +} + +.add-btn { + & span::before { + position: absolute; + content: 'add'; + left: 2px; + font-family: var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + font-size: 3.2rem; + line-height: 48px; + } +} + +.action-btn { + & span::before { + position: absolute; + content: attr(data-icon); + left: 2px; + font-family: var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + font-size: 2.8rem; + line-height: 48px; + } +} + +.form-ctrls { + display: flex; + justify-content: end; + align-items: center; + gap: 20px; +} + +.form-nav-links { + display: flex; + justify-content: space-between; + margin-bottom: 1rem; + + +} + +.i18n__from-ctrls { + display: flex; + gap: 4px; + & label { + font-size: 1.5rem; + cursor: pointer; + text-transform: uppercase; + letter-spacing: 0.00625em; + } + & span { + border-radius: 50%; + display: inline-block; + width: 32px; + height: 32px; + line-height: 32px; + text-align: center; + user-select: none; + } + & input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + + &:checked + span { + background-color: var(--clr-grey-600); + color: var(--white); + } + } +} + +.form-ctrls a, +.delete-link { + color: var(--secondary); + text-decoration: none; + font-size: 1.5rem; + letter-spacing: 0.00625em; + appearance: none; + background: none; + border: none; +} + +.dropzone { + width: 80px; + height: 80px; + display: flex; + justify-content: center; + align-items: center; + margin-top: 1em; + box-sizing: border-box; + cursor: pointer; + + border: 2px dotted var(--clr-grey-200); + border-radius: 8px; + + & input[type=file] { + display: none; + } + + &:hover { + box-shadow: 0px 0px 1.5px rgba(0, 0, 0, 0.105), 0px 1px 3px rgba(0, 0, 0, 0.21); + border-color: var(--clr-grey-200); + background-color: var(--clr-grey-200); + + & span { + color: var(--clr-black); + } + } + + + & span { + color: var(--clr-grey-500); + font-family: var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + + font-size: 4.2rem; + } +} + +.dropzone--small { + margin-top: 0; + width: 48px; + height: 48px; + + & span { + font-size: 3.2rem; + } +} + +.drag-over { + border-color: var(--clr-grey-400); + border-style: solid; + background-color: var(--clr-grey-200); + + & span { + color: var(--clr-black); + } +} + +#attachments { + gap: 0; +} + + + +.file-details { + text-align: center; + font: 1.2rem var(--font-mono); + +} + +.file-name { + hyphens: auto; + word-break: break-word; + word-wrap: break-word; + overflow-wrap: break-word; + margin-bottom: 2px; +} + +.file-data { + color: var(--action); +} + +.file-data-large { + font: 1.4rem var(--font-mono); +} + +.asset-ctrls { + display: none; + gap: 4px; + justify-content: center; + font: 2.4rem var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + + & > div { + display: flex; + gap: 4px; + } + + & a, & .handle { + display: inline-flex; + width: 28px; + height: 28px; + justify-content: center; + align-items: center; + border-radius: 50%; + text-decoration: none; + color: var(--clr-grey-400); + + &:hover { + background-color: var(--clr-grey-200); + color: var(--clr-grey-600); + } + } +} + + +.asset-thumbnail { + position: relative; + width: 100%; + + & img { + position: absolute; + max-width: 100%; + max-height: 100%; + box-shadow: rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 3px 1px -2px, rgba(0, 0, 0, 0.2) 0px 1px 5px 0px; + } + + &::after { + content: ""; + display: block; + padding-bottom: 100%; + } +} + +.image-container { + position: absolute; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + display: flex; + align-items: center; + justify-content: center; +} + +#node-tags { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 20px; + + font-size: 1.6rem; + + & label { + font-size: 1.4rem; + color: #555; + } + +} + +#overlay { + position: fixed; + inset: 0 0 0 0; + background-color: rgba(32,33,36,0.6); + z-index: 1100; + +} + + +.inner-window-container { + position: fixed; + inset: 50px 0 0 0; + overflow: hidden; + background-color: var(--bg); + border-radius: 20px 20px 0 0; +} + +#inner-window { + position: absolute; + inset: 60px 40px 0 40px; + display: flex; + flex-direction: column; + + & .list-container { + display: flex; + flex: 1 1 auto; + flex-direction: column; + } + + & .list { + display: flex; + flex: 1 1 auto; + flex-direction: column; + padding: 0; + + } + + & .assets__container { + flex: 1 1 0; + overflow-y: scroll; + } +} + + +.close-overlay { + position: absolute; + top: 20px; + right: 20px; + font: 3.2rem/36px var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + display: inline-flex; + justify-content: center; + align-items: center; + width: 36px; + height: 36px; + border-radius: 50%; + color: var(--clr-black); + border: none; + background-color: transparent; + + &:hover { + color: var(--black); + background-color: var(--clr-grey-200); + } +} + +.tag-context { + & .tag-container { + margin-top: 2px; + } +} + +.tag-container { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.tag { + font: 1.4rem/1.4 var(--font-mono); + background: var(--clr-grey-300); + padding: 2px 6px 3px 6px; + border-radius: 4px; + text-decoration: none; + color: var(--black); +} + +.parent__tag { + background: var(--clr-grey-500); + color: var(--clr-grey-100); +} + +.trix-button-group--file-tools { + display: none !important; +} + +trix-toolbar, trix-editor { + font-size: 1.6rem; +} + +trix-editor { + + & h1 { + font-weight: 600; + font-size: 2.4rem; + line-height: 1.25; + margin: 0 0 0.4em 0; + } + + & h2 { + font-weight: 600; + font-size: 1.8rem; + line-height: 1.25; + margin: 0 0 0.4em 0; + } + + & ul, ol { + display: block; + margin-block-start: 1em; + margin-block-end: 1em; + padding-inline-start: 1.2em; + } + & ul { + list-style-type: disc; + } + & ol { + list-style-type: decimal; + } + + & a { + color: blue; + } + + & pre { + background-color: #eee; + padding: 0.5em; + } + + & blockquote { + margin: 1em 0; + padding: 0.5em; + background-color: #eee; + &::before { + content: '"'; + color: #999; + } + &::after { + content: '"'; + color: #999; + } + } + + +} + +.trix-button--icon-heading-1::before { + background-image: url("ico-h1.svg") !important; +} + +.trix-button--icon-heading-2::before { + background-image: url("ico-h2.svg"); +} + +.trix-button--icon-heading-3::before { + background-image: url("ico-h3.svg"); +} + + +.attachment-container { + padding-left: calc((100vw - 80px) / 2); + margin: 30px 40px; +} + +.image-viewer { + position: fixed; + left: 80px; + top: 0; + bottom: 0; + width: calc((100vw - 80px) / 2); + border-right: 1px solid var(--border); +} + + + + +#scan-result { + font-size: 1.6rem; + border-radius: 4px; + + &:has(div) { + background-color: var(--clr-grey-100); + margin-top: 1em; + padding: 1em; + } + + + & > div { + tab-size: 4; + text-align: left; + white-space: pre; + width: auto; + word-break: normal; + word-spacing: 0px; + } + + +} + + +.date-formatted { + font-size: 1.6rem; + margin-top: 12px; +} + + + +ul { + margin: 0; + padding: 0; + list-style: none; + +} + +.node__status { + font-size: 1.6rem; + letter-spacing: 0.00625em; + line-height: 1.2; + + & li+li { + margin-top: 0.4em; + } + + li { + display: flex; + gap: 0.4em; + } +} + +.datetime__select { + display: flex; + justify-content: flex-start; + align-items: center; + gap: 0.6em; + font-size: 1.6rem; + letter-spacing: 0.00625em; + + select { + height: 3.2rem; + &:focus { + &:focus { + outline: 2px solid var(--action); + } + + } + } + + & button { + color: var(--data-icon-color, #717274); + font-family: var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + font-size: 2.8rem; + line-height: 1.142857142857143; + appearance: none; + background: none; + border: none; + padding: 0; + } + + & select { + width: auto; + padding-right: 20px; + } + + &.with-toggles { + & button:nth-of-type(1) { + display: none; + } + & button:nth-of-type(2) { + display: inline; + } + + &:has(select:disabled) { + + & span, select { + display: none; + } + + & button:nth-of-type(1) { + display: inline; + } + & button:nth-of-type(2) { + display: none; + } + } + + } +} + +.ts-wrapper { + flex-grow: 1; + align-self: center; +} + +.ts-control { + border: none; + padding: 8px 0; +} + +.ts-dropdown, .ts-control, .ts-control input { + font-size: 1.6rem; +} + +.ts-dropdown .active { + background-color: var(--clr-grey-300); + color: var(--black); +} + +.ts-wrapper.multi .ts-control > div { + background-color: var(--clr-grey-300); +} \ No newline at end of file diff --git a/app/assets/stylesheets/lists.css b/app/assets/stylesheets/lists.css new file mode 100644 index 0000000..ca3efb4 --- /dev/null +++ b/app/assets/stylesheets/lists.css @@ -0,0 +1,436 @@ +.entries-info { + font-size: 2.2rem; + font-weight: 500; + margin: 0; +} + +.audit-date, .user__role { + color: var(--audit); + font-size: 1.4rem; +} + +.date { + color: var(--audit); + font-size: 1.4rem; +} + +.list-container { + padding-top: 0em; + margin-top: 2em; +} + +.list-ctrls { + display: flex; + justify-content: space-between; + align-items: center; +} + +.list { + padding: 2em 0; +} + +.list-small { + padding: 0; + & .list-header, & .row, & .audit-date { + font-size: 1.2rem; + } + + & .icon { + font-size: 1.8rem; + } +} + +.list-header, +.row { + display: flex; + justify-content: stretch; + align-items: center; +} + +.list-header { + font-size: 1.4rem; + line-height: 1.2; + color: var(--black); + min-height: 38px; + border-bottom: 1px solid var(--border); + + +} + +.sort_link { + font-size: 1.4rem; + text-decoration: none; + color: var(--black); + display: inline-flex; + align-items: center; + gap: 0.4em; + border-radius: 999px; + padding: 0.4em 1em; + margin-left: -1em; + + &:hover { + background-color: var(--clr-grey-200); + } + + &.current { + position: relative; + color: #000; + + &::after { + content: "arrow_downward"; + font: 1.6rem var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + color: var(--black); + } + + &.desc::after { + content: "arrow_upward"; + } + } +} + +.popup-menu .sort_link { + border-radius: 0px; + margin-left: 0; +} + +.row { + border-bottom: 1px solid var(--border); + min-height: 48px; + box-sizing: border-box; + padding: 4px 0; + font-size: 1.6rem; + + & .actions { + + & a, + button { + display: none; + } + } + + &.sortable-drag { + border-bottom: none; + background-color: var(--bg); + + & .handle { + display: inline-flex; + } + } + +} + +.handle { + font-family: var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + display: none; + font-size: 20px; + justify-content: center; + align-items: center; + text-decoration: none; + width: 36px; + height: 36px; + border-radius: 50%; + cursor: move; + + &:hover { + background-color: var(--clr-grey-200); + } +} + +.list:not(:has(.row.sortable-chosen)) { + & .row:hover { + background-color: var(--hover); + + & .actions { + & a, + button, + .handle { + display: inline-flex; + flex-shrink: 0; + } + } + } +} + +.cell { + padding: 0 8px; + flex: 1 1 0px; + + & ul { + margin: 8px 0; + padding: 0; + list-style: none; + + & li { + display: flex; + align-items: center; + gap: 0.4em; + } + } + + &.flags { + flex: 0 0 auto; + width: 40px; + } + + &.with-cb { + width: 40px; + flex: 0 0 auto; + } + + &.mono { + font-size: 1.2rem; + font-family: var(--font-mono); + } + + &.actions { + flex: 0 0 auto; + text-align: right; + width: 72px; + display: flex; + align-items: center; + justify-content: flex-end; + } + + &.actions-one { + width: 40px; + } + +} + +.node__flags { + font-size: 20px; + color: var(--clr-grey-400); + font-variation-settings: 'FILL' 0; + display: flex; + gap: 0; +} + + + +.back-link { + color: var(--secondary); + text-decoration: none; + font-size: 1.6rem; + display: inline-flex; + justify-content: center; + align-items: center; + + &::before { + content: "arrow_back"; + font: 1.6rem/1 var(--font-icons); + color: var(--secondary); + margin-right: 0.4em; + } +} + +.pagination-container { + color: var(--inactive); + padding: 20px 0; + display: flex; + justify-content: end; + align-items: center; + gap: 20px; + font-size: 1.4rem; +} + +.pagination { + font: normal 3.2rem/40px var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + display: flex; + height: 40px; + margin-right: 8px; + align-items: center; + + & span { + color: var(--border); + width: 40px; + height: 40px; + } + + & a { + display: block; + width: 40px; + height: 40px; + text-align: center; + color: var(--secondary); + text-decoration: none; + + &:hover { + color: var(--black); + } + } +} + + + +.category-selector { + font-size: 1.5rem; + line-height: 1.6; + display: flex; + flex-wrap: wrap; + gap: 0.4em; + + & a { + color: var(--black, #000); + text-decoration: none; + padding: 0.4em 0.8em; + border-radius: 999px; + background-color: var(--clr-grey-200); + + &.current { + background-color: var(--clr-grey-750, #000); + color: var(--white, #FFF); + + & span::before { + content: 'done'; + font-family: var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + font-size: 2.4rem; + line-height: 1; + margin-right: 2px; + vertical-align: bottom; + } + } + } + +} + +.list-title { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-left: -0.6em; +} + + +.node-path { + display: flex; + flex-wrap: wrap; + + gap: 0.2em; + + &>* { + height: 48px; + } + + & .list-title-link[data-icon] { + display: inline-flex; + gap: 6px; + align-items: center; + + &::before { + color: var(--data-icon-color, #717274); + font-family: var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + font-size: 3.2rem; + line-height: 1; + content: attr(data-icon); + } + } + + & .has-popup-menu { + &::after { + color: var(--black); + font-family: var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + font-size: 2.8rem; + line-height: 32px; + width: 20px; + overflow: visible; + content: 'expand_more'; + } + } +} + + + + +.list-title .flex { + display: flex; + gap: 1em; + + & form { + padding: 0; + display: flex; + + align-items: center; + gap: 4px; + + &>div { + display: flex; + flex-shrink: 0; + flex-grow: 1; + height: 48px; + background-color: var(--clr-grey-200); + border-radius: 999px; + padding: 0 4px 0 1em; + justify-content: start; + align-items: center; + gap: 8px; + max-width: 300px; + + &:focus-within { + outline: 2px solid var(--action); + } + } + + & input[type="text"] { + background-color: transparent; + border: none; + flex-grow: 1; + + font-size: 1.6rem; + line-height: 36px; + height: 36px; + padding: 0 0.4em; + outline: none; + } + + & button, + a { + font-size: 28px; + line-height: 40px; + text-align: center; + width: 40px; + height: 40px; + padding: 0; + border: none; + background: none; + color: var(--inactive); + outline: none; + border-radius: 50%; + + &:focus { + background-color: var(--clr-grey-300); + color: var(--black); + } + + &:hover { + background-color: var(--clr-grey-300); + color: var(--black); + } + } + } +} + +.search__collection:has(input:placeholder-shown) { + & .reset__search { + display: none; + } +} + + +#load-more { + height: 1em; +} + +.utm { + margin-top: 8px; + font-size: 1.2rem; + font-family: var(--font-mono); + & .tag { + font-size: 1.2rem; + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/nodes.css b/app/assets/stylesheets/nodes.css new file mode 100644 index 0000000..194af39 --- /dev/null +++ b/app/assets/stylesheets/nodes.css @@ -0,0 +1,238 @@ +#drawer { + position: fixed; + top: 0; + left: 80px; + width: 320px; + bottom: 0; + padding: 16px 0; + background-color: #28292A; + color: var(--white, #FFF); + overflow: auto; + + &::-webkit-scrollbar { + width: 8px; + } + + &::-webkit-scrollbar-track { + background-color: #28292A; + } + + &::-webkit-scrollbar-thumb { + background-color: #BDC1C6; + outline: 1px solid #BDC1C6; + border-radius: 8px; + } + + & h1 { + margin: 0; + font: 1.4rem/1 var(--font-base); + } + + &:hover { + --clr-spacer: #444; + } +} + +#drawer-structure { + font-size: 1.4rem; + line-height: 1.5; + margin: 0 8px; +} + +.node-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + padding: 8px 10px; +} + +.node { + & a { + color: var(--white, #fff); + text-decoration: none; + } + + &.current>.node-row:nth-child(1) { + background-color: #37383A; + } +} + +.node-row { + --level: 0; + --toggle-width: 1.6rem; + + display: grid; + grid-template-columns: calc(var(--level) * (var(--toggle-width) / 2)) var(--toggle-width) 1fr; + grid-template-areas: "spacer toggle content"; + + min-height: 3.2rem; + padding: 0 10px 0 0; + cursor: pointer; + + &:hover { + background-color: #37383A; + border-radius: 4px; + } + + + .child { + color: var(--clr-grey-400); + font: 1.8rem/1 var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + grid-area: toggle; + display: flex; + align-items: center; + justify-content: center; + } + + .parent { + + &:hover { + + background-color: var(--clr-grey-600); + border-radius: 4px 0 0 4px; + } + + & span:nth-child(1) { + display: inherit; + } + + & span:nth-child(2) { + display: none; + } + } + + &.closed { + .parent { + + & span:nth-child(1) { + display: none; + } + + & span:nth-child(2) { + display: inherit; + } + } + } + +} + +.node-title { + display: flex; + align-items: center; + gap: 4px; + grid-area: content; + letter-spacing: 0.00625em; + text-overflow: ellipsis; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-break: normal; + + &::before { + color: var(--data-icon-color, #717274); + font-family: var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + font-size: 2.0rem; + content: attr(data-icon); + } +} + +.node-link { + text-decoration: none; + color: var(--black); + display: flex; + align-items: center; + gap: 8px; + + &:not(:has(img))::before { + color: var(--data-icon-color, #717274); + font-family: var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + font-size: 2.4rem; + line-height: 1.5; + content: attr(data-icon); + } + + &:has(img) { + & img { + width: 24px; + height: 36px; + object-fit: contain; + } + } + +} + +[data-icon=inventory_2] { + --data-icon-color: #886F65; +} +[data-icon=folder] { + --data-icon-color: #EC5F59; +} +[data-icon=book_2] { + --data-icon-color: #F8CA4F; +} +[data-icon=description] { + --data-icon-color: #94A3AD; +} + + + + + + +.spacer { + grid-area: spacer; + display: flex; + justify-content: stretch; + + & div { + width: 100%; + height: 100%; + border-right: 1px solid var(--clr-spacer, transparent); + } + +} + +.closed~.node { + display: none; +} + +.node-ctrls { + display: flex; + gap: 8px; + + & button { + font: 1.6rem/1 var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + } +} + +#tree { + margin-left: 320px; +} + +.tree-title { + font-size: 1.6rem; + font-weight: 600; + display: flex; + justify-content: start; + gap: 0.2em; + + & a { + color: var(--action); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } +} + +.tree-children { + + font-size: 1.6rem; +} \ No newline at end of file diff --git a/app/assets/stylesheets/popup-menu.css b/app/assets/stylesheets/popup-menu.css new file mode 100644 index 0000000..f9b3b0b --- /dev/null +++ b/app/assets/stylesheets/popup-menu.css @@ -0,0 +1,86 @@ +.popup-menu { + position: absolute; + display: none; + background-color: var(--bg); + padding: 0; + overflow: hidden; + border-radius: 4px; + font-size: 1.6rem; + line-height: 1.2; + min-width: 240px; + + &.open { + display: block; + z-index: 1000; + box-shadow: 0px 0px 1.5px rgba(0, 0, 0, 0.105), 0px 1px 3px rgba(0, 0, 0, 0.21); + } + + & ul { + margin: 0; + padding: 0; + list-style: none; + + & .with-icon { + display: flex; + justify-content: space-between; + } + } + + & li>* { + display: flex; + align-items: center; + justify-content: space-between; + } + + & li>div { + padding: 2px 12px; + min-height: 40px; + } + + + + + & form.button_to { + display: block; + } + + & a, button[type="submit"] { + display: flex; + appearance: none; + border: none; + font-size: 1.6rem; + justify-content: flex-start; + flex-grow: 1; + align-items: center; + color: var(--black, #000); + text-decoration: none; + gap: 0.4em; + padding: 2px 12px; + min-height: 40px; + + &[data-icon]::before { + color: var(--data-icon-color); + font-family: var(--font-icons); + font-variation-settings: 'opsz' 24, 'wght' 300, 'FILL' 1, 'GRAD' 0; + font-size: 2.4rem; + line-height: 1; + content: attr(data-icon); + } + + & .icon { + font-size: 2.8rem; + color: var(--clr-grey-800); + } + } + + & a, button[type="submit"] { + &:hover { + background-color: var(--clr-grey-300); + } + } + + & button[type="submit"] { + background-color: transparent; + width: 100%; + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/sessions.css b/app/assets/stylesheets/sessions.css new file mode 100644 index 0000000..e308ad9 --- /dev/null +++ b/app/assets/stylesheets/sessions.css @@ -0,0 +1,172 @@ +:root { + font: 400 10px/1.3 system-ui, sans-serif; + --red: #B24226; + --border: #ccc; + --font-base: system-ui, sans-serif; +} + +body { + background: #E4E4E4; + display: flex; + align-items: center; + justify-content: center; + margin: 0; +} + +html, +body { + height: 100%; +} + +main { + max-width: 360px; + width: 100%; + background: white; + padding: 40px 32px 40px 32px; + font-size: 1.6rem; + box-shadow: 0 3px 3px rgba(0, 0, 0, 0.2); + display: flex; + flex-direction: column; + gap: 20px; +} + + +svg { + display: block; + width: 100%; + height: auto; + margin: 0 auto; +} + +label { + font-size: 1.4rem; + color: #666; + order: 0; +} + + +input[type="text"], +input[type="password"] { + display: flex; + border: 1px solid var(--border); + font-family: var(--font-base); + font-size: 1.6rem; + letter-spacing: 0.00625em; + border: none !important; + background-color: transparent; + width: 100%; + height: 100%; + appearance: none; + padding: 0; + box-shadow: none; + + &:focus { + outline: none; + } +} + +.input-box { + display: flex; + height: 46px; + width: 100%; + box-sizing: border-box; + align-items: baseline; + padding: 0 16px; + margin: 8px 0 0 0; + overflow: visible; + -webkit-box-align: baseline; + --outline: var(--action); + border-radius: 4px; + border: 1px solid var(--border); + + & .field_with_errors { + display: flex; + height: 100%; + width: 100%; + } + + + &:hover { + border-color: var(--inactive); + } + + &:focus-within { + outline: 2px solid var(--outline); + outline-offset: -2px; + } + + &:has(.field_with_errors) { + --outline: var(--error); + + &:hover { + border-color: var(--error); + } + } +} + + +button[type=submit] { + background: #333; + border: none; + outline: none; + font: 400 1.6rem system-ui, sans-serif; + padding: 8px 20px; + border-radius: 4px; + cursor: pointer; + color: #FFF; + + &:active { + background-color: #999; + } +} + +p[role=alert] { + margin: 4px 0 0 0; + color: var(--red); + font-size: 1.4rem; + order: 3; +} + +form { + margin: 0; + + &>div { + margin-top: 32px; + display: flex; + flex-direction: column; + + &:last-of-type { + text-align: right; + display: block; + } + + } + + +} + +.with-errors { + & label { + color: var(--red); + } + + & input[type=text], + input[type=password] { + border-color: var(--red); + + &:focus { + border-color: var(--red); + + &+label { + color: var(--red); + } + } + } +} + +footer { + text-align: right; + margin-top: 40px; + font-size: 1.4rem; + color: #666; +} \ No newline at end of file diff --git a/app/assets/stylesheets/tom-select.css b/app/assets/stylesheets/tom-select.css new file mode 100644 index 0000000..5634e64 --- /dev/null +++ b/app/assets/stylesheets/tom-select.css @@ -0,0 +1,412 @@ +/** + * tom-select.css (v2.3.1) + * Copyright (c) contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + * + */ +.ts-control { + border: 1px solid #d0d0d0; + padding: 8px 8px; + width: 100%; + overflow: hidden; + position: relative; + z-index: 1; + box-sizing: border-box; + box-shadow: none; + border-radius: 3px; + display: flex; + flex-wrap: wrap; +} +.ts-wrapper.multi.has-items .ts-control { + padding: calc(8px - 2px - 0) 8px calc(8px - 2px - 3px - 0); +} +.full .ts-control { + background-color: #fff; +} +.disabled .ts-control, .disabled .ts-control * { + cursor: default !important; +} +.focus .ts-control { + box-shadow: none; +} +.ts-control > * { + vertical-align: baseline; + display: inline-block; +} +.ts-wrapper.multi .ts-control > div { + cursor: pointer; + margin: 0 3px 3px 0; + padding: 2px 6px; + background: #f2f2f2; + color: #303030; + border: 0 solid #d0d0d0; +} +.ts-wrapper.multi .ts-control > div.active { + background: #e8e8e8; + color: #303030; + border: 0 solid #cacaca; +} +.ts-wrapper.multi.disabled .ts-control > div, .ts-wrapper.multi.disabled .ts-control > div.active { + color: #7d7d7d; + background: white; + border: 0 solid white; +} +.ts-control > input { + flex: 1 1 auto; + min-width: 7rem; + display: inline-block !important; + padding: 0 !important; + min-height: 0 !important; + max-height: none !important; + max-width: 100% !important; + margin: 0 !important; + text-indent: 0 !important; + border: 0 none !important; + background: none !important; + line-height: inherit !important; + -webkit-user-select: auto !important; + -moz-user-select: auto !important; + -ms-user-select: auto !important; + user-select: auto !important; + box-shadow: none !important; +} +.ts-control > input::-ms-clear { + display: none; +} +.ts-control > input:focus { + outline: none !important; +} +.has-items .ts-control > input { + margin: 0 4px !important; +} +.ts-control.rtl { + text-align: right; +} +.ts-control.rtl.single .ts-control:after { + left: 15px; + right: auto; +} +.ts-control.rtl .ts-control > input { + margin: 0 4px 0 -2px !important; +} +.disabled .ts-control { + opacity: 0.5; + background-color: #fafafa; +} +.input-hidden .ts-control > input { + opacity: 0; + position: absolute; + left: -10000px; +} + +.ts-dropdown { + position: absolute; + top: 100%; + left: 0; + width: 100%; + z-index: 10; + border: 1px solid #d0d0d0; + background: #fff; + margin: 0.25rem 0 0; + border-top: 0 none; + box-sizing: border-box; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + border-radius: 0 0 3px 3px; +} +.ts-dropdown [data-selectable] { + cursor: pointer; + overflow: hidden; +} +.ts-dropdown [data-selectable] .highlight { + background: rgba(125, 168, 208, 0.2); + border-radius: 1px; +} +.ts-dropdown .option, +.ts-dropdown .optgroup-header, +.ts-dropdown .no-results, +.ts-dropdown .create { + padding: 5px 8px; +} +.ts-dropdown .option, .ts-dropdown [data-disabled], .ts-dropdown [data-disabled] [data-selectable].option { + cursor: inherit; + opacity: 0.5; +} +.ts-dropdown [data-selectable].option { + opacity: 1; + cursor: pointer; +} +.ts-dropdown .optgroup:first-child .optgroup-header { + border-top: 0 none; +} +.ts-dropdown .optgroup-header { + color: #303030; + background: #fff; + cursor: default; +} +.ts-dropdown .active { + background-color: #f5fafd; + color: #495c68; +} +.ts-dropdown .active.create { + color: #495c68; +} +.ts-dropdown .create { + color: rgba(48, 48, 48, 0.5); +} +.ts-dropdown .spinner { + display: inline-block; + width: 30px; + height: 30px; + margin: 5px 8px; +} +.ts-dropdown .spinner::after { + content: " "; + display: block; + width: 24px; + height: 24px; + margin: 3px; + border-radius: 50%; + border: 5px solid #d0d0d0; + border-color: #d0d0d0 transparent #d0d0d0 transparent; + animation: lds-dual-ring 1.2s linear infinite; +} +@keyframes lds-dual-ring { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.ts-dropdown-content { + overflow: hidden auto; + max-height: 200px; + scroll-behavior: smooth; +} + +.ts-wrapper.plugin-drag_drop .ts-dragging { + color: transparent !important; +} +.ts-wrapper.plugin-drag_drop .ts-dragging > * { + visibility: hidden !important; +} + +.plugin-checkbox_options:not(.rtl) .option input { + margin-right: 0.5rem; +} + +.plugin-checkbox_options.rtl .option input { + margin-left: 0.5rem; +} + +/* stylelint-disable function-name-case */ +.plugin-clear_button { + --ts-pr-clear-button: 1em; +} +.plugin-clear_button .clear-button { + opacity: 0; + position: absolute; + top: 50%; + transform: translateY(-50%); + right: calc(8px - 6px); + margin-right: 0 !important; + background: transparent !important; + transition: opacity 0.5s; + cursor: pointer; +} +.plugin-clear_button.form-select .clear-button, .plugin-clear_button.single .clear-button { + right: max(var(--ts-pr-caret), 8px); +} +.plugin-clear_button.focus.has-items .clear-button, .plugin-clear_button:not(.disabled):hover.has-items .clear-button { + opacity: 1; +} + +.ts-wrapper .dropdown-header { + position: relative; + padding: 10px 8px; + border-bottom: 1px solid #d0d0d0; + background: color-mix(#fff, #d0d0d0, 85%); + border-radius: 3px 3px 0 0; +} +.ts-wrapper .dropdown-header-close { + position: absolute; + right: 8px; + top: 50%; + color: #303030; + opacity: 0.4; + margin-top: -12px; + line-height: 20px; + font-size: 20px !important; +} +.ts-wrapper .dropdown-header-close:hover { + color: black; +} + +.plugin-dropdown_input.focus.dropdown-active .ts-control { + box-shadow: none; + border: 1px solid #d0d0d0; +} +.plugin-dropdown_input .dropdown-input { + border: 1px solid #d0d0d0; + border-width: 0 0 1px; + display: block; + padding: 8px 8px; + box-shadow: none; + width: 100%; + background: transparent; +} +.plugin-dropdown_input .items-placeholder { + border: 0 none !important; + box-shadow: none !important; + width: 100%; +} +.plugin-dropdown_input.has-items .items-placeholder, .plugin-dropdown_input.dropdown-active .items-placeholder { + display: none !important; +} + +.ts-wrapper.plugin-input_autogrow.has-items .ts-control > input { + min-width: 0; +} +.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input { + flex: none; + min-width: 4px; +} +.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input::-ms-input-placeholder { + color: transparent; +} +.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input::placeholder { + color: transparent; +} + +.ts-dropdown.plugin-optgroup_columns .ts-dropdown-content { + display: flex; +} +.ts-dropdown.plugin-optgroup_columns .optgroup { + border-right: 1px solid #f2f2f2; + border-top: 0 none; + flex-grow: 1; + flex-basis: 0; + min-width: 0; +} +.ts-dropdown.plugin-optgroup_columns .optgroup:last-child { + border-right: 0 none; +} +.ts-dropdown.plugin-optgroup_columns .optgroup::before { + display: none; +} +.ts-dropdown.plugin-optgroup_columns .optgroup-header { + border-top: 0 none; +} + +.ts-wrapper.plugin-remove_button .item { + display: inline-flex; + align-items: center; +} +.ts-wrapper.plugin-remove_button .item .remove { + color: inherit; + text-decoration: none; + vertical-align: middle; + display: inline-block; + padding: 0 6px; + border-radius: 0 2px 2px 0; + box-sizing: border-box; +} +.ts-wrapper.plugin-remove_button .item .remove:hover { + background: rgba(0, 0, 0, 0.05); +} +.ts-wrapper.plugin-remove_button.disabled .item .remove:hover { + background: none; +} +.ts-wrapper.plugin-remove_button .remove-single { + position: absolute; + right: 0; + top: 0; + font-size: 23px; +} + +.ts-wrapper.plugin-remove_button:not(.rtl) .item { + padding-right: 0 !important; +} +.ts-wrapper.plugin-remove_button:not(.rtl) .item .remove { + border-left: 1px solid #d0d0d0; + margin-left: 6px; +} +.ts-wrapper.plugin-remove_button:not(.rtl) .item.active .remove { + border-left-color: #cacaca; +} +.ts-wrapper.plugin-remove_button:not(.rtl).disabled .item .remove { + border-left-color: white; +} + +.ts-wrapper.plugin-remove_button.rtl .item { + padding-left: 0 !important; +} +.ts-wrapper.plugin-remove_button.rtl .item .remove { + border-right: 1px solid #d0d0d0; + margin-right: 6px; +} +.ts-wrapper.plugin-remove_button.rtl .item.active .remove { + border-right-color: #cacaca; +} +.ts-wrapper.plugin-remove_button.rtl.disabled .item .remove { + border-right-color: white; +} + +:root { + --ts-pr-clear-button: 0; + --ts-pr-caret: 0; + --ts-pr-min: .75rem; +} + +.ts-wrapper.single .ts-control, .ts-wrapper.single .ts-control input { + cursor: pointer; +} + +.ts-control:not(.rtl) { + padding-right: max(var(--ts-pr-min), var(--ts-pr-clear-button) + var(--ts-pr-caret)) !important; +} + +.ts-control.rtl { + padding-left: max(var(--ts-pr-min), var(--ts-pr-clear-button) + var(--ts-pr-caret)) !important; +} + +.ts-wrapper { + position: relative; +} + +.ts-dropdown, +.ts-control, +.ts-control input { + color: #303030; + font-family: inherit; + font-size: 13px; + line-height: 18px; +} + +.ts-control, +.ts-wrapper.single.input-active .ts-control { + background: #fff; + cursor: text; +} + +.ts-hidden-accessible { + border: 0 !important; + clip: rect(0 0 0 0) !important; + -webkit-clip-path: inset(50%) !important; + clip-path: inset(50%) !important; + overflow: hidden !important; + padding: 0 !important; + position: absolute !important; + width: 1px !important; + white-space: nowrap !important; +} +/*# sourceMappingURL=tom-select.css.map */ \ No newline at end of file diff --git a/app/assets/stylesheets/trix.css b/app/assets/stylesheets/trix.css new file mode 100644 index 0000000..a7447a8 --- /dev/null +++ b/app/assets/stylesheets/trix.css @@ -0,0 +1,410 @@ +trix-editor { + border: 1px solid #bbb; + border-radius: 3px; + margin: 0; + padding: 0.4em 0.6em; + min-height: 5em; + outline: none; } + +trix-toolbar * { + box-sizing: border-box; } + +trix-toolbar .trix-button-row { + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + overflow-x: auto; } + +trix-toolbar .trix-button-group { + display: flex; + margin-bottom: 10px; + border: 1px solid #bbb; + border-top-color: #ccc; + border-bottom-color: #888; + border-radius: 3px; } + trix-toolbar .trix-button-group:not(:first-child) { + margin-left: 1.5vw; } + @media (max-device-width: 768px) { + trix-toolbar .trix-button-group:not(:first-child) { + margin-left: 0; } } + +trix-toolbar .trix-button-group-spacer { + flex-grow: 1; } + @media (max-device-width: 768px) { + trix-toolbar .trix-button-group-spacer { + display: none; } } + +trix-toolbar .trix-button { + position: relative; + float: left; + color: rgba(0, 0, 0, 0.6); + font-size: 0.75em; + font-weight: 600; + white-space: nowrap; + padding: 0 0.5em; + margin: 0; + outline: none; + border: none; + border-bottom: 1px solid #ddd; + border-radius: 0; + background: transparent; } + trix-toolbar .trix-button:not(:first-child) { + border-left: 1px solid #ccc; } + trix-toolbar .trix-button.trix-active { + background: #cbeefa; + color: black; } + trix-toolbar .trix-button:not(:disabled) { + cursor: pointer; } + trix-toolbar .trix-button:disabled { + color: rgba(0, 0, 0, 0.125); } + @media (max-device-width: 768px) { + trix-toolbar .trix-button { + letter-spacing: -0.01em; + padding: 0 0.3em; } } + +trix-toolbar .trix-button--icon { + font-size: inherit; + width: 2.6em; + height: 1.6em; + max-width: calc(0.8em + 4vw); + text-indent: -9999px; } + @media (max-device-width: 768px) { + trix-toolbar .trix-button--icon { + height: 2em; + max-width: calc(0.8em + 3.5vw); } } + trix-toolbar .trix-button--icon::before { + display: inline-block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + opacity: 0.6; + content: ""; + background-position: center; + background-repeat: no-repeat; + background-size: contain; } + @media (max-device-width: 768px) { + trix-toolbar .trix-button--icon::before { + right: 6%; + left: 6%; } } + trix-toolbar .trix-button--icon.trix-active::before { + opacity: 1; } + trix-toolbar .trix-button--icon:disabled::before { + opacity: 0.125; } + +trix-toolbar .trix-button--icon-attach::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M10.5%2018V7.5c0-2.25%203-2.25%203%200V18c0%204.125-6%204.125-6%200V7.5c0-6.375%209-6.375%209%200V18%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-miterlimit%3D%2210%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E"); + top: 8%; + bottom: 4%; } + +trix-toolbar .trix-button--icon-bold::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M6.522%2019.242a.5.5%200%200%201-.5-.5V5.35a.5.5%200%200%201%20.5-.5h5.783c1.347%200%202.46.345%203.24.982.783.64%201.216%201.562%201.216%202.683%200%201.13-.587%202.129-1.476%202.71a.35.35%200%200%200%20.049.613c1.259.56%202.101%201.742%202.101%203.22%200%201.282-.483%202.334-1.363%203.063-.876.726-2.132%201.12-3.66%201.12h-5.89ZM9.27%207.347v3.362h1.97c.766%200%201.347-.17%201.733-.464.38-.291.587-.716.587-1.27%200-.53-.183-.928-.513-1.198-.334-.273-.838-.43-1.505-.43H9.27Zm0%205.606v3.791h2.389c.832%200%201.448-.177%201.853-.497.399-.315.614-.786.614-1.423%200-.62-.22-1.077-.63-1.385-.418-.313-1.053-.486-1.905-.486H9.27Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-italic::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M9%205h6.5v2h-2.23l-2.31%2010H13v2H6v-2h2.461l2.306-10H9V5Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-link::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M18.948%205.258a4.337%204.337%200%200%200-6.108%200L11.217%206.87a.993.993%200%200%200%200%201.41c.392.39%201.027.39%201.418%200l1.623-1.613a2.323%202.323%200%200%201%203.271%200%202.29%202.29%200%200%201%200%203.251l-2.393%202.38a3.021%203.021%200%200%201-4.255%200l-.05-.049a1.007%201.007%200%200%200-1.418%200%20.993.993%200%200%200%200%201.41l.05.049a5.036%205.036%200%200%200%207.091%200l2.394-2.38a4.275%204.275%200%200%200%200-6.072Zm-13.683%2013.6a4.337%204.337%200%200%200%206.108%200l1.262-1.255a.993.993%200%200%200%200-1.41%201.007%201.007%200%200%200-1.418%200L9.954%2017.45a2.323%202.323%200%200%201-3.27%200%202.29%202.29%200%200%201%200-3.251l2.344-2.331a2.579%202.579%200%200%201%203.631%200c.392.39%201.027.39%201.419%200a.993.993%200%200%200%200-1.41%204.593%204.593%200%200%200-6.468%200l-2.345%202.33a4.275%204.275%200%200%200%200%206.072Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-strike::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M6%2014.986c.088%202.647%202.246%204.258%205.635%204.258%203.496%200%205.713-1.728%205.713-4.463%200-.275-.02-.536-.062-.781h-3.461c.398.293.573.654.573%201.123%200%201.035-1.074%201.787-2.646%201.787-1.563%200-2.773-.762-2.91-1.924H6ZM6.432%2010h3.763c-.632-.314-.914-.715-.914-1.273%200-1.045.977-1.739%202.432-1.739%201.475%200%202.52.723%202.617%201.914h2.764c-.05-2.548-2.11-4.238-5.39-4.238-3.145%200-5.392%201.719-5.392%204.316%200%20.363.04.703.12%201.02ZM4%2011a1%201%200%201%200%200%202h15a1%201%200%201%200%200-2H4Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-quote::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M4.581%208.471c.44-.5%201.056-.834%201.758-.995C8.074%207.17%209.201%207.822%2010%208.752c1.354%201.578%201.33%203.555.394%205.277-.941%201.731-2.788%203.163-4.988%203.56a.622.622%200%200%201-.653-.317c-.113-.205-.121-.49.16-.764.294-.286.567-.566.791-.835.222-.266.413-.54.524-.815.113-.28.156-.597.026-.908-.128-.303-.39-.524-.72-.69a3.02%203.02%200%200%201-1.674-2.7c0-.905.283-1.59.72-2.088Zm9.419%200c.44-.5%201.055-.834%201.758-.995%201.734-.306%202.862.346%203.66%201.276%201.355%201.578%201.33%203.555.395%205.277-.941%201.731-2.789%203.163-4.988%203.56a.622.622%200%200%201-.653-.317c-.113-.205-.122-.49.16-.764.294-.286.567-.566.791-.835.222-.266.412-.54.523-.815.114-.28.157-.597.026-.908-.127-.303-.39-.524-.72-.69a3.02%203.02%200%200%201-1.672-2.701c0-.905.283-1.59.72-2.088Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-heading-1::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M21.5%207.5v-3h-12v3H14v13h3v-13h4.5ZM9%2013.5h3.5v-3h-10v3H6v7h3v-7Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-code::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M3.293%2011.293a1%201%200%200%200%200%201.414l4%204a1%201%200%201%200%201.414-1.414L5.414%2012l3.293-3.293a1%201%200%200%200-1.414-1.414l-4%204Zm13.414%205.414%204-4a1%201%200%200%200%200-1.414l-4-4a1%201%200%201%200-1.414%201.414L18.586%2012l-3.293%203.293a1%201%200%200%200%201.414%201.414Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-bullet-list::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M5%207.5a1.5%201.5%200%201%200%200-3%201.5%201.5%200%200%200%200%203ZM8%206a1%201%200%200%201%201-1h11a1%201%200%201%201%200%202H9a1%201%200%200%201-1-1Zm1%205a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm0%206a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm-2.5-5a1.5%201.5%200%201%201-3%200%201.5%201.5%200%200%201%203%200ZM5%2019.5a1.5%201.5%200%201%200%200-3%201.5%201.5%200%200%200%200%203Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-number-list::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M3%204h2v4H4V5H3V4Zm5%202a1%201%200%200%201%201-1h11a1%201%200%201%201%200%202H9a1%201%200%200%201-1-1Zm1%205a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm0%206a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm-3.5-7H6v1l-1.5%202H6v1H3v-1l1.667-2H3v-1h2.5ZM3%2017v-1h3v4H3v-1h2v-.5H4v-1h1V17H3Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-undo::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M3%2014a1%201%200%200%200%201%201h6a1%201%200%201%200%200-2H6.257c2.247-2.764%205.151-3.668%207.579-3.264%202.589.432%204.739%202.356%205.174%205.405a1%201%200%200%200%201.98-.283c-.564-3.95-3.415-6.526-6.825-7.095C11.084%207.25%207.63%208.377%205%2011.39V8a1%201%200%200%200-2%200v6Zm2-1Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-redo::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M21%2014a1%201%200%200%201-1%201h-6a1%201%200%201%201%200-2h3.743c-2.247-2.764-5.151-3.668-7.579-3.264-2.589.432-4.739%202.356-5.174%205.405a1%201%200%200%201-1.98-.283c.564-3.95%203.415-6.526%206.826-7.095%203.08-.513%206.534.614%209.164%203.626V8a1%201%200%201%201%202%200v6Zm-2-1Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-decrease-nesting-level::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M5%206a1%201%200%200%201%201-1h12a1%201%200%201%201%200%202H6a1%201%200%200%201-1-1Zm4%205a1%201%200%201%200%200%202h9a1%201%200%201%200%200-2H9Zm-3%206a1%201%200%201%200%200%202h12a1%201%200%201%200%200-2H6Zm-3.707-5.707a1%201%200%200%200%200%201.414l2%202a1%201%200%201%200%201.414-1.414L4.414%2012l1.293-1.293a1%201%200%200%200-1.414-1.414l-2%202Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-increase-nesting-level::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M5%206a1%201%200%200%201%201-1h12a1%201%200%201%201%200%202H6a1%201%200%200%201-1-1Zm4%205a1%201%200%201%200%200%202h9a1%201%200%201%200%200-2H9Zm-3%206a1%201%200%201%200%200%202h12a1%201%200%201%200%200-2H6Zm-2.293-2.293%202-2a1%201%200%200%200%200-1.414l-2-2a1%201%200%201%200-1.414%201.414L3.586%2012l-1.293%201.293a1%201%200%201%200%201.414%201.414Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-dialogs { + position: relative; } + +trix-toolbar .trix-dialog { + position: absolute; + top: 0; + left: 0; + right: 0; + font-size: 0.75em; + padding: 15px 10px; + background: #fff; + box-shadow: 0 0.3em 1em #ccc; + border-top: 2px solid #888; + border-radius: 5px; + z-index: 5; } + +trix-toolbar .trix-input--dialog { + font-size: inherit; + font-weight: normal; + padding: 0.5em 0.8em; + margin: 0 10px 0 0; + border-radius: 3px; + border: 1px solid #bbb; + background-color: #fff; + box-shadow: none; + outline: none; + -webkit-appearance: none; + -moz-appearance: none; } + trix-toolbar .trix-input--dialog.validate:invalid { + box-shadow: #F00 0px 0px 1.5px 1px; } + +trix-toolbar .trix-button--dialog { + font-size: inherit; + padding: 0.5em; + border-bottom: none; } + +trix-toolbar .trix-dialog--link { + max-width: 600px; } + +trix-toolbar .trix-dialog__link-fields { + display: flex; + align-items: baseline; } + trix-toolbar .trix-dialog__link-fields .trix-input { + flex: 1; } + trix-toolbar .trix-dialog__link-fields .trix-button-group { + flex: 0 0 content; + margin: 0; } + +trix-editor [data-trix-mutable]:not(.attachment__caption-editor) { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + +trix-editor [data-trix-mutable]::-moz-selection, +trix-editor [data-trix-cursor-target]::-moz-selection, trix-editor [data-trix-mutable] ::-moz-selection { + background: none; } + +trix-editor [data-trix-mutable]::selection, +trix-editor [data-trix-cursor-target]::selection, trix-editor [data-trix-mutable] ::selection { + background: none; } + +trix-editor .attachment__caption-editor:focus[data-trix-mutable]::-moz-selection { + background: highlight; } + +trix-editor .attachment__caption-editor:focus[data-trix-mutable]::selection { + background: highlight; } + +trix-editor [data-trix-mutable].attachment.attachment--file { + box-shadow: 0 0 0 2px highlight; + border-color: transparent; } + +trix-editor [data-trix-mutable].attachment img { + box-shadow: 0 0 0 2px highlight; } + +trix-editor .attachment { + position: relative; } + trix-editor .attachment:hover { + cursor: default; } + +trix-editor .attachment--preview .attachment__caption:hover { + cursor: text; } + +trix-editor .attachment__progress { + position: absolute; + z-index: 1; + height: 20px; + top: calc(50% - 10px); + left: 5%; + width: 90%; + opacity: 0.9; + transition: opacity 200ms ease-in; } + trix-editor .attachment__progress[value="100"] { + opacity: 0; } + +trix-editor .attachment__caption-editor { + display: inline-block; + width: 100%; + margin: 0; + padding: 0; + font-size: inherit; + font-family: inherit; + line-height: inherit; + color: inherit; + text-align: center; + vertical-align: top; + border: none; + outline: none; + -webkit-appearance: none; + -moz-appearance: none; } + +trix-editor .attachment__toolbar { + position: absolute; + z-index: 1; + top: -0.9em; + left: 0; + width: 100%; + text-align: center; } + +trix-editor .trix-button-group { + display: inline-flex; } + +trix-editor .trix-button { + position: relative; + float: left; + color: #666; + white-space: nowrap; + font-size: 80%; + padding: 0 0.8em; + margin: 0; + outline: none; + border: none; + border-radius: 0; + background: transparent; } + trix-editor .trix-button:not(:first-child) { + border-left: 1px solid #ccc; } + trix-editor .trix-button.trix-active { + background: #cbeefa; } + trix-editor .trix-button:not(:disabled) { + cursor: pointer; } + +trix-editor .trix-button--remove { + text-indent: -9999px; + display: inline-block; + padding: 0; + outline: none; + width: 1.8em; + height: 1.8em; + line-height: 1.8em; + border-radius: 50%; + background-color: #fff; + border: 2px solid highlight; + box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.25); } + trix-editor .trix-button--remove::before { + display: inline-block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + opacity: 0.7; + content: ""; + background-image: url("data:image/svg+xml,%3Csvg%20height%3D%2224%22%20width%3D%2224%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M19%206.41%2017.59%205%2012%2010.59%206.41%205%205%206.41%2010.59%2012%205%2017.59%206.41%2019%2012%2013.41%2017.59%2019%2019%2017.59%2013.41%2012z%22%2F%3E%3Cpath%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%3C%2Fsvg%3E"); + background-position: center; + background-repeat: no-repeat; + background-size: 90%; } + trix-editor .trix-button--remove:hover { + border-color: #333; } + trix-editor .trix-button--remove:hover::before { + opacity: 1; } + +trix-editor .attachment__metadata-container { + position: relative; } + +trix-editor .attachment__metadata { + position: absolute; + left: 50%; + top: 2em; + transform: translate(-50%, 0); + max-width: 90%; + padding: 0.1em 0.6em; + font-size: 0.8em; + color: #fff; + background-color: rgba(0, 0, 0, 0.7); + border-radius: 3px; } + trix-editor .attachment__metadata .attachment__name { + display: inline-block; + max-width: 100%; + vertical-align: bottom; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } + trix-editor .attachment__metadata .attachment__size { + margin-left: 0.2em; + white-space: nowrap; } + +.trix-content { + line-height: 1.5; } + .trix-content * { + box-sizing: border-box; + margin: 0; + padding: 0; } + .trix-content h1 { + font-size: 1.2em; + line-height: 1.2; } + .trix-content blockquote { + border: 0 solid #ccc; + border-left-width: 0.3em; + margin-left: 0.3em; + padding-left: 0.6em; } + .trix-content [dir=rtl] blockquote, + .trix-content blockquote[dir=rtl] { + border-width: 0; + border-right-width: 0.3em; + margin-right: 0.3em; + padding-right: 0.6em; } + .trix-content li { + margin-left: 1em; } + .trix-content [dir=rtl] li { + margin-right: 1em; } + .trix-content pre { + display: inline-block; + width: 100%; + vertical-align: top; + font-family: monospace; + font-size: 0.9em; + padding: 0.5em; + white-space: pre; + background-color: #eee; + overflow-x: auto; } + .trix-content img { + max-width: 100%; + height: auto; } + .trix-content .attachment { + display: inline-block; + position: relative; + max-width: 100%; } + .trix-content .attachment a { + color: inherit; + text-decoration: none; } + .trix-content .attachment a:hover, .trix-content .attachment a:visited:hover { + color: inherit; } + .trix-content .attachment__caption { + text-align: center; } + .trix-content .attachment__caption .attachment__name + .attachment__size::before { + content: ' \2022 '; } + .trix-content .attachment--preview { + width: 100%; + text-align: center; } + .trix-content .attachment--preview .attachment__caption { + color: #666; + font-size: 0.9em; + line-height: 1.2; } + .trix-content .attachment--file { + color: #333; + line-height: 1; + margin: 0 2px 2px 2px; + padding: 0.4em 1em; + border: 1px solid #bbb; + border-radius: 5px; } + .trix-content .attachment-gallery { + display: flex; + flex-wrap: wrap; + position: relative; } + .trix-content .attachment-gallery .attachment { + flex: 1 0 33%; + padding: 0 0.5em; + max-width: 33%; } + .trix-content .attachment-gallery.attachment-gallery--2 .attachment, .trix-content .attachment-gallery.attachment-gallery--4 .attachment { + flex-basis: 50%; + max-width: 50%; } diff --git a/app/controllers/admin/admin_controller.rb b/app/controllers/admin/admin_controller.rb new file mode 100644 index 0000000..19e9dc0 --- /dev/null +++ b/app/controllers/admin/admin_controller.rb @@ -0,0 +1,81 @@ +class Admin::AdminController < ApplicationController + + layout 'admin' + + before_action :authenticate_user! + before_action :only_admin! + + helper_method :current_user + helper_method :user_signed_in? + helper_method :form_locale + + +private + + + def default_url_options + { locale: I18n.locale } + end + + + def form_locale + (params['form_locale'] || I18n.locale).to_sym + end + + + def authenticate_user! + unless user_signed_in? + store_location + redirect_to admin_sessions_path + end + end + + + def only_admin! + unless current_user&.admin_role? + redirect_to root_path + end + end + + + def current_user + Current.user ||= authenticate_user_from_session + end + + + def authenticate_user_from_session + User.enabled.find_by(id: session[:user_id]) + end + + + def user_signed_in? + current_user.present? + end + + + def login(user) + Current.user = user + reset_session + user.touch(:last_logon_at) + session[:user_id] = user.id + end + + + def logout(user) + Current.user = nil + reset_session + end + + + def redirect_back_or_default(default) + redirect_to(session[:return_to] || default) + session[:return_to] = nil + end + + + def store_location + session[:return_to] = request.fullpath if request.get? + end + + +end diff --git a/app/controllers/admin/assets_controller.rb b/app/controllers/admin/assets_controller.rb new file mode 100644 index 0000000..5ad60c3 --- /dev/null +++ b/app/controllers/admin/assets_controller.rb @@ -0,0 +1,82 @@ +class Admin::AssetsController < Admin::AdminController + + before_action :set_asset, only: %i[ edit update destroy ] + + helper_method :sorting_technique + + + # GET /assets + def index + @assets = Asset.public_send(sorting_technique, params[:reverse].present?) + .simple_search(params[:q]) + .page(params[:page] || 1) + + render (params[:node_id] || params[:newsletter_id]) ? 'explore' : 'index' + end + + + + + # GET /assets/1/edit + def edit + end + + # PATCH/PUT /assets/1 or /assets/1.json + def update + respond_to do |format| + if @asset.update(asset_params) + format.html { redirect_to edit_admin_asset_url(@asset), notice: t(:'assets.updated') } + else + format.html { render :edit, status: :unprocessable_entity } + end + end + end + + + def upload + blob = ActiveStorage::Blob.find_signed(params[:signed_id]) + @asset = Asset.create(file: blob, title: blob.filename.base.gsub("_", " ")) + + respond_to do |format| + format.turbo_stream + end + end + + + # DELETE /assets/1 or /assets/1.json + def destroy + @asset.destroy! + + respond_to do |format| + + format.html { redirect_to url_for(action: :index, format: :html), notice: t('assets.destroyed') } + unless params[:redirect_to] + format.turbo_stream { flash.now[:notice] = t('assets.destroyed') } + end + end + end + + +private + + + + def sorting_technique + params[:sort].presence_in(%w(by_filename by_last_modified)) || :by_filename + end + + + # Use callbacks to share common setup or constraints between actions. + def set_asset + @asset = Asset.find(params[:id]) + end + + + # Only allow a list of trusted parameters through. + def asset_params + params.require(:asset).permit( + :title + ) + end + +end diff --git a/app/controllers/admin/attachments_controller.rb b/app/controllers/admin/attachments_controller.rb new file mode 100644 index 0000000..8d868cb --- /dev/null +++ b/app/controllers/admin/attachments_controller.rb @@ -0,0 +1,29 @@ +class Admin::AttachmentsController < Admin::AdminController + + before_action :set_attachable_for, only: %i[ new ] + + + def new + @attachments = [] + if params[:asset_ids] + Asset.where(id: params[:asset_ids].split(',')).each do |asset| + @attachments << @attachable_for.attachments.new(asset: asset) + end + else + @attachments << @attachable_for.attachments.new + end + end + + + + +private + + + def set_attachable_for + @attachable_for = Node.find(params[:node_id]) if params[:node_id] + @attachable_for = Newsletter.find(params[:newsletter_id]) if params[:newsletter_id] + end + + +end diff --git a/app/controllers/admin/nodes_controller.rb b/app/controllers/admin/nodes_controller.rb new file mode 100644 index 0000000..c688608 --- /dev/null +++ b/app/controllers/admin/nodes_controller.rb @@ -0,0 +1,188 @@ +class Admin::NodesController < Admin::AdminController + + before_action :set_node, only: %i[ edit update destroy children sort toggle ] + helper_method :current_node_id, :open_node_ids + + # GET /nodes + def index + # Can be nil + @node = Node.find_by(id: current_node_id) || Node.roots.first + end + + + # GET /nodes/1/edit + def edit + respond_to do |format| + format.turbo_stream + format.html + end + end + + # GET /nodes/tree /nodes/1/tree + def tree + @node = params[:id] ? Node.find(params[:id]) : Node.roots.first + + session[:current_node_id] = @node&.id + + respond_to do |format| + format.turbo_stream + end + end + + + # POST /nodes or /nodes.json + def create + + @node = Node.new(node_params) + @node.status = :status_draft + @node.template = @node.root.site? ? Node::NODE_TEMPLATES.first : Node::TILE_TEMPLATES.first + + base_title = t('ui.untitled') + title = base_title + count = 0 + + while @node.siblings.exists?(title: {I18n.locale => title}) + count += 1 + title = "#{base_title} #{count}" + end + + @node.title = title + + respond_to do |format| + if @node.save + session[:open_node_ids] << @node.id unless session[:open_node_ids].include?(@node.id) + + format.turbo_stream + format.html { redirect_to edit_admin_node_url(@node), notice: t('ui.category_created', category: t(@node.template, scope: 'nodes.templates')) } + else + format.html { render :new, status: :unprocessable_entity } + end + end + end + + + # GET /nodes/1/children + def children + store_node_toggle_status + + respond_to do |format| + format.turbo_stream + end + end + + + # PATCH/PUT /nodes/1 + def update + + params[:node][:expires_at] ||= nil + + respond_to do |format| + if @node.update(node_params) + format.turbo_stream { + flash.now[:notice] = t('ui.category_updated', category: t(@node.template, scope: 'nodes.templates')) + } + format.html { redirect_to edit_node_url(@node), notice: t('ui.category_updated', category: t(@node.category, scope: 'nodes.categories')) } + else + format.html { render :edit, status: :unprocessable_entity } + end + end + end + + + # DELETE /nodes/1 + def destroy + if @node.destroy! + @destroyed_node = @node + @node = @node.parent + end + respond_to do |format| + format.turbo_stream { + flash.now[:notice] = t('ui.category_destroyed', category: t(@destroyed_node.category, scope: 'nodes.categories')) + } + format.html { redirect_to nodes_url, notice: t(:'nodes.destroyed') } + end + end + + + # PATCH /nodes/sort?id= + # {"id"=>"2", "old_index"=>1, "new_index"=>4, "node"=>{"id"=>"2"}} + def sort + @node.insert_at(params[:new_index].to_i + 1) + respond_to do |format| + format.turbo_stream + end + end + + + + # Expand or collapse node in drawer + # PATCH /nodes/toggle?id=1&expanded=true/false + def toggle + store_node_toggle_status + head :ok + end + + + + +private + + + # Use callbacks to share common setup or constraints between actions. + def set_node + @node = Node.find(params[:id]) + end + + + # Only allow a list of trusted parameters through. + def node_params + params.require(:node).permit( + :parent_id, + :title_da, :title_en, :title_de, + :page_title_da, :page_title_en, :page_title_de, + :page_description_da, :page_description_en, :page_description_de, + :slug_da, :slug_en, :slug_de, + :template, + :href_da, :href_en, :href_de, + :status, + :page_description, + :parent_id, + :position, + :published_at, + :expires_at, + :is_allowlist, + excluded_locales: [], + settings: [], + attachments_attributes: [:id, :asset_id, :body_da, :body_en, :body_de, :fg_color, :bg_color, :alignment, :template, :position, :_destroy] + ) + end + + + def current_node_id + session[:current_node_id] + end + + + def open_node_ids + Array(session[:open_node_ids]) + end + + + def ensure_open_node_ids + session[:open_node_ids] ||= [] + end + + + def store_node_toggle_status + ensure_open_node_ids + + if params[:expanded] == false + session[:open_node_ids].delete(@node.id) + else + session[:open_node_ids] << @node.id unless session[:open_node_ids].include?(@node.id) + end + end + + + +end diff --git a/app/controllers/admin/sessions_controller.rb b/app/controllers/admin/sessions_controller.rb new file mode 100644 index 0000000..ab8c1d1 --- /dev/null +++ b/app/controllers/admin/sessions_controller.rb @@ -0,0 +1,56 @@ +class Admin::SessionsController < Admin::AdminController + + layout 'sessions' + + skip_before_action :authenticate_user!, except: %i[destroy] + skip_before_action :only_admin! + + def index + render action: 'new' + end + + + def create + if user = User.enabled.authenticate_by(params.permit(:email, :password)) + # login user + # redirect_back_or_default(admin_root_path(locale: I18n.default_locale)) + + session[:verify_user_id] = user.id + UserMailer.with(user: user, verification_code: user.verification_codes.create).verify_email.deliver_later + + redirect_to action: 'verification', locale: nil + + else + flash.now.alert = t :'sessions.login_failed' + render action: 'new', status: :unprocessable_entity + end + end + + + def destroy + logout current_user + redirect_to root_path + end + + + # GET + def verification + + end + + + # POST + def verify + if params[:verification_code] =~ /\A\d{6}\z/ and + user = User.enabled.find(session[:verify_user_id]) and + user.verification_codes.valid.find_by(token: params[:verification_code]) + + login user + redirect_back_or_default(admin_root_path(locale: I18n.default_locale)) + else + flash.now.alert = t :'sessions.verification_failed' + render "verification" + end + end + +end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb new file mode 100644 index 0000000..c41ec28 --- /dev/null +++ b/app/controllers/admin/users_controller.rb @@ -0,0 +1,91 @@ +class Admin::UsersController < Admin::AdminController + + before_action :set_user, only: %i[ edit update destroy ] + + helper_method :sorting_technique + + + # GET /users + def index + @users = User.public_send(sorting_technique, params[:reverse].present?) + .simple_search(params[:q]) + .page(params[:page] || 1) + end + + + # GET /users/new + def new + @user = User.new + end + + + # GET /users/1/edit + def edit + end + + + # POST /users or /users.json + def create + @user = User.new(user_params) + + respond_to do |format| + if @user.save + format.html { redirect_to edit_admin_user_url(@user), notice: t(:'users.created') } + else + format.html { render :new, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /users/1 or /users/1.json + def update + respond_to do |format| + if @user.update(user_params) + format.html { redirect_to edit_admin_user_url(@user), notice: t(:'users.updated') } + else + format.html { render :edit, status: :unprocessable_entity } + end + end + end + + # DELETE /users/1 or /users/1.json + def destroy + @user.destroy! + + respond_to do |format| + format.html { redirect_to url_for(action: :index), notice: t(:'users.destroyed') } + end + end + + +private + + + + def sorting_technique + params[:sort].presence_in(%w(by_name by_email by_last_modified)) || :by_name + end + + + # Use callbacks to share common setup or constraints between actions. + def set_user + @user = User.find(params[:id]) + end + + + # Only allow a list of trusted parameters through. + def user_params + params.require(:user).permit( + :enabled_at, + :title, + :role, + :phone, + :firstname, + :lastname, + :email, + :password, + :password_confirmation + ) + end + +end diff --git a/app/helpers/admin/admin_helper.rb b/app/helpers/admin/admin_helper.rb new file mode 100644 index 0000000..c6dc30e --- /dev/null +++ b/app/helpers/admin/admin_helper.rb @@ -0,0 +1,41 @@ +module Admin::AdminHelper + + def entries_info(collection) + n = collection.respond_to?(:total_count) ? collection.total_count : collection.size + + tag.div class: 'entries-info', id: 'n-entries' do + return "#{n} #{collection.model_name.human(count: n)}" if collection.respond_to?(:model_name) + return "#{n} #{collection.first.model_name.human(count: n)}" if collection.is_a?(Array) and collection.any? + end + end + + + def audit_info(obj) + tag.div do + concat tag.div(l(obj.updated_at, format: :listing), title: l(obj.updated_at, format: :long), class: 'audit-date') + concat tag.div(obj.own_and_associated_audits&.first&.user&.name, class: 'audit-info') if obj.respond_to?(:audits) + end + end + + + def link_to_sortable(sort_by, title=nil, options={}) + title ||= sort_by.titleize + current = (sort_by.to_sym == sorting_technique.to_sym) + reverse = (params[:reverse].blank? and current) ? 1 : nil + + css_class = ['sort_link'] + css_class << 'current' if current + css_class << (reverse.blank? ? 'asc' : 'desc') + + link_to title, + url_for( + params.permit(:q, :category, :tag).merge(sort: sort_by, reverse: reverse) + ), + data: options[:data] || { + turbo_action: 'advance' + } , + class: css_class.join(' ') + end + + +end diff --git a/app/helpers/admin/assets_helper.rb b/app/helpers/admin/assets_helper.rb new file mode 100644 index 0000000..bb24b59 --- /dev/null +++ b/app/helpers/admin/assets_helper.rb @@ -0,0 +1,2 @@ +module Admin::AssetsHelper +end diff --git a/app/helpers/admin/attachments_helper.rb b/app/helpers/admin/attachments_helper.rb new file mode 100644 index 0000000..e272eec --- /dev/null +++ b/app/helpers/admin/attachments_helper.rb @@ -0,0 +1,2 @@ +module Admin::AttachmentsHelper +end diff --git a/app/helpers/admin/nodes_helper.rb b/app/helpers/admin/nodes_helper.rb new file mode 100644 index 0000000..74043f9 --- /dev/null +++ b/app/helpers/admin/nodes_helper.rb @@ -0,0 +1,145 @@ +module Admin::NodesHelper + + + def node_structure_for_select(parent, node) + result = [] + + result << [parent.title, parent.id] if parent.root? + + parent.children.ordered.each do |child| + result << ["#{"\u00A0\u00A0" * (child.depth)}#{child.title}".html_safe, child.id, { disabled: (child == node or child.ancestor_ids.include?(node.id)) }] + result = result + node_structure_for_select(child, node) + end + + result + end + + def spacer_node(node) + return if node.root? + + tag.div class: 'spacer' do + node.depth.times do + concat tag.div nil + end + end + end + + + def toggle_node(node) + return tag.div nil, class: 'child' if node.document? + link_to url_for(controller: 'nodes', action: 'children', id: node.id), + class: 'child parent', + data: { + turbo_stream: true, + controller: "nodes", + action: "click->nodes#toggle_children" + } do + concat tag.span('expand_more') + concat tag.span('keyboard_arrow_right') + end + end + + + def tree_title(node) + + + # result = [link_to(ENV['PROJECT_NAME'], + # url_for(url_base_options.merge(id: nil)), + # class: "list-title-link#{' has-popup-menu' if node.blank?}", + # data: { + # icon: 'museum', + # action: node.blank? ? 'click->popup#toggle' : 'click->nodes#set_current', + # turbo_stream: node.blank? ? nil : true, + # })] + result = [] + + node&.path&.each do |n| + result << tree_node_title_link(n, node) + end + + result[-1] = "#{tag.div "#{result[-1]}#{node_popup_menu(node)}".html_safe, data: {controller: 'popup'} }" + + result.join(tag.span('>')).html_safe + end + + + def node_popup_menu(node=nil) + tag.div class: 'popup-menu' do + tag.ul do + concat tag.li(link_to t('ui.edit'), edit_admin_node_path(node), data: {icon: 'edit', turbo_action: 'advance'}) if node + Node.categories.each do |node_category| + concat tag.li button_to(t(node_category, scope: 'nodes.new_categories'), + url_for(controller: 'nodes', action: 'create'), + params: {node: {parent_id: node&.id}}, + data: { + action: 'click->popup#close_open', + icon: t(node_category, scope: 'nodes.icons') + }) + end + # concat tag.li button_to(t('ui.reindex'), + # url_for(controller: 'nodes', action: 'reindex', id: node.id), + # method: :patch, + # data: { + # action: 'click->popup#close_open', + # icon: 'refresh' + # }) if node + end + end + end + + + def node_link_title(node) + + # return node.title if !node.document? or node.attachments.blank? or !node.attachments.first.asset.representable? + + # asset = node.attachments.first.asset + # node_icon = image_tag(rails_storage_proxy_path(asset.representation(resize_to_limit: [48,72], format: :jpg))) if asset.representable? + node_icon = nil + node_title = tag.span(node.title) + + (node_icon || '').concat(node_title).html_safe + end + + + def node_flags(node) + return if node.copyright_with_inheritance.blank? and node.tags_with_inheritance.blank? + tag.div class: 'node__flags' do + concat tag.span('copyright') if node.copyright_with_inheritance.present? + concat tag.span('sell') if node.tags_with_inheritance.any? + end + end + + + def drawer_node_link(node) + link_to tag.span(node.title), + url_for(controller: 'nodes', action: node.document? ? 'edit' : 'tree', id: node.id), + title: node.title, + class: 'node-title', + id: dom_id(node, 'drawer-link'), + data: { + nodes_id_param: node.id, + turbo_stream: true, + action: 'click->nodes#set_current', + icon: t("nodes.icons.#{node.category}") + } + end + + + def tree_node_title_link(node, current_node=nil) + + current_node ||= node + url_base_options = {controller: 'nodes', action: 'tree'} + + link_to(node.title, + url_for(url_base_options.merge(id: node.id)), + class: current_node == node ? 'list-title-link has-popup-menu' : 'list-title-link', + id: dom_id(node, 'list-title-link'), + data: { + action: current_node == node ? 'click->popup#toggle' : 'click->nodes#set_current', + turbo_stream: current_node != node ? true : nil, + nodes_id_param: node.id, + icon: t("nodes.icons.#{node.category}") + }) + end + +end diff --git a/app/helpers/admin/users_helper.rb b/app/helpers/admin/users_helper.rb new file mode 100644 index 0000000..5995c2a --- /dev/null +++ b/app/helpers/admin/users_helper.rb @@ -0,0 +1,2 @@ +module Admin::UsersHelper +end diff --git a/app/javascript/admin.js b/app/javascript/admin.js new file mode 100644 index 0000000..18a2582 --- /dev/null +++ b/app/javascript/admin.js @@ -0,0 +1,38 @@ +import "@hotwired/turbo-rails" +import "controllers" +import Trix from "trix" + +// Bind ctrl + s to submit form +document.addEventListener('keydown', function (event) { + // Check if Ctrl key is pressed and the pressed key is 'S' + if ((event.ctrlKey || event.metaKey) && event.key === 's') { + // Prevent the browser's default save action + event.preventDefault(); + + // Submit the form + const form = document.querySelector('.has--key-ctrls'); + if (form) { + form.requestSubmit(); + } + } +}); + +// Trix +Trix.config.blockAttributes.heading2 = { tagName: "h2", terminal: true, breakOnReturn: true, group: false } +// Trix.config.blockAttributes.heading3 = { tagName: "h3", terminal: true, breakOnReturn: true, group: false } + + +document.addEventListener("trix-initialize", event => { + var buttonHTML + + buttonHTML = ''; + + const groupElement = event.target.toolbarElement.querySelector(".trix-button-group--block-tools .trix-button--icon-heading-1") + groupElement.insertAdjacentHTML("afterend", buttonHTML) + + const { toolbarElement } = event.target + const inputElement = toolbarElement.querySelector("input[name=href]") + inputElement.type = "text" + inputElement.pattern = "(https?://|/).+" +}) + diff --git a/app/javascript/controllers/assets_controller.js b/app/javascript/controllers/assets_controller.js new file mode 100644 index 0000000..1390627 --- /dev/null +++ b/app/javascript/controllers/assets_controller.js @@ -0,0 +1,34 @@ +import { Controller } from "@hotwired/stimulus" +import { get } from '@rails/request.js' + +export default class extends Controller { + + static values = { + url: String + } + + connect () { + this.fetchingData = false + } + + appendAttachments(event) { + + if (this.fetchingData) return + + const checkedCheckboxes = this.element.querySelectorAll('input[name="asset_ids[]"]:checked') + const checkedValues = Array.from(checkedCheckboxes).map(checkbox => checkbox.value) + + this.getAttachments(checkedValues) + + this.dispatch("click", { target: document.getElementById('overlay'), prefix: null}) + + } + + async getAttachments(ids) { + this.fetchingData = true + await get(this.urlValue, { query: {asset_ids: ids.join(',')}, responseKind: "turbo-stream" } ) + this.fetchingData = false + } + + +} diff --git a/app/javascript/controllers/fields_controller.js b/app/javascript/controllers/fields_controller.js new file mode 100644 index 0000000..8f30374 --- /dev/null +++ b/app/javascript/controllers/fields_controller.js @@ -0,0 +1,15 @@ +import { Controller } from "@hotwired/stimulus" + + +export default class extends Controller { + + connect() { + } + + removeField(event) { + const input = this.element.querySelector('.destroy__field') + input.value = '1' + this.element.style = 'display: none'; + } + +} diff --git a/app/javascript/controllers/hello_controller.js b/app/javascript/controllers/hello_controller.js deleted file mode 100644 index 5975c07..0000000 --- a/app/javascript/controllers/hello_controller.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - connect() { - this.element.textContent = "Hello World!" - } -} diff --git a/app/javascript/controllers/i18n_form_controller.js b/app/javascript/controllers/i18n_form_controller.js new file mode 100644 index 0000000..dc9f0dc --- /dev/null +++ b/app/javascript/controllers/i18n_form_controller.js @@ -0,0 +1,26 @@ +import { Controller } from "@hotwired/stimulus" + + +export default class extends Controller { + + connect() { + + this.input = document.createElement('input'); + this.input.type = 'hidden'; + this.input.name = '_locale'; // Set the name for the input + this.input.value = this.element.getAttribute('data-locale'); // Set the value for the input + + this.element.appendChild(this.input); + + } + + setFormLocale(event) { + const locale = event.target.getAttribute('value') + + this.input.value = locale + this.element.setAttribute('data-locale', locale) + } + + +} + diff --git a/app/javascript/controllers/load_more_controller.js b/app/javascript/controllers/load_more_controller.js new file mode 100644 index 0000000..ce875b0 --- /dev/null +++ b/app/javascript/controllers/load_more_controller.js @@ -0,0 +1,27 @@ +import { Controller } from "@hotwired/stimulus" +import { useIntersection } from "stimulus-use" +import { get } from "@rails/request.js" + +export default class extends Controller { + + static values = { + url: String + } + + connect() { + this.fetchingData = false + useIntersection(this) + } + + async appear () { + + if (this.fetchingData) return + + + this.fetchingData = true + await get(this.urlValue, { responseKind: "turbo-stream" } ) + this.fetchingData = false + } + + +} diff --git a/app/javascript/controllers/navbar_controller.js b/app/javascript/controllers/navbar_controller.js new file mode 100644 index 0000000..5f5ff85 --- /dev/null +++ b/app/javascript/controllers/navbar_controller.js @@ -0,0 +1,14 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + + set_current() { + + document.querySelectorAll('#navbar .current').forEach((node) => { + node.classList.remove('current') + }) + + this.element.classList.add('current') + } + +} diff --git a/app/javascript/controllers/nodes_controller.js b/app/javascript/controllers/nodes_controller.js new file mode 100644 index 0000000..711d11a --- /dev/null +++ b/app/javascript/controllers/nodes_controller.js @@ -0,0 +1,59 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + + connect() { + this.url = document.getElementById('drawer').getAttribute('data-url') + this.token = document.querySelector('meta[name="csrf-token"]').getAttribute('content') + } + + // Toggle node children in drawer + toggle_children(event) { + const node = this.element.closest('.node-row'); + node.classList.toggle('closed'); + + // Already loaded + if (node.parentNode.classList.contains('loaded')) { + + fetch(this.url, { + method: 'PATCH', + headers: { + 'X-CSRF-Token': this.token, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + id: node.parentNode.dataset.id, + expanded: !node.classList.contains('closed') + }) + }).then((response) => { + // console.info('resolved', response) + }).catch((err) => { + console.info('rejected', err) + }) + + event.preventDefault(); + + } else { + node.parentNode.classList.add('loaded') + } + + + } + + // Mark current node in drawer + set_current(event) { + + // Remove old current + var node = document.querySelector("#drawer .current") + if (node) { + node.classList.remove('current'); + } + + // Add new current + node = document.getElementById(`node_${event.params['id']}`) + if (node) { + node.classList.add('current') + } + } + +} diff --git a/app/javascript/controllers/popup_controller.js b/app/javascript/controllers/popup_controller.js new file mode 100644 index 0000000..9077c70 --- /dev/null +++ b/app/javascript/controllers/popup_controller.js @@ -0,0 +1,82 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + + connect() { + this.element.style.position = 'relative' + this.boundClosePopupOnClickOutside = this.closePopupOnClickOutside.bind(this) + } + + + toggle(event) { + + const margin = 12 + const btn = event.currentTarget + const popup_menu = btn.nextElementSibling + + popup_menu.classList.toggle('open') + btn.classList.toggle('open') + + if (popup_menu.classList.contains('open')) { + + const bound_rect = popup_menu.getBoundingClientRect() + + // Open to the right + if (btn.classList.contains('open--right')) { + + popup_menu.style.left = btn.offsetWidth + margin + 'px' + popup_menu.style.top = "0px" + + const viewportHeight = window.innerHeight || document.documentElement.clientHeight; + + + // Overflow buttom + if (bound_rect.bottom > viewportHeight) { + popup_menu.style.bottom = "0px" + popup_menu.style.top = "auto" + } + + } else { + + const viewportWidth = window.innerWidth || document.documentElement.clientWidth; + + // Overflow right + if (bound_rect.right > viewportWidth) { + popup_menu.style.left = '-' + (bound_rect.width - btn.offsetWidth) + 'px' + + + } + + + } + + document.addEventListener('mousedown', this.boundClosePopupOnClickOutside) + } else { + popup_menu.removeAttribute('style') + } + + event.preventDefault() + } + + close_open(e) { + document.querySelectorAll('.popup-menu.open, .has-popup-menu.open').forEach((node) => { + node.classList.remove('open') + node.removeAttribute('style') + }) + document.removeEventListener('mousedown', this.boundClosePopupOnClickOutside) + } + + + closePopupOnClickOutside(e) { + const popup_menu = e.target.closest('.popup-menu, .has-popup-menu'); + if (popup_menu == null) { + document.querySelectorAll('.popup-menu.open, .has-popup-menu.open').forEach((node) => { + node.classList.remove('open') + node.removeAttribute('style') + }) + document.removeEventListener('mousedown', this.boundClosePopupOnClickOutside) + } + } + +} + diff --git a/app/javascript/controllers/select_controller.js b/app/javascript/controllers/select_controller.js new file mode 100644 index 0000000..ef8b77d --- /dev/null +++ b/app/javascript/controllers/select_controller.js @@ -0,0 +1,31 @@ +import { Controller } from "@hotwired/stimulus" +import TomSelect from "tom-select"; + +export default class extends Controller { + + connect() { + this.initializeTomSelect(); + } + + // Triggered when the Stimulus controller is removed from the DOM. + disconnect() { + this.destroyTomSelect(); + } + + + // Initialize the TomSelect dropdown with the desired configurations. + initializeTomSelect() { + // Return early if no element is associated with the controller. + if (!this.element) return; + + this.select = new TomSelect(this.element, { + create: this.element.getAttribute('data-tags') == 'true' + }); + } + + destroyTomSelect() { + if (this.select) { + this.select.destroy(); + } + } +} diff --git a/app/javascript/controllers/sort_controller.js b/app/javascript/controllers/sort_controller.js new file mode 100644 index 0000000..d265b04 --- /dev/null +++ b/app/javascript/controllers/sort_controller.js @@ -0,0 +1,51 @@ +import { Controller } from "@hotwired/stimulus" +import Sortable from 'sortablejs' + +export default class extends Controller { + connect() { + this.sortable = new Sortable(this.element, { + onEnd: this.end.bind(this), + handle: '.handle' + }) + + } + + disconnect() { + this.sortable.destroy() + } + + end(event) { + + // sortableUrl + if (this.element.dataset.sortableUrl != undefined) { + const item = this.element.children[event.newIndex] + const url = this.element.dataset.sortableUrl + const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content') + + const formData = new FormData() + formData.append('id', item.dataset.id) + formData.append('new_index', event.newIndex) + // console.log("Move", event.oldIndex, "to", event.newIndex); + + fetch(url, { + method: 'PATCH', + headers: { + 'Accept': "text/vnd.turbo-stream.html", + 'X-CSRF-Token': token + }, + body: formData + }) + .then (response => response.text()) + .then(html => Turbo.renderStreamMessage(html)) + .catch((err) => { + console.info('rejected', err) + }) + } else { + this.element.querySelectorAll('input.position').forEach((input, index) => { + input.value = index + 1 + }) + } + } + + +} diff --git a/app/javascript/controllers/upload_controller.js b/app/javascript/controllers/upload_controller.js new file mode 100644 index 0000000..ec02658 --- /dev/null +++ b/app/javascript/controllers/upload_controller.js @@ -0,0 +1,117 @@ +import { Controller } from "@hotwired/stimulus" +import { DirectUpload } from "@rails/activestorage" + +// Connects to data-controller="upload" +export default class extends Controller { + static values = { + url: String, + createUrl: String, + parentId: Number, + target: String + } + + connect() { + // console.info(this.urlValue) + // console.info('URL', this.createUrlValue) + } + + // This will be triggered when a file is selected or dropped + upload(event) { + event.preventDefault() + + const container = document.getElementById(this.targetValue) + + const files = event.target.files || event.dataTransfer.files; + Array.from(files).forEach((file, index) => { + + const uid = 'new_' + Date.now() + '_' + index + const target_div = document.createElement('div') + target_div.setAttribute('id', uid) + + if (this.targetValue == 'attachments') { + target_div.classList.add('attachment', 'loading') + target_div.innerHTML = '0' + } else if (this.targetValue == 'tree-nodes') { + target_div.classList.add('row') + target_div.innerHTML = '
0
' + } + container.append(target_div) + + new Uploader(this, file, uid).start() + }); + } + + + // Drag over event handler + dragover(event) { + event.preventDefault(); + this.element.classList.add('drag-over'); + } + + // Drag leave event handler + dragleave(event) { + event.preventDefault(); + this.element.classList.remove('drag-over'); + } + + // Drop event handler + drop(event) { + event.preventDefault(); + this.element.classList.remove('drag-over'); + + // Call the upload method and pass in the drop event + this.upload(event); + } + +} + +class Uploader { + + constructor(source, file, uid) { + this.source = source + this.uid = uid + this.upload = new DirectUpload(file, source.urlValue, this) + } + + start() { + this.upload.create((error, blob) => { + if (error) { + // Handle the error + } else { + const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content') + + const formData = new FormData() + if (this.source.parentIdValue > 0) { + formData.append('id', this.source.parentIdValue) + } + formData.append('signed_id', blob.signed_id) + formData.append('uid', this.uid) + + fetch(this.source.createUrlValue, { + method: 'POST', + headers: { + 'Accept': "text/vnd.turbo-stream.html", + 'X-CSRF-Token': token + }, + body: formData + }) + .then (response => response.text()) + .then(html => Turbo.renderStreamMessage(html)) + .catch((err) => { + console.info('rejected', err) + }) + } + }) + } + + directUploadWillStoreFileWithXHR(request) { + request.upload.addEventListener("progress", + event => this.directUploadDidProgress(event)) + } + + directUploadDidProgress(event) { + const target = document.getElementById(this.uid).querySelector('.progress') + const loaded = Math.round((event.loaded / event.total) * 100) + target.innerHTML = loaded + } +} diff --git a/app/javascript/controllers/utils_controller.js b/app/javascript/controllers/utils_controller.js new file mode 100644 index 0000000..03da45a --- /dev/null +++ b/app/javascript/controllers/utils_controller.js @@ -0,0 +1,75 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="removals" +export default class extends Controller { + static values = { + statusText: String + } + + connect() { + // console.info(this.tagIdsValue) + // console.info(this.updateUrlValue) + + } + + replaceWithStatus(event) { + this.element.innerText = this.statusTextValue; + } + + changeDataN() { + // console.info(this.element.selectedOptions[0]) + + const context = this.element.closest('.context-container') + context.setAttribute('data-n', this.element.selectedOptions[0].getAttribute('data-n')) + } + + disableScroll(){ + document.body.style.overflow = 'hidden' + } + + enableScroll(){ + document.body.style.overflow = 'auto' + } + + + toogleSearchFilters(event) { + document.getElementById('search-filters').classList.toggle('open') + } + + setCurrent(event) { + this.element.querySelector('.current').classList.remove('current') + event.currentTarget.closest('.node-attachment').classList.add('current') + } + + closeOverlay(event){ + // console.log(event); + if (event.target == this.element || event.target.getAttribute('data-action') == 'click->utils#closeOverlay') { + this.closeOverlayAndEnableScroll() + } + } + + closeOverlayAndEnableScroll() { + const overlay = document.getElementById('overlay') + if (overlay) { + this.enableScroll() + overlay.remove(); + } + } + + + + remove() { + this.element.remove() + } + + toggleDisabled() { + const elements = this.element.querySelectorAll('select, input') + + elements.forEach(child => { + child.disabled = !child.disabled; + }) + } + + + +} diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 3c34c81..a2aed4d 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,4 +1,11 @@ class ApplicationMailer < ActionMailer::Base - default from: "from@example.com" + + ADMIN = "Two-factor authentication <2fa@onc.dk>" + CLIENT = ADMIN + + + default from: ADMIN, + return_path: ADMIN + layout "mailer" end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb new file mode 100644 index 0000000..961f4ab --- /dev/null +++ b/app/mailers/user_mailer.rb @@ -0,0 +1,12 @@ +class UserMailer < ApplicationMailer + + def verify_email + + @user = params[:user] + @verification_code = params[:verification_code] + + mail to: email_address_with_name(@user.email, @user.name), + subject: t('mailers.verify_email_subject', token: @verification_code&.token) + end + +end diff --git a/app/models/asset.rb b/app/models/asset.rb new file mode 100644 index 0000000..8ddae57 --- /dev/null +++ b/app/models/asset.rb @@ -0,0 +1,19 @@ +class Asset < ApplicationRecord + + has_one_attached :file + + include PgSearch::Model + pg_search_scope :pg_search, + against: [:title], + associated_against: { + file_blob: [:filename, :content_type] + }, + using: { + tsearch: { prefix: true } + } + + scope :by_last_modified, ->(rev) { order(updated_at: rev ? :asc : :desc, id: rev ? :desc : :asc) } + scope :by_filename, ->(rev) { order(title: rev ? :desc : :asc) } + scope :simple_search, ->(q) { pg_search(q) unless q.blank? } + +end diff --git a/app/models/attachment.rb b/app/models/attachment.rb new file mode 100644 index 0000000..ab7e189 --- /dev/null +++ b/app/models/attachment.rb @@ -0,0 +1,49 @@ +class Attachment < ApplicationRecord + + extend Mobility + translates :body, :url, locale_accessors: I18n.available_locales + + ALIGNMENT_TILE = %w"N NE E SE S SW W NW" + ALIGNMENT_SITE = %w"E W" + + TEMPLATE_SITE_ASSET = %w"Hero L" + + belongs_to :attachable_for, polymorphic: true, optional: true + belongs_to :asset, optional: true + + store_accessor :props, :fg_color, :bg_color, :alignment, :template + + acts_as_list scope: [:attachable_for_id, :attachable_for_type] + + scope :ordered, -> { order(position: :asc) } + scope :favorites, -> { where(is_favorite: true) } + + scope :favorites_first, -> { reorder("(is_favorite = TRUE) DESC, position ASC") } + scope :portraits_first, -> { reorder(Arel.sql("(props ->> 'is_portrait' = '1') DESC, position ASC")) } + + scope :not_portraits, -> { where("(props ->> 'is_portrait' IS NULL OR props ->> 'is_portrait' != '1')") } + + + def is_large? + self.is_large.to_i == 1 + end + + + def is_portrait? + self.is_portrait.to_i == 1 + end + + + def template_subclass + return 'na' if self.template.blank? + self.template.downcase + end + + + def alignment_subclass + return 'na' if self.alignment.blank? + self.alignment.downcase + end + + +end diff --git a/app/models/concerns/ancestry_with_sorted_url.rb b/app/models/concerns/ancestry_with_sorted_url.rb new file mode 100644 index 0000000..6b342f6 --- /dev/null +++ b/app/models/concerns/ancestry_with_sorted_url.rb @@ -0,0 +1,54 @@ +module AncestryWithSortedUrl + extend ActiveSupport::Concern + + + included do + + has_ancestry orphan_strategy: :restrict, + cache_depth: true + + acts_as_list scope: [:ancestry] + + before_validation :format_slug, :generate_url + + # validates_presence_of :slug, :url, unless: Proc.new { |node| node.root? } + # validates_uniqueness_of :slug, scope: [:ancestry] + + after_commit :update_decendensts_url_if_changed + + end + + + + + +private + + + def format_slug + I18n.available_locales.each do |l| + v = self.root? ? (I18n.default_locale == l ? '' : l.to_s) : + self.send("slug_#{l}").blank? ? (self.title(locale: l) || '').parameterize : self.send("slug_#{l}").parameterize + + self.send(:slug=, v, locale: l) + end + end + + + def generate_url + I18n.available_locales.each do |l| + Mobility.with_locale(l) do + v = File.join(self.ancestors.map { |node| node.slug || '' }, self.slug || '') + v = File.join('', v) unless I18n.default_locale == l + self.send(:url=, v, locale: l) + end + end + end + + + def update_decendensts_url_if_changed + self.reload.descendants.find_each(&:save) if self.previous_changes[:slug].present? or self.previous_changes[:ancestry] + end + + +end diff --git a/app/models/concerns/has_attachments.rb b/app/models/concerns/has_attachments.rb new file mode 100644 index 0000000..3d7ebeb --- /dev/null +++ b/app/models/concerns/has_attachments.rb @@ -0,0 +1,37 @@ +module HasAttachments + extend ActiveSupport::Concern + + included do + + has_many :attachments, -> { order :position }, dependent: :destroy, as: :attachable_for, class_name: "Attachment" + has_many :assets, through: :attachments + + has_many :assets, + through: :attachments, + source: :asset + + #has_many :images, + # -> { with_attached_file.where('assets.content_type LIKE ?', "image/%") }, + # through: :attachments, + # source: :asset +# + #has_many :images_favorites_first, + # -> { with_attached_file.where('assets.content_type LIKE ?', "image/%").reorder('attachments.is_favorite DESC', 'attachments.position ASC') }, + # through: :attachments, + # source: :asset + + + accepts_nested_attributes_for :attachments, allow_destroy: true + + end + + + def icon + self.assets.map{ |asset| return asset if asset.file and asset.file.content_type.starts_with?('image/') } + nil + end + + + + +end diff --git a/app/models/concerns/has_tags.rb b/app/models/concerns/has_tags.rb new file mode 100644 index 0000000..6b669c0 --- /dev/null +++ b/app/models/concerns/has_tags.rb @@ -0,0 +1,48 @@ +module HasTags + extend ActiveSupport::Concern + + included do + + scope :tagged_with, ->(tag) { tagged_with_any(tag) } + scope :all_or_tagged_with, ->(tag) { tagged_with_any(tag) if tag } + + scope :tagged_with_any, ->(tags) { where("tags -> '#{I18n.locale}' ?| ARRAY[:tags]", tags: tags) } + scope :tagged_with_all, ->(tags) { where("tags @> ?", {"#{I18n.locale}": [tags].flatten}.to_json) } + + end + + + def descendant_tags + return self.class.tags unless self.respond_to?(:descendants) + + result = {} + I18n.available_locales.each { |l| result[l] = [] } + + self.descendants.viewable.pluck(:tags).flatten.map do |tags| + tags.each do |k,v| + result[k.to_sym] += v + end + end + result.each{ |k, v| result[k] = v.flatten.uniq.sort } + end + + + module ClassMethods + + def tags + result = {} + I18n.available_locales.each { |l| result[l] = [] } + + pluck(:tags).flatten.map do |tags| + tags.each do |k,v| + result[k.to_sym] += v + end + end + result.each{ |k, v| result[k] = v.flatten.uniq.sort } + end + + end + + + +end diff --git a/app/models/current.rb b/app/models/current.rb new file mode 100644 index 0000000..73a9744 --- /dev/null +++ b/app/models/current.rb @@ -0,0 +1,3 @@ +class Current < ActiveSupport::CurrentAttributes + attribute :user +end diff --git a/app/models/node.rb b/app/models/node.rb new file mode 100644 index 0000000..0a3cfca --- /dev/null +++ b/app/models/node.rb @@ -0,0 +1,134 @@ +class Node < ApplicationRecord + + MENUS = %i"main_menu sub_menu footer_node cta_link opening_hours negative_menu buy_ticket newsletter" + COOKIE_POLICY = :cookie_policy + SETTINGS = MENUS << COOKIE_POLICY + + include AncestryWithSortedUrl + include HasAttachments + include HasTags + + + extend Mobility + translates :slug, locale_accessors: I18n.available_locales + translates :tags, locale_accessors: I18n.available_locales + + translates :title, + :url, + :href, + :page_title, + :page_description, + fallbacks: { en: :da, de: :en }, + locale_accessors: I18n.available_locales + + NODE_TEMPLATES = %w"tmpl_article tmpl_index" + + enum status: { status_published: 0, status_draft: 1, status_archived: 2 } + + enum template: { tmpl_index: 0, + tmpl_article: 1 + } + + include PgSearch::Model + pg_search_scope :pg_search, + against: {title: 'A', url: 'B', page_title: 'A', page_description: 'B', href: 'B', slug: 'B' }, + associated_against: { + attachments: [:body, :url] + } + + before_validation :remove_empty_tags + + validates_presence_of :title + + validates :expires_at, + date: { after: :published_at }, + allow_nil: true + + scope :ordered, -> { order(position: :asc) } + scope :by_title, ->(rev) { order(Arel.sql(rev ? "title->>'#{I18n.locale.to_s}' DESC, id DESC": "title ->>'#{I18n.locale.to_s}' ASC, id ASC")) } + scope :by_status, ->(rev) { order(status: rev ? :desc : :asc, id: rev ? :desc : :asc) } + scope :by_slug, ->(rev) { order(ancestry: rev ? :desc : :asc, position: rev ? :desc : :asc, id: rev ? :desc : :asc) } + scope :by_last_modified, ->(rev) { order(updated_at: rev ? :asc : :desc, id: rev ? :desc : :asc) } + + # scope :not_excluded, -> { where "NOT(? = ANY (excluded_locales))", I18n.locale.to_s } + + scope :for_current_locale, -> { where("(cardinality(excluded_locales) = 0 AND is_allowlist = false) OR + (is_allowlist = true AND ? = ANY(excluded_locales)) OR + (is_allowlist = false AND NOT(? = ANY(excluded_locales)))", + I18n.locale.to_s, I18n.locale.to_s ) } + + scope :viewable, -> { status_published.for_current_locale.where('published_at <= ? AND (expires_at IS NULL OR expires_at > ?)', Time.current, Time.current) } + scope :of_template, ->(tmpl) { where(template: Node.templates[tmpl.to_s]) } + + scope :with_setting, ->(setting) { setting.kind_of?(Array) ? where("settings && ?", "{#{setting.join(',')}}") : where("settings @> ?", "{#{setting}}") } + + scope :simple_search, ->(q) { pg_search(q) unless q.blank? } + + scope :tiles, -> { where("ancestry LIKE '/?/%'", Node.where(position: 2, ancestry_depth: 0).first) } + + def self.tags + result = {} + I18n.available_locales.each do |locale| + result[locale] = pluck(Arel.sql("tags->'#{locale}'")).flatten.compact.uniq.sort.reject{|v| v.blank?} + end + result + end + + + def self.categories + ['document'] + end + + + def category + self.children.any? ? 'folder' : 'document' + end + + + def document? + self.category == 'document' + end + + + def href_or_url + href.present?? href : url + end + + + def viewable? + Node.viewable.where(id: self.path_ids).count == self.path_ids.length + end + + + def index? + self.attachments.blank? + end + + + def site? + (self.root || self).position == 1 + end + + + def tile? + (self.root || self).position == 2 + end + + + def node_type + return "site" if self.site? + 'tile' + end + + + + +private + + def remove_empty_tags + %w"tags_da tags_en tags_de settings excluded_locales".map do |k| + self.send "#{k}=", self.send(k).reject { |v| v.blank? } unless self.send(k).blank? + end + end + +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..19c9e2c --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,70 @@ +class User < ApplicationRecord + + enum role: { user: 'user', admin: 'admin' }, _suffix: true + + include PgSearch::Model + pg_search_scope :pg_search, against: [:lastname, :firstname, :email, :phone, :title], + using: {tsearch: {dictionary: "danish"}} + + has_secure_password + has_many :verification_codes, dependent: :destroy + + before_destroy :dont_destroy_admin + + validates_presence_of :email + validates_presence_of :password, on: :create + validates_uniqueness_of :email + validates_format_of :email, with: URI::MailTo::EMAIL_REGEXP + + normalizes :email, with: -> email { email.strip.downcase } + + validate :cant_change_admin, on: :update + + scope :enabled, -> { where.not enabled_at: nil } + + scope :by_last_modified, ->(rev) { order(updated_at: rev ? :asc : :desc) } + scope :by_name, ->(rev) { order(lastname: rev ? :desc : :asc, firstname: rev ? :desc : :asc) } + scope :by_email, ->(rev) { order(email: rev ? :desc : :asc) } + scope :by_title, ->(rev) { order(title: rev ? :desc : :asc) } + + scope :simple_search, ->(q) { pg_search(q) unless q.blank? } + + + def su? + email == 'mattias@oncotype.dk' + end + + + def name + return email if lastname.blank? and firstname.blank? + [firstname, lastname].select{ |v| !v.blank? }.join(' ') + end + + + def initials + name.split(' ').map{ |s| s[0] }.join('').mb_chars.upcase + end + + + def enabled? + !self.enabled_at.nil? + end + +protected + + + #Prevent the user admins from beeing changed + def cant_change_admin + user = self.class.find(self.id) + errors.add(:email, I18n.t(:you_cant_change_the_email_on_this_user, scope: 'users')) if user.su? and self.email != user.email + errors.add(:email, I18n.t(:you_cant_change_this_on_this_user, scope: 'users')) if user.su? and !self.admin_role? and self.role_changed? + errors.add(:email, I18n.t(:you_cant_disable_this_user, scope: 'users')) if user.su? and self.enabled_at.nil? + end + + + # Prevents the super user admin to be removed" + def dont_destroy_admin + raise I18n.t(:cant_destroy_this_user, scope: 'users') if self.su? + end + +end diff --git a/app/models/verification_code.rb b/app/models/verification_code.rb new file mode 100644 index 0000000..c324755 --- /dev/null +++ b/app/models/verification_code.rb @@ -0,0 +1,21 @@ +class VerificationCode < ApplicationRecord + + VALID_FOR = 15.minutes + + before_validation(on: :create) do + self.token = rand(100000..999999).to_s + end + + validates_presence_of :token + + belongs_to :user + + scope :valid, -> { where("verification_codes.created_at > ?", VALID_FOR.ago) } + scope :expired, -> { where("verification_codes.created_at <= ?", VALID_FOR.ago) } + + + def expires_at + self.created_at + VALID_FOR + end + +end diff --git a/app/views/admin/assets/_asset.html.erb b/app/views/admin/assets/_asset.html.erb new file mode 100644 index 0000000..f0c55f8 --- /dev/null +++ b/app/views/admin/assets/_asset.html.erb @@ -0,0 +1,33 @@ +<%= tag.div class: 'asset', id: dom_id(asset) do %> + +
+
<%= image_tag rails_storage_proxy_path(asset.file.representation(resize_to_limit: [320,320], format: :jpg)) if asset.file.representable? %>
+
+ + <%= tag.div asset.title, class: 'asset__title' %> + + <%= tag.div [ + asset.file.filename.extension_without_delimiter.upcase, + number_to_human_size(asset.file.byte_size).sub(/\s/, ''), + [asset.file.metadata.dig('width'), asset.file.metadata.dig('height')].compact.join('x') + ].reject{ |v| v.blank? }.join(' '), + class: 'asset__mimetypes' %> + + + <%= label_tag nil, class: "icon-cb-round" do %> + <%= check_box_tag 'asset_ids[]', asset.id, false, id: nil%> + + <%- end -%> + + +
+
+ <%= link_to 'edit', edit_admin_asset_path(asset), data: {turbo_frame: 'main', turbo_action: 'advance'}, title: t('ui.edit') %> + <%= link_to 'download', rails_blob_path(asset.file, disposition: "attachment"), title: t('ui.download') %> +
+ <%= link_to 'delete_forever', admin_asset_path(asset), data: { turbo_confirm: t(:'ui.are_you_sure'), turbo_method: :delete }, title: t('ui.delete') %> +
+ + + +<% end %> \ No newline at end of file diff --git a/app/views/admin/assets/_form.html.erb b/app/views/admin/assets/_form.html.erb new file mode 100644 index 0000000..f8afae6 --- /dev/null +++ b/app/views/admin/assets/_form.html.erb @@ -0,0 +1,14 @@ + <%= form_with(model: [ :admin, asset ], class: 'form-plain') do |form| %> + + +
+ <%= render partial: 'material/text_field', locals: { f: form, attr: :title } %> +
+ + +
+ <%= link_to t(:'ui.cancel'), url_for(controller: 'assets', action: 'index'), data: {turbo_action: 'advance'} %> + <%= form.submit t(:'ui.save') %> +
+ + <% end %> diff --git a/app/views/admin/assets/_list.html.erb b/app/views/admin/assets/_list.html.erb new file mode 100644 index 0000000..be40fba --- /dev/null +++ b/app/views/admin/assets/_list.html.erb @@ -0,0 +1,51 @@ +
+

<%= link_to yield(:title), params.permit(:sort, :reverse), class: 'list-title-link' %>

+ +
+ <%= render partial: 'material/search', locals: (params[:node_id] ? {turbo_stream: true} : nil) %> + + <%= tag.form do %> + <%= tag.label class: "dropzone dropzone--small", + data: { + controller: 'upload', + upload_url_value: rails_direct_uploads_url, + upload_create_url_value: url_for(controller: 'assets', action: 'upload'), + upload_target_value: 'assets', + action: "dragover->upload#dragover dragleave->upload#dragleave drop->upload#drop" + } do %> + arrow_upward + + <% end %> + <% end %> + +
+
+ +
+ + + + + +
+
+
+ <%= render @assets %> +
+ + <%= tag.div nil, + id: 'load-more', + data: { + controller: "load-more", + load_more_list_id_value: "results", + load_more_url_value: path_to_next_page(@assets) + } unless @assets.blank? or @assets.last_page? %> +
+
+
\ No newline at end of file diff --git a/app/views/admin/assets/_sort.html.erb b/app/views/admin/assets/_sort.html.erb new file mode 100644 index 0000000..bb4331f --- /dev/null +++ b/app/views/admin/assets/_sort.html.erb @@ -0,0 +1,12 @@ +
+ + + +
\ No newline at end of file diff --git a/app/views/admin/assets/destroy.turbo_stream.erb b/app/views/admin/assets/destroy.turbo_stream.erb new file mode 100644 index 0000000..a9df7a2 --- /dev/null +++ b/app/views/admin/assets/destroy.turbo_stream.erb @@ -0,0 +1,3 @@ +<%= turbo_stream.remove @asset %> + +<%= turbo_stream.append 'flash', partial: 'layouts/flash' %> \ No newline at end of file diff --git a/app/views/admin/assets/edit.html.erb b/app/views/admin/assets/edit.html.erb new file mode 100644 index 0000000..32d4bc2 --- /dev/null +++ b/app/views/admin/assets/edit.html.erb @@ -0,0 +1,25 @@ +<%= content_for :title, @asset.title %> + +<%= turbo_frame_tag 'main' do %> + + <%= turbo_stream.append 'flash', partial: 'layouts/flash' %> + +
+ <%= link_to t(:'assets.list'), url_for(controller: 'assets', action: 'index'), data: {turbo_action: 'advance'}, class: 'back-link' %> + +
+

<%= yield :title %>

+ + <%= button_to t(:'assets.destroy'), + url_for(action: 'show', id: @asset), + method: :delete, + params: {redirect_to: admin_assets_path}, + data: { turbo_confirm: t(:'ui.are_you_sure') }, + tabindex: '-1', + class: 'delete-link' %> +
+ + <%= render "form", asset: @asset %> + +
+<% end %> \ No newline at end of file diff --git a/app/views/admin/assets/explore.turbo_stream.erb b/app/views/admin/assets/explore.turbo_stream.erb new file mode 100644 index 0000000..9643dd0 --- /dev/null +++ b/app/views/admin/assets/explore.turbo_stream.erb @@ -0,0 +1,57 @@ +<%= content_for :title, t(:'assets.title') %> + +<% if request.query_string.blank? %> + + + + + +<% else %> + + + + + + + + + + + + + +<% end %> \ No newline at end of file diff --git a/app/views/admin/assets/index.html.erb b/app/views/admin/assets/index.html.erb new file mode 100644 index 0000000..b77ea89 --- /dev/null +++ b/app/views/admin/assets/index.html.erb @@ -0,0 +1,9 @@ +<%= content_for :title, t(:'assets.title') %> + +<%= turbo_frame_tag 'main' do %> + + <%= turbo_stream.append 'flash', partial: 'layouts/flash' %> + + <%= render partial: 'list' %> + +<% end %> \ No newline at end of file diff --git a/app/views/admin/assets/index.turbo_stream.erb b/app/views/admin/assets/index.turbo_stream.erb new file mode 100644 index 0000000..b012328 --- /dev/null +++ b/app/views/admin/assets/index.turbo_stream.erb @@ -0,0 +1,30 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/views/admin/assets/upload.turbo_stream.erb b/app/views/admin/assets/upload.turbo_stream.erb new file mode 100644 index 0000000..58b4632 --- /dev/null +++ b/app/views/admin/assets/upload.turbo_stream.erb @@ -0,0 +1,7 @@ + + + + + diff --git a/app/views/admin/attachments/_asset.html.erb b/app/views/admin/attachments/_asset.html.erb new file mode 100644 index 0000000..fca5b47 --- /dev/null +++ b/app/views/admin/attachments/_asset.html.erb @@ -0,0 +1,14 @@ +
+
+
<%= image_tag rails_storage_proxy_path(asset.file.representation(resize_to_limit: [320,320], format: :jpg)) if asset.file.representable? %>
+
+ + <%= tag.div asset.title, class: 'asset__title' %> + + <%= tag.div [ + asset.file.filename.extension_without_delimiter.upcase, + number_to_human_size(asset.file.byte_size).sub(/\s/, ''), + [asset.file.metadata.dig('width'), asset.file.metadata.dig('height')].compact.join('x') + ].reject{ |v| v.blank? }.join(' '), + class: 'asset__mimetypes' %> +
\ No newline at end of file diff --git a/app/views/admin/attachments/_attachment.html.erb b/app/views/admin/attachments/_attachment.html.erb new file mode 100644 index 0000000..d21c3ff --- /dev/null +++ b/app/views/admin/attachments/_attachment.html.erb @@ -0,0 +1,43 @@ +
+ +
+ <%= render partial: 'admin/attachments/asset', collection: Array(f.object.asset) %> + <%= render partial: 'material/trix_field_i18n', locals: { f: f, attr: :body } %> + + +
+ <%= render partial: 'material/text_field', locals: { f: f, attr: :bg_color } %> + + <%= render partial: 'material/text_field', locals: { f: f, attr: :fg_color } %> + + <%= render partial: 'material/select_field', + locals: { + f: f, + attr: :alignment, + choices: (f.object.attachable_for&.site? ? Attachment::ALIGNMENT_SITE : Attachment::ALIGNMENT_TILE).map { |k,v| [t(k, scope: :'attachments.alignments'), k] }, + include_blank: true + } %> + + <%= render partial: 'material/select_field', + locals: { + f: f, + attr: :template, + choices: Array(Attachment::TEMPLATE_SITE_ASSET).map { |k,v| [t(k, scope: :'attachments.templates'), k] }, + include_blank: true + } %> +
+
+ + + + <%= button_tag 'delete', + type: 'button', + data: { action: 'click->fields#removeField' }, + class: 'destroy__field-btn' %> + + <%= f.hidden_field :_destroy, class: 'destroy__field' %> + <%= f.hidden_field :position, class: 'position' %> + <%= f.hidden_field :asset_id %> + +
more_vert
+
diff --git a/app/views/admin/attachments/new.turbo_stream.erb b/app/views/admin/attachments/new.turbo_stream.erb new file mode 100644 index 0000000..0630106 --- /dev/null +++ b/app/views/admin/attachments/new.turbo_stream.erb @@ -0,0 +1,14 @@ + + + diff --git a/app/views/admin/nodes/_drawer.html.erb b/app/views/admin/nodes/_drawer.html.erb new file mode 100644 index 0000000..efa7881 --- /dev/null +++ b/app/views/admin/nodes/_drawer.html.erb @@ -0,0 +1,5 @@ +
+
+ <%= render Node.roots.ordered %> +
+
\ No newline at end of file diff --git a/app/views/admin/nodes/_edit.html.erb b/app/views/admin/nodes/_edit.html.erb new file mode 100644 index 0000000..1903f33 --- /dev/null +++ b/app/views/admin/nodes/_edit.html.erb @@ -0,0 +1,14 @@ +
+ + + + <%= render "form" , node: @node %> + +
\ No newline at end of file diff --git a/app/views/admin/nodes/_form.html.erb b/app/views/admin/nodes/_form.html.erb new file mode 100644 index 0000000..2b4bb76 --- /dev/null +++ b/app/views/admin/nodes/_form.html.erb @@ -0,0 +1,203 @@ +<%= form_with(model: [:admin, node], + class: 'form-plain has--key-ctrls', + id: 'node_form', + data: { + locale: form_locale, + controller: "i18n-form" + }) do |form| %> + +
+ + <%= form.label :title, class: "title-box", data: { icon: t("nodes.icons.#{@node.category}") } do %> + <%= render partial: 'material/text_field_i18n_simple', locals: { f: form, attr: :title } %> + <% end %> + +
+ <%- I18n.available_locales.each do |locale| %> + <%= label_tag do %> + <%= radio_button_tag 'form_locale', locale, form_locale == locale, data: {action: 'change->i18n-form#setFormLocale' } %> + <%= tag.span locale %> + <% end %> + <% end %> +
+
+ + +
+
+
+ + +
+ <%= link_to t('ui.append_text'), + url_for(controller: 'attachments', action: 'new', node_id: node.id), + class: 'btn', + data: { turbo_stream: true } %> + <%= link_to t('ui.append_asset'), + url_for(controller: 'assets', action: 'index', node_id: node.id), + class: 'btn', + data: { + controller: 'utils', + action: 'click->utils#disableScroll', + turbo_stream: true + } %> +
+
+ +
+ <%= form.fields_for :attachments do |builder| %> + <%= render partial: 'admin/attachments/attachment', locals: {f: builder} %> + <% end%> +
+
+
+ + +
+ + <%= render partial: 'material/select_field', + locals: { + f: form, + attr: :parent_id, + choices: (@node.root? ? [] : node_structure_for_select(@node.root, @node)), + selected: @node.parent_id, + include_blank: false + } unless form.object.root? %> + + <%= render partial: 'material/select_field', + locals: { + f: form, + attr: :template, + include_blank: form.object.root?, + choices: Node.templates.slice(*(Node::NODE_TEMPLATES)).map { |k,v| [t(k, scope: :'nodes.templates'), k] }.sort } %> + + <%= render partial: 'material/tom_select_field', + locals: { + f: form, + attr: :settings, + choices: Node::SETTINGS.map{ |tag| [ t(tag, scope: :'nodes.settings') , tag.to_s] }.sort, + multiple: true + } %> + + <%= render partial: 'material/text_field_i18n', locals: { f: form, attr: :href } unless form.object.root? %> + + <%= render partial: 'material/tom_select_field_i18n', + locals: { + f: form, + attr: :tags, + choices: Node.tags, + multiple: true, + tags: true + } unless form.object.root? %> + +
+ +
+ <%= render partial: 'material/text_field_i18n', locals: { f: form, attr: :page_title } %> + <%= render partial: 'material/text_field_i18n', locals: { f: form, attr: :page_description } %> + + <%= tag.div class: 'field' do %> +
+ <%= form.label :slug, for: nil, class: 'i18n__label' %> +
+ +
+ <%- I18n.available_locales.each do |locale| %> + <%- i18n_attr = "slug_#{locale}" -%> + <%= form.label i18n_attr, class: "input-box input-box-url i18n__input i18n__input-#{locale}" do %> + + <%= tag.span File.join(form.object.parent&.url(locale: locale) || '', ''), class: 'base__url' %> + + <%= form.text_field i18n_attr, + class: 'material__input', + disabled: form.object.root? %> + <% end %> + + <%- form.object.errors.full_messages_for(i18n_attr).uniq.each do |msg| -%> + <%= content_tag :p, msg, role: 'alert' %> + <% end %> + <% end %> +
+ <%- end -%> + +
+ + + + + +
+
+
+ <%= form.label :status, for: nil %> +
+
+
    + <%- Node.statuses.each do |status| -%> +
  • <%= form.radio_button :status, status[0], id: status[1] %> <%= label_tag status[1], t(status[0], scope: :'nodes.statuses'), class: 'plain' %>
  • + <% end %> +
+
+
+ +
+
<%= form.label :published_at %>
+
+
+ <%= form.datetime_select :published_at, + {datetime_separator: '@', + time_separator: ':', + use_short_month: true}, {class: 'material__input material__input-select'} %> +
+ <%- form.object.errors.full_messages_for(:published_at).uniq.each do |msg| -%> + <%= content_tag :p, msg, role: 'alert' %> + <% end %> +
+
+ +
+
<%= form.label :expires_at %>
+
+
+ <%= form.datetime_select :expires_at, + {datetime_separator: '@', + time_separator: ':', + use_short_month: true, + default: (1.month.from_now.at_midnight-1.minute), + disabled: (@node.expires_at.blank?)}, { class: 'material__input material__input-select' } %> + + <%= button_tag 'add_alarm', type: 'button', data: { action: 'click->utils#toggleDisabled' } %> + <%= button_tag 'remove_circle_outline', type: 'button', data: { action: 'click->utils#toggleDisabled' } %> +
+ + <%- form.object.errors.full_messages_for(:expires_at).uniq.each do |msg| -%> + <%= content_tag :p, msg, role: 'alert' %> + <% end %> +
+
+ + <%= render partial: 'material/tom_select_field', + locals: { + f: form, + attr: :excluded_locales, + choices: options_for_select(I18n.available_locales.map{ |v| [t(v, scope: :'nodes.langs'), v.to_s] }, form.object.excluded_locales), + multiple: true + } %> + + <%= render partial: 'material/check_box_icon', locals: { f: form, attr: :is_allowlist} %> + +
+ + + +
+ <%= link_to t(:'ui.cancel'), + url_for(action: 'tree', id: @node.parent&.id), + data: { + turbo_stream: true, + turbo_action: 'advance' + } %> + <%= form.submit t(:'ui.save') %> +
+ +<% end %> \ No newline at end of file diff --git a/app/views/admin/nodes/_node.html.erb b/app/views/admin/nodes/_node.html.erb new file mode 100644 index 0000000..13b2cd4 --- /dev/null +++ b/app/views/admin/nodes/_node.html.erb @@ -0,0 +1,17 @@ +<%= tag.div class: "node#{' loaded' if open_node_ids.include?(node.id)}#{' current' if node.id == current_node_id}", + id: dom_id(node), + data: { + id: node.id + } do %> + + <%= tag.div class: "node-row #{node.category}#{' closed' unless open_node_ids.include?(node.id)}", + style: "--level: #{node.depth}" do %> + <%= spacer_node node %> + <%= toggle_node node %> + <%= drawer_node_link node %> + <% end %> + + <%= render node.children.ordered if open_node_ids.include?(node.id) %> + +<% end %> + diff --git a/app/views/admin/nodes/_tree.html.erb b/app/views/admin/nodes/_tree.html.erb new file mode 100644 index 0000000..d559c94 --- /dev/null +++ b/app/views/admin/nodes/_tree.html.erb @@ -0,0 +1,24 @@ +
+ +
+
+ <%= tree_title(@node) %> +
+ +
+ +
+
+
<%= t(:'activerecord.attributes.node.title') %>
+
+
<%= t(:'activerecord.attributes.node.occasions') %>
+
<%= t(:'ui.updated') %>
+
+
+ +
+ <%= render partial: 'tree_node', collection: @node ? @node.children.ordered : Node.roots.ordered, as: 'node' %> +
+
+ +
\ No newline at end of file diff --git a/app/views/admin/nodes/_tree_node.html.erb b/app/views/admin/nodes/_tree_node.html.erb new file mode 100644 index 0000000..feb038b --- /dev/null +++ b/app/views/admin/nodes/_tree_node.html.erb @@ -0,0 +1,38 @@ +
+ +
+ + <%= link_to node_link_title(node), + url_for(action: 'tree', id: node), + class: 'node-link', + data: { + turbo_stream: true, + icon: t("nodes.icons.#{node.category}"), + nodes_id_param: node.id, + action: 'click->nodes#set_current' + } %> +
+ +
+ +
+ +
+ <%# node_flags(node) %> + +
+ +
+ <%= audit_info node %> +
+ +
+ <%= link_to 'edit', edit_admin_node_path(node), class:"icon size--medium round", data: {turbo_action: 'advance'} %> + <%= tag.div 'more_vert', class: 'handle' %> +
+ +
\ No newline at end of file diff --git a/app/views/admin/nodes/children.turbo_stream.erb b/app/views/admin/nodes/children.turbo_stream.erb new file mode 100644 index 0000000..6ce0c00 --- /dev/null +++ b/app/views/admin/nodes/children.turbo_stream.erb @@ -0,0 +1,8 @@ +<%= turbo_stream.append @node do %> + + <% @node.children.ordered.each do |node| %> + <%= render node, closed: true %> + <% end %> + +<% end %> + diff --git a/app/views/admin/nodes/create.turbo_stream.erb b/app/views/admin/nodes/create.turbo_stream.erb new file mode 100644 index 0000000..8f9537c --- /dev/null +++ b/app/views/admin/nodes/create.turbo_stream.erb @@ -0,0 +1,27 @@ +<%= turbo_stream.append "tree-nodes" do %> + <%= render partial: 'tree_node', object: @node, as: 'node' %> +<% end %> + + +<% if @node.parent %> + + + + + + + + + + +<% else %> + + + + + +<% end %> diff --git a/app/views/admin/nodes/destroy.turbo_stream.erb b/app/views/admin/nodes/destroy.turbo_stream.erb new file mode 100644 index 0000000..d635f5e --- /dev/null +++ b/app/views/admin/nodes/destroy.turbo_stream.erb @@ -0,0 +1,16 @@ +<%= turbo_stream.remove @destroyed_node %> + +<% if @node %> + + + +<% end %> + + +<%= turbo_stream.update "tree" do %> + <%= render partial: "tree" %> +<% end %> + +<%= turbo_stream.append 'flash', partial: 'layouts/flash' %> \ No newline at end of file diff --git a/app/views/admin/nodes/edit.html.erb b/app/views/admin/nodes/edit.html.erb new file mode 100644 index 0000000..02a4a52 --- /dev/null +++ b/app/views/admin/nodes/edit.html.erb @@ -0,0 +1,13 @@ +<%= content_for :title, @node.title %> + +<%= turbo_frame_tag 'main' do %> + + <%= turbo_stream.append 'flash' , partial: 'layouts/flash' %> + + <%= render partial: 'drawer' %> + + <%= turbo_frame_tag 'tree' do %> + <%= render partial: 'edit' %> + <% end %> + +<% end %> \ No newline at end of file diff --git a/app/views/admin/nodes/edit.turbo_stream.erb b/app/views/admin/nodes/edit.turbo_stream.erb new file mode 100644 index 0000000..fea29dc --- /dev/null +++ b/app/views/admin/nodes/edit.turbo_stream.erb @@ -0,0 +1,5 @@ +<%= turbo_stream.update "tree" do %> + <%= render partial: "edit" %> +<% end %> + + diff --git a/app/views/admin/nodes/index.html.erb b/app/views/admin/nodes/index.html.erb new file mode 100644 index 0000000..4dbd86c --- /dev/null +++ b/app/views/admin/nodes/index.html.erb @@ -0,0 +1,15 @@ +<%= content_for :title, t(:'nodes.title') %> + +<%= turbo_frame_tag 'main' do %> + + <%= turbo_stream.append 'flash', partial: 'layouts/flash' %> + + <%= render partial: 'drawer' %> + + <%= turbo_frame_tag 'tree' do %> + <%= render partial: 'tree' %> + <% end %> + +<% end %> + + \ No newline at end of file diff --git a/app/views/admin/nodes/sort.turbo_stream.erb b/app/views/admin/nodes/sort.turbo_stream.erb new file mode 100644 index 0000000..99b6c18 --- /dev/null +++ b/app/views/admin/nodes/sort.turbo_stream.erb @@ -0,0 +1,15 @@ +<% if @node.root? %> + + + +<% end %> + +<% if @node.parent %> + + + +<% end %> \ No newline at end of file diff --git a/app/views/admin/nodes/tree.turbo_stream.erb b/app/views/admin/nodes/tree.turbo_stream.erb new file mode 100644 index 0000000..76ef6c7 --- /dev/null +++ b/app/views/admin/nodes/tree.turbo_stream.erb @@ -0,0 +1,3 @@ +<%= turbo_stream.update 'tree' do %> + <%= render partial: 'tree' %> +<% end %> diff --git a/app/views/admin/nodes/update.turbo_stream.erb b/app/views/admin/nodes/update.turbo_stream.erb new file mode 100644 index 0000000..051eda4 --- /dev/null +++ b/app/views/admin/nodes/update.turbo_stream.erb @@ -0,0 +1,10 @@ +<%= turbo_stream.replace "node_form" do %> + <%= render "form", node: @node %> +<% end %> + + + + + + +<%= turbo_stream.append 'flash', partial: 'layouts/flash' %> \ No newline at end of file diff --git a/app/views/admin/sessions/new.html.erb b/app/views/admin/sessions/new.html.erb new file mode 100644 index 0000000..0df9da6 --- /dev/null +++ b/app/views/admin/sessions/new.html.erb @@ -0,0 +1,29 @@ +<%= link_to(svg('ikea-foundation'), root_path) %> + +<%= form_tag url_for(action: 'index', locale: nil) do %> + +
+ <%= label_tag :email, t('sessions.email') %> + <%= label_tag :email, class: "input-box" do %> + <%= text_field_tag :email, params[:email] %> + <% end %> +
+ + + <%= content_tag :div, class: flash.any? ? 'with-errors' : nil do %> + <%= label_tag :password, t('sessions.password') %> + <%= label_tag :password, class: "input-box" do %> + <%= password_field_tag :password %> + <% end %> + + + <%- flash.each do |name, msg| -%> + <%= content_tag :p, msg, role: 'alert' %> + <%- end -%> + <%- end -%> + +
+ <%= button_tag t(:'sessions.login') %> +
+ +<% end %> diff --git a/app/views/admin/sessions/verification.html.erb b/app/views/admin/sessions/verification.html.erb new file mode 100644 index 0000000..5574ba2 --- /dev/null +++ b/app/views/admin/sessions/verification.html.erb @@ -0,0 +1,20 @@ +<%= link_to(svg('ikea-foundation'), root_path) %> + +<%= form_tag url_for(action: 'verify', locale: nil) do %> + + <%= content_tag :div, class: flash.any? ? 'with-errors' : nil do %> + <%= label_tag :verification_code, t('sessions.verification_code') %> + <%= label_tag :verification_code, class: "input-box" do %> + <%= text_field_tag :verification_code, params[:verification_code] %> + <% end %> + + <%- flash.each do |name, msg| -%> + <%= content_tag :p, msg, role: 'alert' %> + <%- end -%> + <% end %> + +
+ <%= button_tag t(:'sessions.verify_email') %> +
+ +<% end %> \ No newline at end of file diff --git a/app/views/admin/users/_form.html.erb b/app/views/admin/users/_form.html.erb new file mode 100644 index 0000000..9731d58 --- /dev/null +++ b/app/views/admin/users/_form.html.erb @@ -0,0 +1,27 @@ + + <%= form_with(model: [ :admin, user ], class: 'form-plain has--key-ctrls' ) do |form| %> + +
+ <%= render partial: 'material/check_box_icon', locals: { f: form, attr: :enabled_at, label: t(:'activerecord.attributes.user.enabled_at')} %> + <%= render partial: 'material/select_field', locals: { f: form, attr: :role, include_blank: false, choices: User.roles.map { |k,v| [t(k, scope: :'users.roles'), k] }.sort } %> +
+ +
+ <%= render partial: 'material/text_field', locals: { f: form, attr: :firstname } %> + <%= render partial: 'material/text_field', locals: { f: form, attr: :lastname } %> + <%= render partial: 'material/text_field', locals: { f: form, attr: :title } %> + <%= render partial: 'material/text_field', locals: { f: form, attr: :email } %> + <%= render partial: 'material/text_field', locals: { f: form, attr: :phone } %> +
+ +
+ <%= render partial: 'material/password_field', locals: { f: form, attr: :password } %> + <%= render partial: 'material/password_field', locals: { f: form, attr: :password_confirmation } %> +
+ +
+ <%= link_to t(:'ui.cancel'), url_for(controller: 'users', action: 'index'), data: {turbo_action: 'advance'} %> + <%= form.submit t(:'ui.save') %> +
+ + <% end %> diff --git a/app/views/admin/users/_user.html.erb b/app/views/admin/users/_user.html.erb new file mode 100644 index 0000000..7bed2b2 --- /dev/null +++ b/app/views/admin/users/_user.html.erb @@ -0,0 +1,29 @@ +
+
+ +
+
+ <%= user.email %> +
+
+ <%= user.phone %> +
+
+ +
+
+ <%= link_to 'edit', url_for(action: 'edit', id: user), class:"icon size--medium round", data: {turbo_action: 'advance'} %> +
+
\ No newline at end of file diff --git a/app/views/admin/users/edit.html.erb b/app/views/admin/users/edit.html.erb new file mode 100644 index 0000000..721b7d8 --- /dev/null +++ b/app/views/admin/users/edit.html.erb @@ -0,0 +1,19 @@ +<%= content_for :title, @user.name %> + +<%= turbo_frame_tag 'main' do %> + + <%= turbo_stream.append 'flash', partial: 'layouts/flash' %> + +
+ <%= link_to t(:'users.list'), url_for(controller: 'users', action: 'index'), data: {turbo_action: 'advance'}, class: 'back-link' %> + +
+

<%= yield :title %>

+ + <%= button_to t(:'users.destroy'), url_for(action: 'show', id: @user), method: :delete, data: { turbo_confirm: t(:'ui.are_you_sure') }, tabindex: '-1', class: 'delete-link' %> +
+ + <%= render "form", user: @user %> + +
+<% end %> \ No newline at end of file diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb new file mode 100644 index 0000000..b96112b --- /dev/null +++ b/app/views/admin/users/index.html.erb @@ -0,0 +1,41 @@ +<%= content_for :title, t(:'users.title') %> + +<%= turbo_frame_tag 'main' do %> + + <%= turbo_stream.append 'flash', partial: 'layouts/flash' %> + +
+

<%= link_to yield(:title), params.permit(:sort, :reverse), class: 'list-title-link' %>

+ +
+ <%= render 'material/search' %> + <%= link_to tag.span(t(:'ui.new')), url_for(action: 'new'), class: 'btn add-btn', data: {turbo_action: 'advance'} %> +
+
+ +
+ +
+ +
+
<%= link_to_sortable :by_name, t(:'activerecord.attributes.user.name') %>
+
<%= link_to_sortable :by_email, t(:'activerecord.attributes.user.email') %>
+
<%= t(:'activerecord.attributes.user.phone') %>
+
<%= t(:'activerecord.attributes.user.status') %>
+
+
+ +
+ <%= render @users %> +
+ +
+
<%= page_entries_info @users %>
+ <%= paginate @users %> +
+ + +
+
+ +<% end %> \ No newline at end of file diff --git a/app/views/admin/users/new.html.erb b/app/views/admin/users/new.html.erb new file mode 100644 index 0000000..1f1db32 --- /dev/null +++ b/app/views/admin/users/new.html.erb @@ -0,0 +1,17 @@ +<%= content_for :title, t(:'users.new') %> + +<%= turbo_frame_tag 'main' do %> + + <%= turbo_stream.append 'flash', partial: 'layouts/flash' %> + +
+ <%= link_to t(:'users.list'), url_for(controller: 'users', action: 'index'), data: {turbo_action: 'advance'}, class: 'back-link' %> + +
+

<%= yield :title %>

+
+ + <%= render "form", user: @user %> + +
+<% end %> \ No newline at end of file diff --git a/app/views/kaminari/_first_page.html.erb b/app/views/kaminari/_first_page.html.erb new file mode 100644 index 0000000..0cc83b9 --- /dev/null +++ b/app/views/kaminari/_first_page.html.erb @@ -0,0 +1,11 @@ +<%# Link to the "First" page + - available local variables + url: url to the first page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> + + <%= link_to_unless current_page.first?, t('views.pagination.first').html_safe, url, remote: remote %> + diff --git a/app/views/kaminari/_gap.html.erb b/app/views/kaminari/_gap.html.erb new file mode 100644 index 0000000..bbb0f98 --- /dev/null +++ b/app/views/kaminari/_gap.html.erb @@ -0,0 +1,8 @@ +<%# Non-link tag that stands for skipped pages... + - available local variables + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> +<%= t('views.pagination.truncate').html_safe %> diff --git a/app/views/kaminari/_last_page.html.erb b/app/views/kaminari/_last_page.html.erb new file mode 100644 index 0000000..bc777b4 --- /dev/null +++ b/app/views/kaminari/_last_page.html.erb @@ -0,0 +1,11 @@ +<%# Link to the "Last" page + - available local variables + url: url to the last page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> + + <%= link_to_unless current_page.last?, t('views.pagination.last').html_safe, url, remote: remote %> + diff --git a/app/views/kaminari/_next_page.html.erb b/app/views/kaminari/_next_page.html.erb new file mode 100644 index 0000000..3b0d054 --- /dev/null +++ b/app/views/kaminari/_next_page.html.erb @@ -0,0 +1,11 @@ +<%# Link to the "Next" page + - available local variables + url: url to the next page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> + + <%= link_to_unless current_page.last?, t('views.pagination.next').html_safe, url, rel: 'next', remote: remote %> + diff --git a/app/views/kaminari/_page.html.erb b/app/views/kaminari/_page.html.erb new file mode 100644 index 0000000..393bfc4 --- /dev/null +++ b/app/views/kaminari/_page.html.erb @@ -0,0 +1,12 @@ +<%# Link showing page number + - available local variables + page: a page object for "this" page + url: url to this page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> + + <%= link_to_unless page.current?, page, url, {remote: remote, rel: page.rel} %> + diff --git a/app/views/kaminari/_paginator.html.erb b/app/views/kaminari/_paginator.html.erb new file mode 100644 index 0000000..183a16a --- /dev/null +++ b/app/views/kaminari/_paginator.html.erb @@ -0,0 +1,18 @@ +<%# The container tag + - available local variables + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote + paginator: the paginator that renders the pagination tags inside +-%> +<%= paginator.render do -%> + +<% end -%> diff --git a/app/views/kaminari/_prev_page.html.erb b/app/views/kaminari/_prev_page.html.erb new file mode 100644 index 0000000..0f32af4 --- /dev/null +++ b/app/views/kaminari/_prev_page.html.erb @@ -0,0 +1,11 @@ +<%# Link to the "Previous" page + - available local variables + url: url to the previous page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> + + <%= link_to_unless current_page.first?, t('views.pagination.previous').html_safe, url, rel: 'prev', remote: remote %> + diff --git a/app/views/layouts/_flash.html.erb b/app/views/layouts/_flash.html.erb new file mode 100644 index 0000000..ea7d857 --- /dev/null +++ b/app/views/layouts/_flash.html.erb @@ -0,0 +1,5 @@ + <% flash.each do |flash_type, message| %> +
+ info<%= tag.span message %> +
+ <% end %> diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb new file mode 100644 index 0000000..d94bc71 --- /dev/null +++ b/app/views/layouts/admin.html.erb @@ -0,0 +1,70 @@ + + + + <%= content_for?(:title) ? yield(:title) : ENV['PROJECT_NAME_LONG'] %> + + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= stylesheet_link_tag "admin", "trix", "tom-select", "popup-menu", "forms", "lists", "assets", "nodes", "attachments" %> + <%= javascript_importmap_tags 'admin' %> + + + + + <%= yield %> + +
+ <%= render 'layouts/flash' %> +
+ + + \ No newline at end of file diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb index 3aac900..3868f9b 100644 --- a/app/views/layouts/mailer.html.erb +++ b/app/views/layouts/mailer.html.erb @@ -1,13 +1,121 @@ - + - - - - - - - <%= yield %> - - + + + + + + + + + + + + + + + + +
+ + + + + + <%= yield %> + +
+ <%# link_to image_tag('den-hirschsprungske-samling.png', size: "180x58" , alt: I18n.t(:client_name)), + root_url, title: I18n.t(:client_name) %> +

+ Two-factor authentication +

+
+
+ + + + \ No newline at end of file diff --git a/app/views/layouts/sessions.html.erb b/app/views/layouts/sessions.html.erb new file mode 100644 index 0000000..d2d3706 --- /dev/null +++ b/app/views/layouts/sessions.html.erb @@ -0,0 +1,18 @@ + + + + <%= content_for?(:title) ? yield(:title) : t('project_name') %> + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= stylesheet_link_tag "sessions" %> + + + +
+ <%= yield %> +
+ + + diff --git a/app/views/material/_buttons_number_field.html.erb b/app/views/material/_buttons_number_field.html.erb new file mode 100644 index 0000000..511b182 --- /dev/null +++ b/app/views/material/_buttons_number_field.html.erb @@ -0,0 +1,31 @@ +<%= tag.div class: 'button_number_field' do %> + + <%= f.label attr, local_assigns[:label] unless local_assigns[:skip_label] %> + +
+ <%= button_tag('remove', + class: "decrease-button hidden btn", + type: 'button', + data: { + cvalue: -1, + })%> + + <%= f.number_field attr, + value: local_assigns[:value] || f.object.public_send(attr), + class: local_assigns[:class], + in: local_assigns[:in], + disabled: local_assigns[:disabled]%> + + <%= button_tag('add', + class: "increase-button hidden btn", + type: 'button', + data: { + cvalue: 1, + })%> +
+ + <%- f.object.errors.full_messages_for(attr).uniq.each do |msg| -%> + <%= tag.p msg, role: 'alert' %> + <% end %> + +<%- end -%> diff --git a/app/views/material/_check_box.html.erb b/app/views/material/_check_box.html.erb new file mode 100644 index 0000000..93135b4 --- /dev/null +++ b/app/views/material/_check_box.html.erb @@ -0,0 +1,5 @@ +<%= f.label attr, class: "icon-cb #{attr}", title: local_assigns[:title] do %> + <%= local_assigns[:label] %> + <%= f.check_box attr %> + <%= local_assigns[:icon] %> +<%- end -%> \ No newline at end of file diff --git a/app/views/material/_check_box_icon.html.erb b/app/views/material/_check_box_icon.html.erb new file mode 100644 index 0000000..3b5df2a --- /dev/null +++ b/app/views/material/_check_box_icon.html.erb @@ -0,0 +1,19 @@ +<%= tag.div class: 'field' do %> +
+ <%= f.label attr, local_assigns[:label], for: nil unless local_assigns[:skip_label] %> +
+ +
+ <%= f.label attr, class: "icon-cb toogle #{attr}", title: local_assigns[:title] do %> + + <% if attr.to_s.ends_with?("_at") %> + <%= f.check_box attr, {}, f.object.public_send(attr) || Time.now, 0 %> + <% else %> + <%= f.check_box attr %> + <% end %> + + check_box_outline_blankcheck_box + <%- end -%> +
+<%- end -%> + diff --git a/app/views/material/_date_select.html.erb b/app/views/material/_date_select.html.erb new file mode 100644 index 0000000..6abe00b --- /dev/null +++ b/app/views/material/_date_select.html.erb @@ -0,0 +1,9 @@ +
+ <%= f.label attr %> +
+ <%= f.date_select attr, use_short_month: true, start_year: (local_assigns[:start_year].present?? start_year : 1990), end_year: (Date.today.year + 5), :include_blank => attr || false %> +
+ <%- f.object.errors.full_messages_for(attr).uniq.each do |msg| -%> + <%= content_tag :p, msg, role: 'alert' %> + <% end %> +
diff --git a/app/views/material/_i18n.html.erb b/app/views/material/_i18n.html.erb new file mode 100644 index 0000000..1efc2a0 --- /dev/null +++ b/app/views/material/_i18n.html.erb @@ -0,0 +1,7 @@ +
+ <%- I18n.available_locales.each do |locale| %> + <%= button_tag type: 'button', data: { locale: locale }, class: locale == form_locale ? 'current' : nil do %> + <%= locale.to_s %> + <%- end -%> + <% end %> +
\ No newline at end of file diff --git a/app/views/material/_number_field.html.erb b/app/views/material/_number_field.html.erb new file mode 100644 index 0000000..a1b9485 --- /dev/null +++ b/app/views/material/_number_field.html.erb @@ -0,0 +1,14 @@ +<%= content_tag :div, class: 'field', style: local_assigns[:style] do %> + <%= f.number_field attr, + value: local_assigns[:value] || f.object.public_send(attr), + class: local_assigns[:class], + min: local_assigns[:min], + max: local_assigns[:max], + step: local_assigns[:step], + + disabled: local_assigns[:disabled]%> + <%= f.label attr, local_assigns[:label] unless local_assigns[:skip_label] %> + <%- f.object.errors.full_messages_for(attr).uniq.each do |msg| -%> + <%= content_tag :p, msg, role: 'alert' %> + <% end %> +<%- end -%> diff --git a/app/views/material/_password_field.html.erb b/app/views/material/_password_field.html.erb new file mode 100644 index 0000000..e4730d3 --- /dev/null +++ b/app/views/material/_password_field.html.erb @@ -0,0 +1,15 @@ +<%= tag.div class: 'field' do %> +
+ <%= f.label attr, local_assigns[:label], for: nil unless local_assigns[:skip_label] %> +
+ +
+ <%= f.label attr, class: "input-box" do %> + <%= f.password_field attr, class: 'material__input' %> + <% end %> + + <%- f.object.errors.full_messages_for(attr).uniq.each do |msg| -%> + <%= content_tag :p, msg, role: 'alert' %> + <% end %> +
+<%- end -%> diff --git a/app/views/material/_radio_button.html.erb b/app/views/material/_radio_button.html.erb new file mode 100644 index 0000000..c6c601e --- /dev/null +++ b/app/views/material/_radio_button.html.erb @@ -0,0 +1,5 @@ +<%= f.label attr, class: "icon-rb #{attr}", title: local_assigns[:title], for: nil do %> + <%= local_assigns[:label] %> + <%= f.radio_button attr, local_assigns[:value], :checked => local_assigns[:checked] %> + <%= local_assigns[:icon] %> +<%- end -%> diff --git a/app/views/material/_search.html.erb b/app/views/material/_search.html.erb new file mode 100644 index 0000000..eb24c7d --- /dev/null +++ b/app/views/material/_search.html.erb @@ -0,0 +1,14 @@ +<%= form_tag url_for(), method: :get, class: "search__collection", data: {turbo_stream: local_assigns[:turbo_stream]} do %> + + <%= link_to 'close', params.permit(:sort, :reverse, :category, :tag), class: 'icon reset__search', data: {turbo_stream: local_assigns[:turbo_stream]} %> + +
+ <%= text_field_tag :q, params[:q], spellcheck: false, placeholder: t(:'ui.search') %> + <%= button_tag 'search', id: nil, name: nil, class: 'icon' %> +
+ + <%- [:sort, :reverse, :category, :tag].each do |k| -%> + <%= hidden_field_tag k, params[k] if params[k] %> + <% end %> + +<%- end -%> diff --git a/app/views/material/_select_field.html.erb b/app/views/material/_select_field.html.erb new file mode 100644 index 0000000..6f9851d --- /dev/null +++ b/app/views/material/_select_field.html.erb @@ -0,0 +1,23 @@ +<%= tag.div class: 'field' do %> +
+ <%= f.label attr, local_assigns[:label], for: nil unless local_assigns[:skip_label] %> +
+ +
+ <%= f.label attr, class: "input-box" do %> + <%= f.select attr, + choices, + { + include_blank: local_assigns[:include_blank].nil? ? true : local_assigns[:include_blank], + multiple: local_assigns[:multiple] + }, + { + data: local_assigns[:data], + class: 'material__input material__input-select' + } %> + <% end %> + <%- f.object.errors.full_messages_for(attr).uniq.each do |msg| -%> + <%= content_tag :p, msg, role: 'alert' %> + <% end %> +
+<%- end -%> diff --git a/app/views/material/_text_area.html.erb b/app/views/material/_text_area.html.erb new file mode 100644 index 0000000..a76ef4d --- /dev/null +++ b/app/views/material/_text_area.html.erb @@ -0,0 +1,18 @@ +<%= tag.div class: 'field' do %> +
+ <%= f.label attr, local_assigns[:label], for: nil unless local_assigns[:skip_label] %> +
+ +
+ <%= f.label attr, class: "input-box" do %> + <%= f.text_area attr, + rows: local_assigns[:rows] || 3, + class: 'material__input', + placeholder: local_assigns[:placeholder] %> + <% end %> + + <%- f.object.errors.full_messages_for(attr).uniq.each do |msg| -%> + <%= content_tag :p, msg, role: 'alert' %> + <% end %> +
+<%- end -%> diff --git a/app/views/material/_text_area_i18n.html.erb b/app/views/material/_text_area_i18n.html.erb new file mode 100644 index 0000000..db5b2dd --- /dev/null +++ b/app/views/material/_text_area_i18n.html.erb @@ -0,0 +1,16 @@ +<%= content_tag :div, class: 'field', style: local_assigns[:style] do %> + + <%- I18n.available_locales.each do |locale| %> + <%- i18n_attr = "#{attr}_#{locale}" -%> + <%= content_tag :div, class: "i18n #{locale}" do %> + + <%= f.text_area i18n_attr, class: local_assigns[:class], disabled: local_assigns[:disabled] %> + <%= f.label i18n_attr, t(attr, scope: "activerecord.attributes.#{f.object.class.to_s.downcase}") %> + + <%- f.object.errors.full_messages_for(attr).uniq.each do |msg| -%> + <%= content_tag :p, msg, role: 'alert' %> + <% end %> + <%- end -%> + <%- end -%> + +<%- end -%> \ No newline at end of file diff --git a/app/views/material/_text_field.html.erb b/app/views/material/_text_field.html.erb new file mode 100644 index 0000000..7a08903 --- /dev/null +++ b/app/views/material/_text_field.html.erb @@ -0,0 +1,21 @@ +<%= tag.div class: 'field' do %> +
+ <%= f.label attr, local_assigns[:label], for: nil unless local_assigns[:skip_label] %> +
+ +
+ <%= f.label attr, class: "input-box" do %> + <%= f.text_field attr, + class: 'material__input', + disabled: local_assigns[:disabled], + data: local_assigns[:data], + placeholder: local_assigns[:placeholder] %> + <% end %> + + <%= tag.p local_assigns[:faq], role: 'tooltip' if local_assigns[:faq] %> + + <%- f.object.errors.full_messages_for(attr).uniq.each do |msg| -%> + <%= tag.p msg, role: 'alert' %> + <% end %> +
+<%- end -%> diff --git a/app/views/material/_text_field_i18n.html.erb b/app/views/material/_text_field_i18n.html.erb new file mode 100644 index 0000000..8415436 --- /dev/null +++ b/app/views/material/_text_field_i18n.html.erb @@ -0,0 +1,23 @@ +<%= tag.div class: 'field' do %> +
+ <%= f.label attr, local_assigns[:label], for: nil, class: 'i18n__label' unless local_assigns[:skip_label] %> +
+ +
+ <%- I18n.available_locales.each do |locale| %> + <%- i18n_attr = "#{attr}_#{locale}" -%> + <%= f.label i18n_attr, class: "input-box i18n__input i18n__input-#{locale}" do %> + + <%= f.text_field i18n_attr, + class: 'material__input', + disabled: local_assigns[:disabled], + data: local_assigns[:data], + placeholder: local_assigns[:placeholder] %> + <% end %> + + <%- f.object.errors.full_messages_for(i18n_attr).uniq.each do |msg| -%> + <%= content_tag :p, msg, role: 'alert' %> + <% end %> + <% end %> +
+<%- end -%> diff --git a/app/views/material/_text_field_i18n_simple.html.erb b/app/views/material/_text_field_i18n_simple.html.erb new file mode 100644 index 0000000..04415d9 --- /dev/null +++ b/app/views/material/_text_field_i18n_simple.html.erb @@ -0,0 +1,14 @@ +<%- I18n.available_locales.each do |locale| %> + <%- i18n_attr = "#{attr}_#{locale}" -%> +
+ <%= f.text_field i18n_attr, + class: "material__input", + disabled: local_assigns[:disabled], + data: local_assigns[:data], + placeholder: local_assigns[:placeholder] %> + + <%- f.object.errors.full_messages_for(i18n_attr).uniq.each do |msg| -%> + <%= content_tag :p, msg, role: 'alert' %> + <% end %> +
+<% end %> diff --git a/app/views/material/_text_field_simple.html.erb b/app/views/material/_text_field_simple.html.erb new file mode 100644 index 0000000..867217a --- /dev/null +++ b/app/views/material/_text_field_simple.html.erb @@ -0,0 +1,7 @@ +<%- + error_attr = attr.to_s.sub(/price_str$/, 'price_minor') + error_attr = f.object.has_attribute?(error_attr) ? error_attr : attr +-%> +<%= f.text_field attr, + class: f.object.errors.include?(error_attr) ? 'field_with_errors' : nil, + title: f.object.errors.full_messages_for(error_attr).uniq.join("\n") %> \ No newline at end of file diff --git a/app/views/material/_time_field.html.erb b/app/views/material/_time_field.html.erb new file mode 100644 index 0000000..fe809fa --- /dev/null +++ b/app/views/material/_time_field.html.erb @@ -0,0 +1,9 @@ +
+ <%= f.label attr %> +
+ <%= f.time_select attr, include_seconds: false, minute_step: 15, time_separator: ' ' %> +
+ <%- f.object.errors.full_messages_for(attr).uniq.each do |msg| -%> + <%= content_tag :p, msg, role: 'alert' %> + <% end %> +
diff --git a/app/views/material/_tom_select_field.html.erb b/app/views/material/_tom_select_field.html.erb new file mode 100644 index 0000000..ff3e9e6 --- /dev/null +++ b/app/views/material/_tom_select_field.html.erb @@ -0,0 +1,26 @@ +<%= tag.div class: 'field' do %> +
+ <%= f.label attr, local_assigns[:label], for: nil unless local_assigns[:skip_label] %> +
+ +
+ <%= f.label attr, class: "input-box" do %> + <%= f.select attr, + choices, + { + include_blank: local_assigns[:include_blank].nil? ? true : local_assigns[:include_blank], + multiple: local_assigns[:multiple] + }, + { + data: { + controller: 'select', + tags: local_assigns[:tags] + } + } %> + <% end %> + <%- f.object.errors.full_messages_for(attr).uniq.each do |msg| -%> + <%= content_tag :p, msg, role: 'alert' %> + <% end %> +
+<%- end -%> + diff --git a/app/views/material/_tom_select_field_i18n.html.erb b/app/views/material/_tom_select_field_i18n.html.erb new file mode 100644 index 0000000..112bc98 --- /dev/null +++ b/app/views/material/_tom_select_field_i18n.html.erb @@ -0,0 +1,32 @@ +<%= tag.div class: 'field' do %> +
+ <%= f.label attr, local_assigns[:label], for: nil unless local_assigns[:skip_label] %> +
+ +
+ <%- I18n.available_locales.each do |locale| %> + <%- i18n_attr = "#{attr}_#{locale}" -%> + <%= f.label i18n_attr, class: "input-box i18n__input i18n__input-#{locale}" do %> + + <%= f.select i18n_attr, + choices[locale], + { + include_blank: local_assigns[:include_blank].nil? ? true : local_assigns[:include_blank], + multiple: local_assigns[:multiple] + }, + { data: { + controller: 'select', + tags: local_assigns[:tags] + } + } %> + <% end %> + + <%- f.object.errors.full_messages_for(i18n_attr).uniq.each do |msg| -%> + <%= content_tag :p, msg, role: 'alert' %> + <% end %> + <% end %> + + + +
+<%- end -%> diff --git a/app/views/material/_trix_field.html.erb b/app/views/material/_trix_field.html.erb new file mode 100644 index 0000000..a36efad --- /dev/null +++ b/app/views/material/_trix_field.html.erb @@ -0,0 +1,13 @@ +<%= content_tag :div, class: local_assigns[:class] do %> +
+ + <%- trix_id = f.object.new_record? ? (local_assigns[:child_index] ? "#{dom_id(f.object, attr)}_#{local_assigns[:child_index]}" : + "#{attr}_new_#{model_name_from_record_or_class(f.object).param_key.pluralize}" ) : + dom_id(f.object, attr) -%> + + <%= f.label attr, local_assigns[:label], for: nil unless local_assigns[:skip_label] %> + + <%= f.hidden_field attr, id: trix_id %> + +
+<%- end -%> \ No newline at end of file diff --git a/app/views/material/_trix_field_i18n.html.erb b/app/views/material/_trix_field_i18n.html.erb new file mode 100644 index 0000000..c282803 --- /dev/null +++ b/app/views/material/_trix_field_i18n.html.erb @@ -0,0 +1,15 @@ +<%= content_tag :div, class: 'trix__field-i18n' do %> + + <%- I18n.available_locales.each do |locale| %> + <%- i18n_attr = "#{attr}_#{locale}" -%> + <%= render partial: 'material/trix_field', + locals: { + f: f, + attr: i18n_attr, + class: "i18n__input i18n__input-#{locale}", + label_class: '', + label: local_assigns[:label] + } %> + <%- end -%> + +<%- end -%> \ No newline at end of file diff --git a/app/views/user_mailer/verify_email.html.erb b/app/views/user_mailer/verify_email.html.erb new file mode 100644 index 0000000..2a5548c --- /dev/null +++ b/app/views/user_mailer/verify_email.html.erb @@ -0,0 +1,32 @@ + + +
+ + + + + + +
+ + + + + + +
+
+ <%= @verification_code.token %> +
+
+
+
+ + + + + + <%= simple_format t(:'mailers.not_you', time: @verification_code.expires_at.strftime("%H.%M")) %> + + \ No newline at end of file diff --git a/app/views/user_mailer/verify_email.text.erb b/app/views/user_mailer/verify_email.text.erb new file mode 100644 index 0000000..9196bfd --- /dev/null +++ b/app/views/user_mailer/verify_email.text.erb @@ -0,0 +1,3 @@ +<%= t('mailers.verify_email_subject', token: @verification_code&.token) %> + +<%= t('mailers.not_you', time: @verification_code.expires_at.strftime("%H.%M")) %> \ No newline at end of file diff --git a/config/environments/development.rb b/config/environments/development.rb index c129b5f..1b982fd 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -39,6 +39,21 @@ Rails.application.configure do # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false + config.action_mailer.delivery_method = :smtp + + config.action_mailer.default_url_options = { host: 'gw.oncotype.dk', protocol: 'https' } + + config.action_mailer.smtp_settings = { + address: 'asmtp.bluepipe.dk', + port: 587, + domain: 'week2024.ikeafoundation.org', + authentication: :login, + user_name: 'oncotype@oncotype.dk', + password: 'y9usn&rpErwY' + } + + config.hosts << "gw.oncotype.dk" + config.action_mailer.perform_caching = false # Print deprecation notices to the Rails logger. diff --git a/config/environments/production.rb b/config/environments/production.rb index 637b41f..1516bb8 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -44,7 +44,7 @@ Rails.application.configure do # config.assume_ssl = true # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - config.force_ssl = true + config.force_ssl = false # Log to STDOUT by default config.logger = ActiveSupport::Logger.new(STDOUT) @@ -72,6 +72,17 @@ Rails.application.configure do # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false + config.action_mailer.delivery_method = :smtp + + config.action_mailer.default_url_options = { host: 'ikea-foundation-week-2024.onc.dk', protocol: 'https' } + + config.action_mailer.smtp_settings = { + address: 'smtp.bluepipe.net', + port: 25, + domain: 'ikea-foundation-week-2024.onc.dk' + } + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true diff --git a/config/importmap.rb b/config/importmap.rb index ec32e4d..3f59ca1 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -1,9 +1,16 @@ -# Pin npm packages by running ./bin/importmap +pin "admin" pin "@hotwired/turbo-rails", to: "turbo.min.js" -pin "@hotwired/stimulus", to: "stimulus.min.js" +pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2 pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" pin_all_from "app/javascript/controllers", under: "controllers" +pin "sortablejs" # @1.15.2 +pin "stimulus-use" # @0.52.2 +pin "@rails/request.js", to: "@rails--request.js.js" # @0.0.9 +pin "@rails/activestorage", to: "@rails--activestorage.js" # @7.1.3 +pin "tom-select" # @2.3.1 +pin "trix" # @2.1.0 + # site_helper pin "application", preload: false diff --git a/config/initializers/dkim.rb b/config/initializers/dkim.rb new file mode 100644 index 0000000..bc32ba6 --- /dev/null +++ b/config/initializers/dkim.rb @@ -0,0 +1,7 @@ +# Configure dkim globally +Dkim::domain = 'onc.dk' +Dkim::selector = '042024' +Dkim::private_key = open(Rails.root.join('dkim', 'private.key')).read + +# This will sign all ActionMailer deliveries +ActionMailer::Base.register_interceptor(Dkim::Interceptor) diff --git a/config/initializers/premailer_rails.rb b/config/initializers/premailer_rails.rb new file mode 100644 index 0000000..56f2acc --- /dev/null +++ b/config/initializers/premailer_rails.rb @@ -0,0 +1,8 @@ +Premailer::Rails.config.merge!( + preserve_styles: true, + remove_ids: true, + input_encoding: 'UTF-8', + generate_text_part: true, + strategies: [:filesystem, :asset_pipeline, :network], + base_url: (Rails.env.production? ? 'https://week2024.ikeafoundation.org' : 'https://gw.oncotype.dk') +) diff --git a/config/locales/en.yml b/config/locales/en.yml index b964a36..7e44ddf 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,5 +1,6 @@ en: + domain: week2024.ikeafoundation.org project_name: IKEA Foundation Week 2024 languages: @@ -67,5 +68,500 @@ en: See if you can spot the link between our partners’ game-changing initiatives and their remarkable impact. + ui: + 'yes': Yes + 'no': No + send: Send + active: Active + inactive: Inactive + cancel: Cancel + search: Search + are_you_sure: Are you sure you want to delete this item? This action cannot be undone. + will_overwrite: You are about to perform an action that will overwrite the existing transcription. This process is irreversible, and all previous transcription data will be permanently replaced. + updated: Updated + untitled: Untitled + main_menu: Main Menu + new: New + edit: Edit + save: Save + add: Create + append: Add + append_text: Add Text + append_asset: Add File + download: Download + category_created: "%{category} saved" + category_updated: "%{category} updated" + category_destroyed: "%{category} deleted" + delete: Delete + destroy: Delete + destroyed: Deleted + example: Example + copy: Copy + copied: Copied + reindex: Reindex + utils: + search_engines: SEO + visibility: Visibility + settings: Settings + danish: Danish + english: English + transparent: Transparent + example: Example + choose: Choose + choose_image: Choose image + change_image: Change image + add: Add + out_of: out of + save: Save + cancel: Cancel + back: Back + remove: Delete + address: Address + clear_cache: Clear cache + cache_cleared: The cache is now cleared + search: Search + updated_at: Last updated + close: Close + new: Add + edit: Edit + details: Details + delete: Delete + are_you_sure: This action cannot be undone - Are you sure you want to continue? + upload: Upload + download: Download + move: Move + plus: + + up: Up + down: Down + copy: Copy + send: Send + preview: Preview + content: Content + or: or + reset: Reset + read_more: Read more + behavior: Behavior + systematics: Systematics + + users: + you_cant_change_the_email_on_this_user: You cannot change the email for this user + you_cant_change_this_on_this_user: You cannot change this for this user + you_cant_disable_this_user: You cannot deactivate this user + cant_destroy_this_user: You cannot remove this user + new: Create user + created: User saved + updated: User updated + destroy: Delete user + destroyed: User deleted + destroy_failed: Could not delete this user + title: Users and Permissions + list: User list + roles: + admin: Admin + user: Guest + + assets: + new: Upload file + created: File saved + updated: File updated + destroy: Delete file + destroyed: File deleted + destroy_failed: Could not delete this file + title: Files + + sort: + by_name: Name + by_filename: Filename + by_last_modified: Last updated + + nodes: + sort: Sort subpages %{parent} + labels: Facts + project_details: Details + content: Content + title: Pages + created: Page saved + updated: Page updated + destroy: Delete page + destroyed: Page deleted + destroy_failed: Could not delete this page + move: Move folder/page to... + layout: Page attributes + add_expire_date: Add expiration date + remove_expire_date: Remove + add_part: New section + settings: + main_menu: Main Menu + sub_menu: Submenu + footer_node: Footer + cookie_policy: Cookie policy + cta_link: Call To Action link + opening_hours: Opening hours + negative_menu: White menu + buy_ticket: Buy ticket & Annual pass + newsletter: Subscribe to newsletter + langs: + da: Danish + en: English + de: German + templates: + tmpl_index: Home page + tmpl_article: Page + tmpl_lists: Lists + tmpl_list: List + tmpl_exhibitions: Exhibitions + tmpl_occasions: Calendar + tmpl_poster: Poster + tmpl_story: Short story + tmpl_text: Text + categories: + box: Box + folder: Folder + cover: Cover + document: Page + new_categories: + box: Box + folder: Folder + cover: Cover + document: Add page + statuses: + status_draft: Draft + status_published: Published + status_archived: Archived + icons: + box: inventory_2 + folder: folder + cover: book_2 + document: description + + attachments: + alignments: + N: North + NE: Northeast + E: East + SE: Southeast + S: South + SW: Southwest + W: West + NW: Northwest + templates: + Hero: Hero + L: Large + + + mailers: + not_you: "This code expires at %{time}. If it was not you who logged in, you should reset your password." + verify_email_subject: "Your verification code is: %{token}" + + icons: + assets: image + users: person + nodes: file_copy + tags: sell + date_formats: schedule + subscribers: group + newsletters: mail + + sessions: + login: Log in + email: Email + password: Password + login_failed: Login failed + logout: Log out + verification_code: Verification code + verify_email: Verify + verification_failed: Verification failed + + activerecord: + errors: + messages: + record_invalid: 'Validation failed: %{errors}' + restrict_dependent_destroy: + has_one: Cannot delete record because a dependent %{record} exists + has_many: Cannot delete record because dependent %{record} exist + models: + node: + attributes: + expires_at: + not_a_date: is not valid + after: must be a future date + models: + user: + zero: users + one: user + other: users + attachment: + zero: attachments + one: attachment + other: attachments + asset: + zero: files + one: file + other: files + node: + zero: pages + one: page + other: pages + + attributes: + node: + title: Title + page_title: Page Title + page_description: Meta Description + slug: Part of URL + url: URL + published_at: Published From + status: Status + template: Template + href: Link to + parent_id: Parent + expires_at: Published Until + settings: Settings + tags_da: Tags + tags_en: Tags + tags_de: Tags + excluded_locales: Not visible under languages + is_allowlist: Invert list to only visible + occasions: Event + attachments: Attachments + + attachment: + body: Content + body_da: Content + body_en: Content + body_de: Content + url: Link + fg_color: Text Color + bg_color: Background Color + alignment: Alignment + template: Template + + asset: + title: File Name + created_at: Created + updated_at: Last Updated + + user: + enabled_at: Active + role: Role + title: Title + phone: Phone + name: Name + firstname: First Name + lastname: Last Name + email: Email + password: Password + password_confirmation: Confirm Password + + date: + abbr_day_names: + - Sun + - Mon + - Tue + - Wed + - Thu + - Fri + - Sat + abbr_month_names: + - + - Jan + - Feb + - Mar + - Apr + - May + - Jun + - Jul + - Aug + - Sep + - Oct + - Nov + - Dec + day_names: + - Sunday + - Monday + - Tuesday + - Wednesday + - Thursday + - Friday + - Saturday + formats: + default: "%Y-%m-%d" + long: "%B %d, %Y" + short: "%b %d" + month_names: + - + - January + - February + - March + - April + - May + - June + - July + - August + - September + - October + - November + - December + order: + - :year + - :month + - :day + datetime: + distance_in_words: + about_x_hours: + one: about %{count} hour + other: about %{count} hours + about_x_months: + one: about %{count} month + other: about %{count} months + about_x_years: + one: about %{count} year + other: about %{count} years + almost_x_years: + one: almost %{count} year + other: almost %{count} years + half_a_minute: half a minute + less_than_x_seconds: + one: less than %{count} second + other: less than %{count} seconds + less_than_x_minutes: + one: less than a minute + other: less than %{count} minutes + over_x_years: + one: over %{count} year + other: over %{count} years + x_seconds: + one: "%{count} second" + other: "%{count} seconds" + x_minutes: + one: "%{count} minute" + other: "%{count} minutes" + x_days: + one: "%{count} day" + other: "%{count} days" + x_months: + one: "%{count} month" + other: "%{count} months" + x_years: + one: "%{count} year" + other: "%{count} years" + prompts: + second: Second + minute: Minute + hour: Hour + day: Day + month: Month + year: Year + errors: + format: "%{attribute} %{message}" + messages: + accepted: must be accepted + blank: can't be blank + confirmation: doesn't match %{attribute} + empty: can't be empty + equal_to: must be equal to %{count} + even: must be even + exclusion: is reserved + greater_than: must be greater than %{count} + greater_than_or_equal_to: must be greater than or equal to %{count} + in: must be in %{count} + inclusion: is not included in the list + invalid: is invalid + less_than: must be less than %{count} + less_than_or_equal_to: must be less than or equal to %{count} + model_invalid: 'Validation failed: %{errors}' + not_a_number: is not a number + not_an_integer: must be an integer + odd: must be odd + other_than: must be other than %{count} + present: must be blank + required: must exist + taken: has already been taken + too_long: + one: is too long (maximum is %{count} character) + other: is too long (maximum is %{count} characters) + too_short: + one: is too short (minimum is %{count} character) + other: is too short (minimum is %{count} characters) + wrong_length: + one: is the wrong length (should be %{count} character) + other: is the wrong length (should be %{count} characters) + template: + body: 'There were problems with the following fields:' + header: + one: "%{count} error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + helpers: + select: + prompt: Please select + submit: + create: Create %{model} + submit: Save %{model} + update: Update %{model} + number: + currency: + format: + delimiter: "," + format: "%u%n" + precision: 2 + separator: "." + significant: false + strip_insignificant_zeros: false + unit: "$" + format: + delimiter: "," + precision: 3 + round_mode: default + separator: "." + significant: false + strip_insignificant_zeros: false + human: + decimal_units: + format: "%n %u" + units: + billion: Billion + million: Million + quadrillion: Quadrillion + thousand: Thousand + trillion: Trillion + unit: '' + format: + delimiter: '' + precision: 3 + significant: true + strip_insignificant_zeros: true + storage_units: + format: "%n %u" + units: + byte: + one: Byte + other: Bytes + eb: EB + gb: GB + kb: KB + mb: MB + pb: PB + tb: TB + percentage: + format: + delimiter: '' + format: "%n%" + precision: + format: + delimiter: '' + support: + array: + last_word_connector: ", and " + two_words_connector: " and " + words_connector: ", " + time: + am: am + formats: + default: "%a, %d %b %Y %H:%M:%S %z" + long: "%B %d, %Y %H:%M" + short: "%d %b %H:%M" + listing: ! "%e. %b %Y" + medium: ! '%e. %b %Y, %H.%M' + pm: pm diff --git a/config/routes.rb b/config/routes.rb index 7c92576..33bd0c4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,7 +5,46 @@ Rails.application.routes.draw do # Can be used by load balancers and uptime monitors to verify that the app is live. # get "up" => "rails/health#show", as: :rails_health_check - + get 'admin', to: redirect('/admin/en') + + namespace :admin do + scope ':locale', constraints: { locale: /en|zh|hr|cs|da|nl|fi|fr|fr|de|hu|it|ja|ko|nb|pl|pt|ro|sr|sk|sl|es|sv|uk/ } do + + # Assets + resources :assets do + collection do + post :upload + end + end + + # Nodes + resources :nodes do + member do + get :children + get :tree + end + collection do + get :tree + patch :sort + patch :toggle + end + resources :assets, only: [ :index ] + resources :attachments, only: [ :new ] + end + + # Users + resources :users + + root to: 'nodes#index' + end + + resources :sessions, path: 'login', only: [:index, :create, :destroy] + get "logout", to: 'sessions#destroy', as: "logout" + + # 2 step auth + post 'login/verify', to: 'sessions#verify' + get 'login/verification', to: 'sessions#verification' + end scope ':locale', constraints: { locale: /en|zh|hr|cs|da|nl|fi|fr|fr|de|hu|it|ja|ko|nb|pl|pt|ro|sr|sk|sl|es|sv|uk/ } do get '', to: 'site#index' diff --git a/db/migrate/20240419142317_create_user_roles.rb b/db/migrate/20240419142317_create_user_roles.rb new file mode 100644 index 0000000..e048e3a --- /dev/null +++ b/db/migrate/20240419142317_create_user_roles.rb @@ -0,0 +1,13 @@ +class CreateUserRoles < ActiveRecord::Migration[7.1] + def up + create_enum :user_roles, %w[user admin] + end + + def down + # While there is a `create_enum` method, there is no way to drop it. You can + # how ever, use raw SQL to drop the enum type. + execute <<-SQL + DROP TYPE user_roles; + SQL + end +end diff --git a/db/migrate/20240424092457_create_assets.rb b/db/migrate/20240424092457_create_assets.rb new file mode 100644 index 0000000..50cd8b7 --- /dev/null +++ b/db/migrate/20240424092457_create_assets.rb @@ -0,0 +1,8 @@ +class CreateAssets < ActiveRecord::Migration[7.1] + def change + create_table :assets do |t| + t.text "title" + t.timestamps + end + end +end diff --git a/db/migrate/20240424092958_create_users.rb b/db/migrate/20240424092958_create_users.rb new file mode 100644 index 0000000..098428a --- /dev/null +++ b/db/migrate/20240424092958_create_users.rb @@ -0,0 +1,12 @@ +class CreateUsers < ActiveRecord::Migration[7.1] + def change + create_table :users do |t| + t.text :email, :password_digest, :firstname, :lastname, :title, :phone, :description + t.enum :role, enum_type: :user_roles, default: :user + t.datetime :enabled_at, :last_logon_at + t.timestamps + + t.index :email + end + end +end diff --git a/db/migrate/20240424093230_create_verification_codes.rb b/db/migrate/20240424093230_create_verification_codes.rb new file mode 100644 index 0000000..4681e98 --- /dev/null +++ b/db/migrate/20240424093230_create_verification_codes.rb @@ -0,0 +1,11 @@ +class CreateVerificationCodes < ActiveRecord::Migration[7.1] + def change + create_table :verification_codes do |t| + t.references :user + t.text :token + t.timestamps + + t.index :token + end + end +end diff --git a/db/migrate/20240424093500_create_nodes.rb b/db/migrate/20240424093500_create_nodes.rb new file mode 100644 index 0000000..dad19d0 --- /dev/null +++ b/db/migrate/20240424093500_create_nodes.rb @@ -0,0 +1,46 @@ +class CreateNodes < ActiveRecord::Migration[7.1] + def change + create_table :nodes do |t| + t.integer :position + t.integer "status", default: 0 + t.integer "template", default: 0 + + t.integer "ancestry_depth", default: 0 + t.string "ancestry", collation: 'C', null: false + + t.jsonb "slug", default: {} + t.jsonb "url", default: {} + t.jsonb "title", default: {} + t.jsonb "page_title", default: {} + t.jsonb "page_description", default: {} + t.jsonb "href", default: {} + t.jsonb "tags", default: {} + + t.text "settings", default: [], array: true + t.text "excluded_locales", default: [], array: true + + t.boolean "is_allowlist", default: false + + t.datetime "published_at" + t.datetime "expires_at" + + t.timestamps + + t.index "ancestry" + t.index "ancestry_depth" + t.index "position" + + t.index "slug", using: :gin + t.index "url", using: :gin + + t.index "tags", using: :gin + t.index "settings", using: :gin + + t.index "status" + t.index "template" + + t.index "excluded_locales", using: :gin + t.index "is_allowlist" + end + end +end diff --git a/db/migrate/20240424093536_create_attachments.rb b/db/migrate/20240424093536_create_attachments.rb new file mode 100644 index 0000000..0ee63ca --- /dev/null +++ b/db/migrate/20240424093536_create_attachments.rb @@ -0,0 +1,22 @@ +class CreateAttachments < ActiveRecord::Migration[7.1] + def change + create_table :attachments do |t| + + t.references 'asset' + + t.integer "attachable_for_id" + t.text "attachable_for_type" + + t.jsonb "body", default: {} + t.jsonb "url", default: {} + t.jsonb "props", default: {} + t.boolean "is_favorite", default: false, null: false + t.integer "position" + t.timestamps + + t.index ["attachable_for_id", "attachable_for_type"], name: "attachable_for" + t.index ["is_favorite"], name: "index_attachments_on_is_favorite" + + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 8077199..af05f02 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,10 +10,14 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_04_19_142316) do +ActiveRecord::Schema[7.1].define(version: 2024_04_24_093536) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + # Custom types defined in this database. + # Note that some types may not work with other database engines. Be careful if changing database. + create_enum "user_roles", ["user", "admin"] + create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -42,6 +46,86 @@ ActiveRecord::Schema[7.1].define(version: 2024_04_19_142316) do t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end + create_table "assets", force: :cascade do |t| + t.text "title" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "attachments", force: :cascade do |t| + t.bigint "asset_id" + t.integer "attachable_for_id" + t.text "attachable_for_type" + t.jsonb "body", default: {} + t.jsonb "url", default: {} + t.jsonb "props", default: {} + t.boolean "is_favorite", default: false, null: false + t.integer "position" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["asset_id"], name: "index_attachments_on_asset_id" + t.index ["attachable_for_id", "attachable_for_type"], name: "attachable_for" + t.index ["is_favorite"], name: "index_attachments_on_is_favorite" + end + + create_table "nodes", force: :cascade do |t| + t.integer "position" + t.integer "status", default: 0 + t.integer "template", default: 0 + t.integer "ancestry_depth", default: 0 + t.string "ancestry", null: false, collation: "C" + t.jsonb "slug", default: {} + t.jsonb "url", default: {} + t.jsonb "title", default: {} + t.jsonb "page_title", default: {} + t.jsonb "page_description", default: {} + t.jsonb "href", default: {} + t.jsonb "tags", default: {} + t.text "settings", default: [], array: true + t.text "excluded_locales", default: [], array: true + t.boolean "is_allowlist", default: false + t.datetime "published_at" + t.datetime "expires_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["ancestry"], name: "index_nodes_on_ancestry" + t.index ["ancestry_depth"], name: "index_nodes_on_ancestry_depth" + t.index ["excluded_locales"], name: "index_nodes_on_excluded_locales", using: :gin + t.index ["is_allowlist"], name: "index_nodes_on_is_allowlist" + t.index ["position"], name: "index_nodes_on_position" + t.index ["settings"], name: "index_nodes_on_settings", using: :gin + t.index ["slug"], name: "index_nodes_on_slug", using: :gin + t.index ["status"], name: "index_nodes_on_status" + t.index ["tags"], name: "index_nodes_on_tags", using: :gin + t.index ["template"], name: "index_nodes_on_template" + t.index ["url"], name: "index_nodes_on_url", using: :gin + end + + create_table "users", force: :cascade do |t| + t.text "email" + t.text "password_digest" + t.text "firstname" + t.text "lastname" + t.text "title" + t.text "phone" + t.text "description" + t.enum "role", default: "user", enum_type: "user_roles" + t.datetime "enabled_at" + t.datetime "last_logon_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["email"], name: "index_users_on_email" + end + + create_table "verification_codes", force: :cascade do |t| + t.bigint "user_id" + t.text "token" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["token"], name: "index_verification_codes_on_token" + t.index ["user_id"], name: "index_verification_codes_on_user_id" + end + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" end diff --git a/db/seeds.rb b/db/seeds.rb index 4fbd6ed..5ca5fe7 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,9 +1,14 @@ -# This file should ensure the existence of records required to run the application in every environment (production, -# development, test). The code here should be idempotent so that it can be executed at any point in every environment. -# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). -# -# Example: -# -# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| -# MovieGenre.find_or_create_by!(name: genre_name) -# end +User.create( + email: 'mattias@oncotype.dk', + firstname: 'Mattias', + lastname: 'Bodlund', + password: 'admin', + title: 'Webmaster', + phone: '28 58 18 13', + password_confirmation: 'admin', + enabled_at: Time.now, + role: 'admin' +) + + +Node.create title: 'Week 2024', slug: '', published_at: Time.now, ancestry: '/' diff --git a/test/controllers/admin/admin_controller_test.rb b/test/controllers/admin/admin_controller_test.rb new file mode 100644 index 0000000..46312fd --- /dev/null +++ b/test/controllers/admin/admin_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class Admin::AdminControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/admin/assets_controller_test.rb b/test/controllers/admin/assets_controller_test.rb new file mode 100644 index 0000000..e992ee8 --- /dev/null +++ b/test/controllers/admin/assets_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class Admin::AssetsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/admin/attachments_controller_test.rb b/test/controllers/admin/attachments_controller_test.rb new file mode 100644 index 0000000..bfdd319 --- /dev/null +++ b/test/controllers/admin/attachments_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class Admin::AttachmentsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/admin/nodes_controller_test.rb b/test/controllers/admin/nodes_controller_test.rb new file mode 100644 index 0000000..d249f72 --- /dev/null +++ b/test/controllers/admin/nodes_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class Admin::NodesControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/admin/sessions_controller_test.rb b/test/controllers/admin/sessions_controller_test.rb new file mode 100644 index 0000000..3bea734 --- /dev/null +++ b/test/controllers/admin/sessions_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class Admin::SessionsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/admin/users_controller_test.rb b/test/controllers/admin/users_controller_test.rb new file mode 100644 index 0000000..c34f22c --- /dev/null +++ b/test/controllers/admin/users_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class Admin::UsersControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/fixtures/assets.yml b/test/fixtures/assets.yml new file mode 100644 index 0000000..d7a3329 --- /dev/null +++ b/test/fixtures/assets.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/fixtures/attachments.yml b/test/fixtures/attachments.yml new file mode 100644 index 0000000..d7a3329 --- /dev/null +++ b/test/fixtures/attachments.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/fixtures/nodes.yml b/test/fixtures/nodes.yml new file mode 100644 index 0000000..d7a3329 --- /dev/null +++ b/test/fixtures/nodes.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml new file mode 100644 index 0000000..d7a3329 --- /dev/null +++ b/test/fixtures/users.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/fixtures/verification_codes.yml b/test/fixtures/verification_codes.yml new file mode 100644 index 0000000..d7a3329 --- /dev/null +++ b/test/fixtures/verification_codes.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/mailers/previews/user_mailer_preview.rb b/test/mailers/previews/user_mailer_preview.rb new file mode 100644 index 0000000..957e12b --- /dev/null +++ b/test/mailers/previews/user_mailer_preview.rb @@ -0,0 +1,4 @@ +# Preview all emails at http://localhost:3000/rails/mailers/user_mailer +class UserMailerPreview < ActionMailer::Preview + +end diff --git a/test/mailers/user_mailer_test.rb b/test/mailers/user_mailer_test.rb new file mode 100644 index 0000000..5f65b81 --- /dev/null +++ b/test/mailers/user_mailer_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class UserMailerTest < ActionMailer::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/asset_test.rb b/test/models/asset_test.rb new file mode 100644 index 0000000..593919b --- /dev/null +++ b/test/models/asset_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class AssetTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/attachment_test.rb b/test/models/attachment_test.rb new file mode 100644 index 0000000..034009a --- /dev/null +++ b/test/models/attachment_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class AttachmentTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/node_test.rb b/test/models/node_test.rb new file mode 100644 index 0000000..c03b029 --- /dev/null +++ b/test/models/node_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class NodeTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/user_test.rb b/test/models/user_test.rb new file mode 100644 index 0000000..5c07f49 --- /dev/null +++ b/test/models/user_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class UserTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/verification_code_test.rb b/test/models/verification_code_test.rb new file mode 100644 index 0000000..14bd972 --- /dev/null +++ b/test/models/verification_code_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class VerificationCodeTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/vendor/javascript/@hotwired--stimulus.js b/vendor/javascript/@hotwired--stimulus.js new file mode 100644 index 0000000..98f9eab --- /dev/null +++ b/vendor/javascript/@hotwired--stimulus.js @@ -0,0 +1,2 @@ +class EventListener{constructor(e,t,r){this.eventTarget=e;this.eventName=t;this.eventOptions=r;this.unorderedBindings=new Set}connect(){this.eventTarget.addEventListener(this.eventName,this,this.eventOptions)}disconnect(){this.eventTarget.removeEventListener(this.eventName,this,this.eventOptions)}bindingConnected(e){this.unorderedBindings.add(e)}bindingDisconnected(e){this.unorderedBindings.delete(e)}handleEvent(e){const t=extendEvent(e);for(const e of this.bindings){if(t.immediatePropagationStopped)break;e.handleEvent(t)}}hasBindings(){return this.unorderedBindings.size>0}get bindings(){return Array.from(this.unorderedBindings).sort(((e,t)=>{const r=e.index,s=t.index;return rs?1:0}))}}function extendEvent(e){if("immediatePropagationStopped"in e)return e;{const{stopImmediatePropagation:t}=e;return Object.assign(e,{immediatePropagationStopped:false,stopImmediatePropagation(){this.immediatePropagationStopped=true;t.call(this)}})}}class Dispatcher{constructor(e){this.application=e;this.eventListenerMaps=new Map;this.started=false}start(){if(!this.started){this.started=true;this.eventListeners.forEach((e=>e.connect()))}}stop(){if(this.started){this.started=false;this.eventListeners.forEach((e=>e.disconnect()))}}get eventListeners(){return Array.from(this.eventListenerMaps.values()).reduce(((e,t)=>e.concat(Array.from(t.values()))),[])}bindingConnected(e){this.fetchEventListenerForBinding(e).bindingConnected(e)}bindingDisconnected(e,t=false){this.fetchEventListenerForBinding(e).bindingDisconnected(e);t&&this.clearEventListenersForBinding(e)}handleError(e,t,r={}){this.application.handleError(e,`Error ${t}`,r)}clearEventListenersForBinding(e){const t=this.fetchEventListenerForBinding(e);if(!t.hasBindings()){t.disconnect();this.removeMappedEventListenerFor(e)}}removeMappedEventListenerFor(e){const{eventTarget:t,eventName:r,eventOptions:s}=e;const n=this.fetchEventListenerMapForEventTarget(t);const i=this.cacheKey(r,s);n.delete(i);0==n.size&&this.eventListenerMaps.delete(t)}fetchEventListenerForBinding(e){const{eventTarget:t,eventName:r,eventOptions:s}=e;return this.fetchEventListener(t,r,s)}fetchEventListener(e,t,r){const s=this.fetchEventListenerMapForEventTarget(e);const n=this.cacheKey(t,r);let i=s.get(n);if(!i){i=this.createEventListener(e,t,r);s.set(n,i)}return i}createEventListener(e,t,r){const s=new EventListener(e,t,r);this.started&&s.connect();return s}fetchEventListenerMapForEventTarget(e){let t=this.eventListenerMaps.get(e);if(!t){t=new Map;this.eventListenerMaps.set(e,t)}return t}cacheKey(e,t){const r=[e];Object.keys(t).sort().forEach((e=>{r.push(`${t[e]?"":"!"}${e}`)}));return r.join(":")}}const e={stop({event:e,value:t}){t&&e.stopPropagation();return true},prevent({event:e,value:t}){t&&e.preventDefault();return true},self({event:e,value:t,element:r}){return!t||r===e.target}};const t=/^(?:(?:([^.]+?)\+)?(.+?)(?:\.(.+?))?(?:@(window|document))?->)?(.+?)(?:#([^:]+?))(?::(.+))?$/;function parseActionDescriptorString(e){const r=e.trim();const s=r.match(t)||[];let n=s[2];let i=s[3];if(i&&!["keydown","keyup","keypress"].includes(n)){n+=`.${i}`;i=""}return{eventTarget:parseEventTarget(s[4]),eventName:n,eventOptions:s[7]?parseEventOptions(s[7]):{},identifier:s[5],methodName:s[6],keyFilter:s[1]||i}}function parseEventTarget(e){return"window"==e?window:"document"==e?document:void 0}function parseEventOptions(e){return e.split(":").reduce(((e,t)=>Object.assign(e,{[t.replace(/^!/,"")]:!/^!/.test(t)})),{})}function stringifyEventTarget(e){return e==window?"window":e==document?"document":void 0}function camelize(e){return e.replace(/(?:[_-])([a-z0-9])/g,((e,t)=>t.toUpperCase()))}function namespaceCamelize(e){return camelize(e.replace(/--/g,"-").replace(/__/g,"_"))}function capitalize(e){return e.charAt(0).toUpperCase()+e.slice(1)}function dasherize(e){return e.replace(/([A-Z])/g,((e,t)=>`-${t.toLowerCase()}`))}function tokenize(e){return e.match(/[^\s]+/g)||[]}function isSomething(e){return null!==e&&void 0!==e}function hasProperty(e,t){return Object.prototype.hasOwnProperty.call(e,t)}const r=["meta","ctrl","alt","shift"];class Action{constructor(e,t,r,s){this.element=e;this.index=t;this.eventTarget=r.eventTarget||e;this.eventName=r.eventName||getDefaultEventNameForElement(e)||error("missing event name");this.eventOptions=r.eventOptions||{};this.identifier=r.identifier||error("missing identifier");this.methodName=r.methodName||error("missing method name");this.keyFilter=r.keyFilter||"";this.schema=s}static forToken(e,t){return new this(e.element,e.index,parseActionDescriptorString(e.content),t)}toString(){const e=this.keyFilter?`.${this.keyFilter}`:"";const t=this.eventTargetName?`@${this.eventTargetName}`:"";return`${this.eventName}${e}${t}->${this.identifier}#${this.methodName}`}shouldIgnoreKeyboardEvent(e){if(!this.keyFilter)return false;const t=this.keyFilter.split("+");if(this.keyFilterDissatisfied(e,t))return true;const s=t.filter((e=>!r.includes(e)))[0];if(!s)return false;hasProperty(this.keyMappings,s)||error(`contains unknown key filter: ${this.keyFilter}`);return this.keyMappings[s].toLowerCase()!==e.key.toLowerCase()}shouldIgnoreMouseEvent(e){if(!this.keyFilter)return false;const t=[this.keyFilter];return!!this.keyFilterDissatisfied(e,t)}get params(){const e={};const t=new RegExp(`^data-${this.identifier}-(.+)-param$`,"i");for(const{name:r,value:s}of Array.from(this.element.attributes)){const n=r.match(t);const i=n&&n[1];i&&(e[camelize(i)]=typecast(s))}return e}get eventTargetName(){return stringifyEventTarget(this.eventTarget)}get keyMappings(){return this.schema.keyMappings}keyFilterDissatisfied(e,t){const[s,n,i,o]=r.map((e=>t.includes(e)));return e.metaKey!==s||e.ctrlKey!==n||e.altKey!==i||e.shiftKey!==o}}const s={a:()=>"click",button:()=>"click",form:()=>"submit",details:()=>"toggle",input:e=>"submit"==e.getAttribute("type")?"click":"input",select:()=>"change",textarea:()=>"input"};function getDefaultEventNameForElement(e){const t=e.tagName.toLowerCase();if(t in s)return s[t](e)}function error(e){throw new Error(e)}function typecast(e){try{return JSON.parse(e)}catch(t){return e}}class Binding{constructor(e,t){this.context=e;this.action=t}get index(){return this.action.index}get eventTarget(){return this.action.eventTarget}get eventOptions(){return this.action.eventOptions}get identifier(){return this.context.identifier}handleEvent(e){const t=this.prepareActionEvent(e);this.willBeInvokedByEvent(e)&&this.applyEventModifiers(t)&&this.invokeWithEvent(t)}get eventName(){return this.action.eventName}get method(){const e=this.controller[this.methodName];if("function"==typeof e)return e;throw new Error(`Action "${this.action}" references undefined method "${this.methodName}"`)}applyEventModifiers(e){const{element:t}=this.action;const{actionDescriptorFilters:r}=this.context.application;const{controller:s}=this.context;let n=true;for(const[i,o]of Object.entries(this.eventOptions))if(i in r){const c=r[i];n=n&&c({name:i,value:o,event:e,element:t,controller:s})}return n}prepareActionEvent(e){return Object.assign(e,{params:this.action.params})}invokeWithEvent(e){const{target:t,currentTarget:r}=e;try{this.method.call(this.controller,e);this.context.logDebugActivity(this.methodName,{event:e,target:t,currentTarget:r,action:this.methodName})}catch(t){const{identifier:r,controller:s,element:n,index:i}=this;const o={identifier:r,controller:s,element:n,index:i,event:e};this.context.handleError(t,`invoking action "${this.action}"`,o)}}willBeInvokedByEvent(e){const t=e.target;return!(e instanceof KeyboardEvent&&this.action.shouldIgnoreKeyboardEvent(e))&&(!(e instanceof MouseEvent&&this.action.shouldIgnoreMouseEvent(e))&&(this.element===t||(t instanceof Element&&this.element.contains(t)?this.scope.containsElement(t):this.scope.containsElement(this.action.element))))}get controller(){return this.context.controller}get methodName(){return this.action.methodName}get element(){return this.scope.element}get scope(){return this.context.scope}}class ElementObserver{constructor(e,t){this.mutationObserverInit={attributes:true,childList:true,subtree:true};this.element=e;this.started=false;this.delegate=t;this.elements=new Set;this.mutationObserver=new MutationObserver((e=>this.processMutations(e)))}start(){if(!this.started){this.started=true;this.mutationObserver.observe(this.element,this.mutationObserverInit);this.refresh()}}pause(e){if(this.started){this.mutationObserver.disconnect();this.started=false}e();if(!this.started){this.mutationObserver.observe(this.element,this.mutationObserverInit);this.started=true}}stop(){if(this.started){this.mutationObserver.takeRecords();this.mutationObserver.disconnect();this.started=false}}refresh(){if(this.started){const e=new Set(this.matchElementsInTree());for(const t of Array.from(this.elements))e.has(t)||this.removeElement(t);for(const t of Array.from(e))this.addElement(t)}}processMutations(e){if(this.started)for(const t of e)this.processMutation(t)}processMutation(e){if("attributes"==e.type)this.processAttributeChange(e.target,e.attributeName);else if("childList"==e.type){this.processRemovedNodes(e.removedNodes);this.processAddedNodes(e.addedNodes)}}processAttributeChange(e,t){this.elements.has(e)?this.delegate.elementAttributeChanged&&this.matchElement(e)?this.delegate.elementAttributeChanged(e,t):this.removeElement(e):this.matchElement(e)&&this.addElement(e)}processRemovedNodes(e){for(const t of Array.from(e)){const e=this.elementFromNode(t);e&&this.processTree(e,this.removeElement)}}processAddedNodes(e){for(const t of Array.from(e)){const e=this.elementFromNode(t);e&&this.elementIsActive(e)&&this.processTree(e,this.addElement)}}matchElement(e){return this.delegate.matchElement(e)}matchElementsInTree(e=this.element){return this.delegate.matchElementsInTree(e)}processTree(e,t){for(const r of this.matchElementsInTree(e))t.call(this,r)}elementFromNode(e){if(e.nodeType==Node.ELEMENT_NODE)return e}elementIsActive(e){return e.isConnected==this.element.isConnected&&this.element.contains(e)}addElement(e){if(!this.elements.has(e)&&this.elementIsActive(e)){this.elements.add(e);this.delegate.elementMatched&&this.delegate.elementMatched(e)}}removeElement(e){if(this.elements.has(e)){this.elements.delete(e);this.delegate.elementUnmatched&&this.delegate.elementUnmatched(e)}}}class AttributeObserver{constructor(e,t,r){this.attributeName=t;this.delegate=r;this.elementObserver=new ElementObserver(e,this)}get element(){return this.elementObserver.element}get selector(){return`[${this.attributeName}]`}start(){this.elementObserver.start()}pause(e){this.elementObserver.pause(e)}stop(){this.elementObserver.stop()}refresh(){this.elementObserver.refresh()}get started(){return this.elementObserver.started}matchElement(e){return e.hasAttribute(this.attributeName)}matchElementsInTree(e){const t=this.matchElement(e)?[e]:[];const r=Array.from(e.querySelectorAll(this.selector));return t.concat(r)}elementMatched(e){this.delegate.elementMatchedAttribute&&this.delegate.elementMatchedAttribute(e,this.attributeName)}elementUnmatched(e){this.delegate.elementUnmatchedAttribute&&this.delegate.elementUnmatchedAttribute(e,this.attributeName)}elementAttributeChanged(e,t){this.delegate.elementAttributeValueChanged&&this.attributeName==t&&this.delegate.elementAttributeValueChanged(e,t)}}function add(e,t,r){fetch(e,t).add(r)}function del(e,t,r){fetch(e,t).delete(r);prune(e,t)}function fetch(e,t){let r=e.get(t);if(!r){r=new Set;e.set(t,r)}return r}function prune(e,t){const r=e.get(t);null!=r&&0==r.size&&e.delete(t)}class Multimap{constructor(){this.valuesByKey=new Map}get keys(){return Array.from(this.valuesByKey.keys())}get values(){const e=Array.from(this.valuesByKey.values());return e.reduce(((e,t)=>e.concat(Array.from(t))),[])}get size(){const e=Array.from(this.valuesByKey.values());return e.reduce(((e,t)=>e+t.size),0)}add(e,t){add(this.valuesByKey,e,t)}delete(e,t){del(this.valuesByKey,e,t)}has(e,t){const r=this.valuesByKey.get(e);return null!=r&&r.has(t)}hasKey(e){return this.valuesByKey.has(e)}hasValue(e){const t=Array.from(this.valuesByKey.values());return t.some((t=>t.has(e)))}getValuesForKey(e){const t=this.valuesByKey.get(e);return t?Array.from(t):[]}getKeysForValue(e){return Array.from(this.valuesByKey).filter((([t,r])=>r.has(e))).map((([e,t])=>e))}}class IndexedMultimap extends Multimap{constructor(){super();this.keysByValue=new Map}get values(){return Array.from(this.keysByValue.keys())}add(e,t){super.add(e,t);add(this.keysByValue,t,e)}delete(e,t){super.delete(e,t);del(this.keysByValue,t,e)}hasValue(e){return this.keysByValue.has(e)}getKeysForValue(e){const t=this.keysByValue.get(e);return t?Array.from(t):[]}}class SelectorObserver{constructor(e,t,r,s){this._selector=t;this.details=s;this.elementObserver=new ElementObserver(e,this);this.delegate=r;this.matchesByElement=new Multimap}get started(){return this.elementObserver.started}get selector(){return this._selector}set selector(e){this._selector=e;this.refresh()}start(){this.elementObserver.start()}pause(e){this.elementObserver.pause(e)}stop(){this.elementObserver.stop()}refresh(){this.elementObserver.refresh()}get element(){return this.elementObserver.element}matchElement(e){const{selector:t}=this;if(t){const r=e.matches(t);return this.delegate.selectorMatchElement?r&&this.delegate.selectorMatchElement(e,this.details):r}return false}matchElementsInTree(e){const{selector:t}=this;if(t){const r=this.matchElement(e)?[e]:[];const s=Array.from(e.querySelectorAll(t)).filter((e=>this.matchElement(e)));return r.concat(s)}return[]}elementMatched(e){const{selector:t}=this;t&&this.selectorMatched(e,t)}elementUnmatched(e){const t=this.matchesByElement.getKeysForValue(e);for(const r of t)this.selectorUnmatched(e,r)}elementAttributeChanged(e,t){const{selector:r}=this;if(r){const t=this.matchElement(e);const s=this.matchesByElement.has(r,e);t&&!s?this.selectorMatched(e,r):!t&&s&&this.selectorUnmatched(e,r)}}selectorMatched(e,t){this.delegate.selectorMatched(e,t,this.details);this.matchesByElement.add(t,e)}selectorUnmatched(e,t){this.delegate.selectorUnmatched(e,t,this.details);this.matchesByElement.delete(t,e)}}class StringMapObserver{constructor(e,t){this.element=e;this.delegate=t;this.started=false;this.stringMap=new Map;this.mutationObserver=new MutationObserver((e=>this.processMutations(e)))}start(){if(!this.started){this.started=true;this.mutationObserver.observe(this.element,{attributes:true,attributeOldValue:true});this.refresh()}}stop(){if(this.started){this.mutationObserver.takeRecords();this.mutationObserver.disconnect();this.started=false}}refresh(){if(this.started)for(const e of this.knownAttributeNames)this.refreshAttribute(e,null)}processMutations(e){if(this.started)for(const t of e)this.processMutation(t)}processMutation(e){const t=e.attributeName;t&&this.refreshAttribute(t,e.oldValue)}refreshAttribute(e,t){const r=this.delegate.getStringMapKeyForAttribute(e);if(null!=r){this.stringMap.has(e)||this.stringMapKeyAdded(r,e);const s=this.element.getAttribute(e);this.stringMap.get(e)!=s&&this.stringMapValueChanged(s,r,t);if(null==s){const t=this.stringMap.get(e);this.stringMap.delete(e);t&&this.stringMapKeyRemoved(r,e,t)}else this.stringMap.set(e,s)}}stringMapKeyAdded(e,t){this.delegate.stringMapKeyAdded&&this.delegate.stringMapKeyAdded(e,t)}stringMapValueChanged(e,t,r){this.delegate.stringMapValueChanged&&this.delegate.stringMapValueChanged(e,t,r)}stringMapKeyRemoved(e,t,r){this.delegate.stringMapKeyRemoved&&this.delegate.stringMapKeyRemoved(e,t,r)}get knownAttributeNames(){return Array.from(new Set(this.currentAttributeNames.concat(this.recordedAttributeNames)))}get currentAttributeNames(){return Array.from(this.element.attributes).map((e=>e.name))}get recordedAttributeNames(){return Array.from(this.stringMap.keys())}}class TokenListObserver{constructor(e,t,r){this.attributeObserver=new AttributeObserver(e,t,this);this.delegate=r;this.tokensByElement=new Multimap}get started(){return this.attributeObserver.started}start(){this.attributeObserver.start()}pause(e){this.attributeObserver.pause(e)}stop(){this.attributeObserver.stop()}refresh(){this.attributeObserver.refresh()}get element(){return this.attributeObserver.element}get attributeName(){return this.attributeObserver.attributeName}elementMatchedAttribute(e){this.tokensMatched(this.readTokensForElement(e))}elementAttributeValueChanged(e){const[t,r]=this.refreshTokensForElement(e);this.tokensUnmatched(t);this.tokensMatched(r)}elementUnmatchedAttribute(e){this.tokensUnmatched(this.tokensByElement.getValuesForKey(e))}tokensMatched(e){e.forEach((e=>this.tokenMatched(e)))}tokensUnmatched(e){e.forEach((e=>this.tokenUnmatched(e)))}tokenMatched(e){this.delegate.tokenMatched(e);this.tokensByElement.add(e.element,e)}tokenUnmatched(e){this.delegate.tokenUnmatched(e);this.tokensByElement.delete(e.element,e)}refreshTokensForElement(e){const t=this.tokensByElement.getValuesForKey(e);const r=this.readTokensForElement(e);const s=zip(t,r).findIndex((([e,t])=>!tokensAreEqual(e,t)));return-1==s?[[],[]]:[t.slice(s),r.slice(s)]}readTokensForElement(e){const t=this.attributeName;const r=e.getAttribute(t)||"";return parseTokenString(r,e,t)}}function parseTokenString(e,t,r){return e.trim().split(/\s+/).filter((e=>e.length)).map(((e,s)=>({element:t,attributeName:r,content:e,index:s})))}function zip(e,t){const r=Math.max(e.length,t.length);return Array.from({length:r},((r,s)=>[e[s],t[s]]))}function tokensAreEqual(e,t){return e&&t&&e.index==t.index&&e.content==t.content}class ValueListObserver{constructor(e,t,r){this.tokenListObserver=new TokenListObserver(e,t,this);this.delegate=r;this.parseResultsByToken=new WeakMap;this.valuesByTokenByElement=new WeakMap}get started(){return this.tokenListObserver.started}start(){this.tokenListObserver.start()}stop(){this.tokenListObserver.stop()}refresh(){this.tokenListObserver.refresh()}get element(){return this.tokenListObserver.element}get attributeName(){return this.tokenListObserver.attributeName}tokenMatched(e){const{element:t}=e;const{value:r}=this.fetchParseResultForToken(e);if(r){this.fetchValuesByTokenForElement(t).set(e,r);this.delegate.elementMatchedValue(t,r)}}tokenUnmatched(e){const{element:t}=e;const{value:r}=this.fetchParseResultForToken(e);if(r){this.fetchValuesByTokenForElement(t).delete(e);this.delegate.elementUnmatchedValue(t,r)}}fetchParseResultForToken(e){let t=this.parseResultsByToken.get(e);if(!t){t=this.parseToken(e);this.parseResultsByToken.set(e,t)}return t}fetchValuesByTokenForElement(e){let t=this.valuesByTokenByElement.get(e);if(!t){t=new Map;this.valuesByTokenByElement.set(e,t)}return t}parseToken(e){try{const t=this.delegate.parseValueForToken(e);return{value:t}}catch(e){return{error:e}}}}class BindingObserver{constructor(e,t){this.context=e;this.delegate=t;this.bindingsByAction=new Map}start(){if(!this.valueListObserver){this.valueListObserver=new ValueListObserver(this.element,this.actionAttribute,this);this.valueListObserver.start()}}stop(){if(this.valueListObserver){this.valueListObserver.stop();delete this.valueListObserver;this.disconnectAllActions()}}get element(){return this.context.element}get identifier(){return this.context.identifier}get actionAttribute(){return this.schema.actionAttribute}get schema(){return this.context.schema}get bindings(){return Array.from(this.bindingsByAction.values())}connectAction(e){const t=new Binding(this.context,e);this.bindingsByAction.set(e,t);this.delegate.bindingConnected(t)}disconnectAction(e){const t=this.bindingsByAction.get(e);if(t){this.bindingsByAction.delete(e);this.delegate.bindingDisconnected(t)}}disconnectAllActions(){this.bindings.forEach((e=>this.delegate.bindingDisconnected(e,true)));this.bindingsByAction.clear()}parseValueForToken(e){const t=Action.forToken(e,this.schema);if(t.identifier==this.identifier)return t}elementMatchedValue(e,t){this.connectAction(t)}elementUnmatchedValue(e,t){this.disconnectAction(t)}}class ValueObserver{constructor(e,t){this.context=e;this.receiver=t;this.stringMapObserver=new StringMapObserver(this.element,this);this.valueDescriptorMap=this.controller.valueDescriptorMap}start(){this.stringMapObserver.start();this.invokeChangedCallbacksForDefaultValues()}stop(){this.stringMapObserver.stop()}get element(){return this.context.element}get controller(){return this.context.controller}getStringMapKeyForAttribute(e){if(e in this.valueDescriptorMap)return this.valueDescriptorMap[e].name}stringMapKeyAdded(e,t){const r=this.valueDescriptorMap[t];this.hasValue(e)||this.invokeChangedCallback(e,r.writer(this.receiver[e]),r.writer(r.defaultValue))}stringMapValueChanged(e,t,r){const s=this.valueDescriptorNameMap[t];if(null!==e){null===r&&(r=s.writer(s.defaultValue));this.invokeChangedCallback(t,e,r)}}stringMapKeyRemoved(e,t,r){const s=this.valueDescriptorNameMap[e];this.hasValue(e)?this.invokeChangedCallback(e,s.writer(this.receiver[e]),r):this.invokeChangedCallback(e,s.writer(s.defaultValue),r)}invokeChangedCallbacksForDefaultValues(){for(const{key:e,name:t,defaultValue:r,writer:s}of this.valueDescriptors)void 0==r||this.controller.data.has(e)||this.invokeChangedCallback(t,s(r),void 0)}invokeChangedCallback(e,t,r){const s=`${e}Changed`;const n=this.receiver[s];if("function"==typeof n){const s=this.valueDescriptorNameMap[e];try{const e=s.reader(t);let i=r;r&&(i=s.reader(r));n.call(this.receiver,e,i)}catch(e){e instanceof TypeError&&(e.message=`Stimulus Value "${this.context.identifier}.${s.name}" - ${e.message}`);throw e}}}get valueDescriptors(){const{valueDescriptorMap:e}=this;return Object.keys(e).map((t=>e[t]))}get valueDescriptorNameMap(){const e={};Object.keys(this.valueDescriptorMap).forEach((t=>{const r=this.valueDescriptorMap[t];e[r.name]=r}));return e}hasValue(e){const t=this.valueDescriptorNameMap[e];const r=`has${capitalize(t.name)}`;return this.receiver[r]}}class TargetObserver{constructor(e,t){this.context=e;this.delegate=t;this.targetsByName=new Multimap}start(){if(!this.tokenListObserver){this.tokenListObserver=new TokenListObserver(this.element,this.attributeName,this);this.tokenListObserver.start()}}stop(){if(this.tokenListObserver){this.disconnectAllTargets();this.tokenListObserver.stop();delete this.tokenListObserver}}tokenMatched({element:e,content:t}){this.scope.containsElement(e)&&this.connectTarget(e,t)}tokenUnmatched({element:e,content:t}){this.disconnectTarget(e,t)}connectTarget(e,t){var r;if(!this.targetsByName.has(t,e)){this.targetsByName.add(t,e);null===(r=this.tokenListObserver)||void 0===r?void 0:r.pause((()=>this.delegate.targetConnected(e,t)))}}disconnectTarget(e,t){var r;if(this.targetsByName.has(t,e)){this.targetsByName.delete(t,e);null===(r=this.tokenListObserver)||void 0===r?void 0:r.pause((()=>this.delegate.targetDisconnected(e,t)))}}disconnectAllTargets(){for(const e of this.targetsByName.keys)for(const t of this.targetsByName.getValuesForKey(e))this.disconnectTarget(t,e)}get attributeName(){return`data-${this.context.identifier}-target`}get element(){return this.context.element}get scope(){return this.context.scope}}function readInheritableStaticArrayValues(e,t){const r=getAncestorsForConstructor(e);return Array.from(r.reduce(((e,r)=>{getOwnStaticArrayValues(r,t).forEach((t=>e.add(t)));return e}),new Set))}function readInheritableStaticObjectPairs(e,t){const r=getAncestorsForConstructor(e);return r.reduce(((e,r)=>{e.push(...getOwnStaticObjectPairs(r,t));return e}),[])}function getAncestorsForConstructor(e){const t=[];while(e){t.push(e);e=Object.getPrototypeOf(e)}return t.reverse()}function getOwnStaticArrayValues(e,t){const r=e[t];return Array.isArray(r)?r:[]}function getOwnStaticObjectPairs(e,t){const r=e[t];return r?Object.keys(r).map((e=>[e,r[e]])):[]}class OutletObserver{constructor(e,t){this.started=false;this.context=e;this.delegate=t;this.outletsByName=new Multimap;this.outletElementsByName=new Multimap;this.selectorObserverMap=new Map;this.attributeObserverMap=new Map}start(){if(!this.started){this.outletDefinitions.forEach((e=>{this.setupSelectorObserverForOutlet(e);this.setupAttributeObserverForOutlet(e)}));this.started=true;this.dependentContexts.forEach((e=>e.refresh()))}}refresh(){this.selectorObserverMap.forEach((e=>e.refresh()));this.attributeObserverMap.forEach((e=>e.refresh()))}stop(){if(this.started){this.started=false;this.disconnectAllOutlets();this.stopSelectorObservers();this.stopAttributeObservers()}}stopSelectorObservers(){if(this.selectorObserverMap.size>0){this.selectorObserverMap.forEach((e=>e.stop()));this.selectorObserverMap.clear()}}stopAttributeObservers(){if(this.attributeObserverMap.size>0){this.attributeObserverMap.forEach((e=>e.stop()));this.attributeObserverMap.clear()}}selectorMatched(e,t,{outletName:r}){const s=this.getOutlet(e,r);s&&this.connectOutlet(s,e,r)}selectorUnmatched(e,t,{outletName:r}){const s=this.getOutletFromMap(e,r);s&&this.disconnectOutlet(s,e,r)}selectorMatchElement(e,{outletName:t}){const r=this.selector(t);const s=this.hasOutlet(e,t);const n=e.matches(`[${this.schema.controllerAttribute}~=${t}]`);return!!r&&(s&&n&&e.matches(r))}elementMatchedAttribute(e,t){const r=this.getOutletNameFromOutletAttributeName(t);r&&this.updateSelectorObserverForOutlet(r)}elementAttributeValueChanged(e,t){const r=this.getOutletNameFromOutletAttributeName(t);r&&this.updateSelectorObserverForOutlet(r)}elementUnmatchedAttribute(e,t){const r=this.getOutletNameFromOutletAttributeName(t);r&&this.updateSelectorObserverForOutlet(r)}connectOutlet(e,t,r){var s;if(!this.outletElementsByName.has(r,t)){this.outletsByName.add(r,e);this.outletElementsByName.add(r,t);null===(s=this.selectorObserverMap.get(r))||void 0===s?void 0:s.pause((()=>this.delegate.outletConnected(e,t,r)))}}disconnectOutlet(e,t,r){var s;if(this.outletElementsByName.has(r,t)){this.outletsByName.delete(r,e);this.outletElementsByName.delete(r,t);null===(s=this.selectorObserverMap.get(r))||void 0===s?void 0:s.pause((()=>this.delegate.outletDisconnected(e,t,r)))}}disconnectAllOutlets(){for(const e of this.outletElementsByName.keys)for(const t of this.outletElementsByName.getValuesForKey(e))for(const r of this.outletsByName.getValuesForKey(e))this.disconnectOutlet(r,t,e)}updateSelectorObserverForOutlet(e){const t=this.selectorObserverMap.get(e);t&&(t.selector=this.selector(e))}setupSelectorObserverForOutlet(e){const t=this.selector(e);const r=new SelectorObserver(document.body,t,this,{outletName:e});this.selectorObserverMap.set(e,r);r.start()}setupAttributeObserverForOutlet(e){const t=this.attributeNameForOutletName(e);const r=new AttributeObserver(this.scope.element,t,this);this.attributeObserverMap.set(e,r);r.start()}selector(e){return this.scope.outlets.getSelectorForOutletName(e)}attributeNameForOutletName(e){return this.scope.schema.outletAttributeForScope(this.identifier,e)}getOutletNameFromOutletAttributeName(e){return this.outletDefinitions.find((t=>this.attributeNameForOutletName(t)===e))}get outletDependencies(){const e=new Multimap;this.router.modules.forEach((t=>{const r=t.definition.controllerConstructor;const s=readInheritableStaticArrayValues(r,"outlets");s.forEach((r=>e.add(r,t.identifier)))}));return e}get outletDefinitions(){return this.outletDependencies.getKeysForValue(this.identifier)}get dependentControllerIdentifiers(){return this.outletDependencies.getValuesForKey(this.identifier)}get dependentContexts(){const e=this.dependentControllerIdentifiers;return this.router.contexts.filter((t=>e.includes(t.identifier)))}hasOutlet(e,t){return!!this.getOutlet(e,t)||!!this.getOutletFromMap(e,t)}getOutlet(e,t){return this.application.getControllerForElementAndIdentifier(e,t)}getOutletFromMap(e,t){return this.outletsByName.getValuesForKey(t).find((t=>t.element===e))}get scope(){return this.context.scope}get schema(){return this.context.schema}get identifier(){return this.context.identifier}get application(){return this.context.application}get router(){return this.application.router}}class Context{constructor(e,t){this.logDebugActivity=(e,t={})=>{const{identifier:r,controller:s,element:n}=this;t=Object.assign({identifier:r,controller:s,element:n},t);this.application.logDebugActivity(this.identifier,e,t)};this.module=e;this.scope=t;this.controller=new e.controllerConstructor(this);this.bindingObserver=new BindingObserver(this,this.dispatcher);this.valueObserver=new ValueObserver(this,this.controller);this.targetObserver=new TargetObserver(this,this);this.outletObserver=new OutletObserver(this,this);try{this.controller.initialize();this.logDebugActivity("initialize")}catch(e){this.handleError(e,"initializing controller")}}connect(){this.bindingObserver.start();this.valueObserver.start();this.targetObserver.start();this.outletObserver.start();try{this.controller.connect();this.logDebugActivity("connect")}catch(e){this.handleError(e,"connecting controller")}}refresh(){this.outletObserver.refresh()}disconnect(){try{this.controller.disconnect();this.logDebugActivity("disconnect")}catch(e){this.handleError(e,"disconnecting controller")}this.outletObserver.stop();this.targetObserver.stop();this.valueObserver.stop();this.bindingObserver.stop()}get application(){return this.module.application}get identifier(){return this.module.identifier}get schema(){return this.application.schema}get dispatcher(){return this.application.dispatcher}get element(){return this.scope.element}get parentElement(){return this.element.parentElement}handleError(e,t,r={}){const{identifier:s,controller:n,element:i}=this;r=Object.assign({identifier:s,controller:n,element:i},r);this.application.handleError(e,`Error ${t}`,r)}targetConnected(e,t){this.invokeControllerMethod(`${t}TargetConnected`,e)}targetDisconnected(e,t){this.invokeControllerMethod(`${t}TargetDisconnected`,e)}outletConnected(e,t,r){this.invokeControllerMethod(`${namespaceCamelize(r)}OutletConnected`,e,t)}outletDisconnected(e,t,r){this.invokeControllerMethod(`${namespaceCamelize(r)}OutletDisconnected`,e,t)}invokeControllerMethod(e,...t){const r=this.controller;"function"==typeof r[e]&&r[e](...t)}}function bless(e){return shadow(e,getBlessedProperties(e))}function shadow(e,t){const r=i(e);const s=getShadowProperties(e.prototype,t);Object.defineProperties(r.prototype,s);return r}function getBlessedProperties(e){const t=readInheritableStaticArrayValues(e,"blessings");return t.reduce(((t,r)=>{const s=r(e);for(const e in s){const r=t[e]||{};t[e]=Object.assign(r,s[e])}return t}),{})}function getShadowProperties(e,t){return n(t).reduce(((r,s)=>{const n=getShadowedDescriptor(e,t,s);n&&Object.assign(r,{[s]:n});return r}),{})}function getShadowedDescriptor(e,t,r){const s=Object.getOwnPropertyDescriptor(e,r);const n=s&&"value"in s;if(!n){const e=Object.getOwnPropertyDescriptor(t,r).value;if(s){e.get=s.get||e.get;e.set=s.set||e.set}return e}}const n=(()=>"function"==typeof Object.getOwnPropertySymbols?e=>[...Object.getOwnPropertyNames(e),...Object.getOwnPropertySymbols(e)]:Object.getOwnPropertyNames)();const i=(()=>{function extendWithReflect(e){function extended(){return Reflect.construct(e,arguments,new.target)}extended.prototype=Object.create(e.prototype,{constructor:{value:extended}});Reflect.setPrototypeOf(extended,e);return extended}function testReflectExtension(){const a=function(){this.a.call(this)};const e=extendWithReflect(a);e.prototype.a=function(){};return new e}try{testReflectExtension();return extendWithReflect}catch(e){return e=>class extended extends e{}}})();function blessDefinition(e){return{identifier:e.identifier,controllerConstructor:bless(e.controllerConstructor)}}class Module{constructor(e,t){this.application=e;this.definition=blessDefinition(t);this.contextsByScope=new WeakMap;this.connectedContexts=new Set}get identifier(){return this.definition.identifier}get controllerConstructor(){return this.definition.controllerConstructor}get contexts(){return Array.from(this.connectedContexts)}connectContextForScope(e){const t=this.fetchContextForScope(e);this.connectedContexts.add(t);t.connect()}disconnectContextForScope(e){const t=this.contextsByScope.get(e);if(t){this.connectedContexts.delete(t);t.disconnect()}}fetchContextForScope(e){let t=this.contextsByScope.get(e);if(!t){t=new Context(this,e);this.contextsByScope.set(e,t)}return t}}class ClassMap{constructor(e){this.scope=e}has(e){return this.data.has(this.getDataKey(e))}get(e){return this.getAll(e)[0]}getAll(e){const t=this.data.get(this.getDataKey(e))||"";return tokenize(t)}getAttributeName(e){return this.data.getAttributeNameForKey(this.getDataKey(e))}getDataKey(e){return`${e}-class`}get data(){return this.scope.data}}class DataMap{constructor(e){this.scope=e}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get(e){const t=this.getAttributeNameForKey(e);return this.element.getAttribute(t)}set(e,t){const r=this.getAttributeNameForKey(e);this.element.setAttribute(r,t);return this.get(e)}has(e){const t=this.getAttributeNameForKey(e);return this.element.hasAttribute(t)}delete(e){if(this.has(e)){const t=this.getAttributeNameForKey(e);this.element.removeAttribute(t);return true}return false}getAttributeNameForKey(e){return`data-${this.identifier}-${dasherize(e)}`}}class Guide{constructor(e){this.warnedKeysByObject=new WeakMap;this.logger=e}warn(e,t,r){let s=this.warnedKeysByObject.get(e);if(!s){s=new Set;this.warnedKeysByObject.set(e,s)}if(!s.has(t)){s.add(t);this.logger.warn(r,e)}}}function attributeValueContainsToken(e,t){return`[${e}~="${t}"]`}class TargetSet{constructor(e){this.scope=e}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get schema(){return this.scope.schema}has(e){return null!=this.find(e)}find(...e){return e.reduce(((e,t)=>e||this.findTarget(t)||this.findLegacyTarget(t)),void 0)}findAll(...e){return e.reduce(((e,t)=>[...e,...this.findAllTargets(t),...this.findAllLegacyTargets(t)]),[])}findTarget(e){const t=this.getSelectorForTargetName(e);return this.scope.findElement(t)}findAllTargets(e){const t=this.getSelectorForTargetName(e);return this.scope.findAllElements(t)}getSelectorForTargetName(e){const t=this.schema.targetAttributeForScope(this.identifier);return attributeValueContainsToken(t,e)}findLegacyTarget(e){const t=this.getLegacySelectorForTargetName(e);return this.deprecate(this.scope.findElement(t),e)}findAllLegacyTargets(e){const t=this.getLegacySelectorForTargetName(e);return this.scope.findAllElements(t).map((t=>this.deprecate(t,e)))}getLegacySelectorForTargetName(e){const t=`${this.identifier}.${e}`;return attributeValueContainsToken(this.schema.targetAttribute,t)}deprecate(e,t){if(e){const{identifier:r}=this;const s=this.schema.targetAttribute;const n=this.schema.targetAttributeForScope(r);this.guide.warn(e,`target:${t}`,`Please replace ${s}="${r}.${t}" with ${n}="${t}". The ${s} attribute is deprecated and will be removed in a future version of Stimulus.`)}return e}get guide(){return this.scope.guide}}class OutletSet{constructor(e,t){this.scope=e;this.controllerElement=t}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get schema(){return this.scope.schema}has(e){return null!=this.find(e)}find(...e){return e.reduce(((e,t)=>e||this.findOutlet(t)),void 0)}findAll(...e){return e.reduce(((e,t)=>[...e,...this.findAllOutlets(t)]),[])}getSelectorForOutletName(e){const t=this.schema.outletAttributeForScope(this.identifier,e);return this.controllerElement.getAttribute(t)}findOutlet(e){const t=this.getSelectorForOutletName(e);if(t)return this.findElement(t,e)}findAllOutlets(e){const t=this.getSelectorForOutletName(e);return t?this.findAllElements(t,e):[]}findElement(e,t){const r=this.scope.queryElements(e);return r.filter((r=>this.matchesElement(r,e,t)))[0]}findAllElements(e,t){const r=this.scope.queryElements(e);return r.filter((r=>this.matchesElement(r,e,t)))}matchesElement(e,t,r){const s=e.getAttribute(this.scope.schema.controllerAttribute)||"";return e.matches(t)&&s.split(" ").includes(r)}}class Scope{constructor(e,t,r,s){this.targets=new TargetSet(this);this.classes=new ClassMap(this);this.data=new DataMap(this);this.containsElement=e=>e.closest(this.controllerSelector)===this.element;this.schema=e;this.element=t;this.identifier=r;this.guide=new Guide(s);this.outlets=new OutletSet(this.documentScope,t)}findElement(e){return this.element.matches(e)?this.element:this.queryElements(e).find(this.containsElement)}findAllElements(e){return[...this.element.matches(e)?[this.element]:[],...this.queryElements(e).filter(this.containsElement)]}queryElements(e){return Array.from(this.element.querySelectorAll(e))}get controllerSelector(){return attributeValueContainsToken(this.schema.controllerAttribute,this.identifier)}get isDocumentScope(){return this.element===document.documentElement}get documentScope(){return this.isDocumentScope?this:new Scope(this.schema,document.documentElement,this.identifier,this.guide.logger)}}class ScopeObserver{constructor(e,t,r){this.element=e;this.schema=t;this.delegate=r;this.valueListObserver=new ValueListObserver(this.element,this.controllerAttribute,this);this.scopesByIdentifierByElement=new WeakMap;this.scopeReferenceCounts=new WeakMap}start(){this.valueListObserver.start()}stop(){this.valueListObserver.stop()}get controllerAttribute(){return this.schema.controllerAttribute}parseValueForToken(e){const{element:t,content:r}=e;return this.parseValueForElementAndIdentifier(t,r)}parseValueForElementAndIdentifier(e,t){const r=this.fetchScopesByIdentifierForElement(e);let s=r.get(t);if(!s){s=this.delegate.createScopeForElementAndIdentifier(e,t);r.set(t,s)}return s}elementMatchedValue(e,t){const r=(this.scopeReferenceCounts.get(t)||0)+1;this.scopeReferenceCounts.set(t,r);1==r&&this.delegate.scopeConnected(t)}elementUnmatchedValue(e,t){const r=this.scopeReferenceCounts.get(t);if(r){this.scopeReferenceCounts.set(t,r-1);1==r&&this.delegate.scopeDisconnected(t)}}fetchScopesByIdentifierForElement(e){let t=this.scopesByIdentifierByElement.get(e);if(!t){t=new Map;this.scopesByIdentifierByElement.set(e,t)}return t}}class Router{constructor(e){this.application=e;this.scopeObserver=new ScopeObserver(this.element,this.schema,this);this.scopesByIdentifier=new Multimap;this.modulesByIdentifier=new Map}get element(){return this.application.element}get schema(){return this.application.schema}get logger(){return this.application.logger}get controllerAttribute(){return this.schema.controllerAttribute}get modules(){return Array.from(this.modulesByIdentifier.values())}get contexts(){return this.modules.reduce(((e,t)=>e.concat(t.contexts)),[])}start(){this.scopeObserver.start()}stop(){this.scopeObserver.stop()}loadDefinition(e){this.unloadIdentifier(e.identifier);const t=new Module(this.application,e);this.connectModule(t);const r=e.controllerConstructor.afterLoad;r&&r.call(e.controllerConstructor,e.identifier,this.application)}unloadIdentifier(e){const t=this.modulesByIdentifier.get(e);t&&this.disconnectModule(t)}getContextForElementAndIdentifier(e,t){const r=this.modulesByIdentifier.get(t);if(r)return r.contexts.find((t=>t.element==e))}proposeToConnectScopeForElementAndIdentifier(e,t){const r=this.scopeObserver.parseValueForElementAndIdentifier(e,t);r?this.scopeObserver.elementMatchedValue(r.element,r):console.error(`Couldn't find or create scope for identifier: "${t}" and element:`,e)}handleError(e,t,r){this.application.handleError(e,t,r)}createScopeForElementAndIdentifier(e,t){return new Scope(this.schema,e,t,this.logger)}scopeConnected(e){this.scopesByIdentifier.add(e.identifier,e);const t=this.modulesByIdentifier.get(e.identifier);t&&t.connectContextForScope(e)}scopeDisconnected(e){this.scopesByIdentifier.delete(e.identifier,e);const t=this.modulesByIdentifier.get(e.identifier);t&&t.disconnectContextForScope(e)}connectModule(e){this.modulesByIdentifier.set(e.identifier,e);const t=this.scopesByIdentifier.getValuesForKey(e.identifier);t.forEach((t=>e.connectContextForScope(t)))}disconnectModule(e){this.modulesByIdentifier.delete(e.identifier);const t=this.scopesByIdentifier.getValuesForKey(e.identifier);t.forEach((t=>e.disconnectContextForScope(t)))}}const o={controllerAttribute:"data-controller",actionAttribute:"data-action",targetAttribute:"data-target",targetAttributeForScope:e=>`data-${e}-target`,outletAttributeForScope:(e,t)=>`data-${e}-${t}-outlet`,keyMappings:Object.assign(Object.assign({enter:"Enter",tab:"Tab",esc:"Escape",space:" ",up:"ArrowUp",down:"ArrowDown",left:"ArrowLeft",right:"ArrowRight",home:"Home",end:"End",page_up:"PageUp",page_down:"PageDown"},objectFromEntries("abcdefghijklmnopqrstuvwxyz".split("").map((e=>[e,e])))),objectFromEntries("0123456789".split("").map((e=>[e,e]))))};function objectFromEntries(e){return e.reduce(((e,[t,r])=>Object.assign(Object.assign({},e),{[t]:r})),{})}class Application{constructor(t=document.documentElement,r=o){this.logger=console;this.debug=false;this.logDebugActivity=(e,t,r={})=>{this.debug&&this.logFormattedMessage(e,t,r)};this.element=t;this.schema=r;this.dispatcher=new Dispatcher(this);this.router=new Router(this);this.actionDescriptorFilters=Object.assign({},e)}static start(e,t){const r=new this(e,t);r.start();return r}async start(){await domReady();this.logDebugActivity("application","starting");this.dispatcher.start();this.router.start();this.logDebugActivity("application","start")}stop(){this.logDebugActivity("application","stopping");this.dispatcher.stop();this.router.stop();this.logDebugActivity("application","stop")}register(e,t){this.load({identifier:e,controllerConstructor:t})}registerActionOption(e,t){this.actionDescriptorFilters[e]=t}load(e,...t){const r=Array.isArray(e)?e:[e,...t];r.forEach((e=>{e.controllerConstructor.shouldLoad&&this.router.loadDefinition(e)}))}unload(e,...t){const r=Array.isArray(e)?e:[e,...t];r.forEach((e=>this.router.unloadIdentifier(e)))}get controllers(){return this.router.contexts.map((e=>e.controller))}getControllerForElementAndIdentifier(e,t){const r=this.router.getContextForElementAndIdentifier(e,t);return r?r.controller:null}handleError(e,t,r){var s;this.logger.error("%s\n\n%o\n\n%o",t,e,r);null===(s=window.onerror)||void 0===s?void 0:s.call(window,t,"",0,0,e)}logFormattedMessage(e,t,r={}){r=Object.assign({application:this},r);this.logger.groupCollapsed(`${e} #${t}`);this.logger.log("details:",Object.assign({},r));this.logger.groupEnd()}}function domReady(){return new Promise((e=>{"loading"==document.readyState?document.addEventListener("DOMContentLoaded",(()=>e())):e()}))}function ClassPropertiesBlessing(e){const t=readInheritableStaticArrayValues(e,"classes");return t.reduce(((e,t)=>Object.assign(e,propertiesForClassDefinition(t))),{})}function propertiesForClassDefinition(e){return{[`${e}Class`]:{get(){const{classes:t}=this;if(t.has(e))return t.get(e);{const r=t.getAttributeName(e);throw new Error(`Missing attribute "${r}"`)}}},[`${e}Classes`]:{get(){return this.classes.getAll(e)}},[`has${capitalize(e)}Class`]:{get(){return this.classes.has(e)}}}}function OutletPropertiesBlessing(e){const t=readInheritableStaticArrayValues(e,"outlets");return t.reduce(((e,t)=>Object.assign(e,propertiesForOutletDefinition(t))),{})}function getOutletController(e,t,r){return e.application.getControllerForElementAndIdentifier(t,r)}function getControllerAndEnsureConnectedScope(e,t,r){let s=getOutletController(e,t,r);if(s)return s;e.application.router.proposeToConnectScopeForElementAndIdentifier(t,r);s=getOutletController(e,t,r);return s||void 0}function propertiesForOutletDefinition(e){const t=namespaceCamelize(e);return{[`${t}Outlet`]:{get(){const t=this.outlets.find(e);const r=this.outlets.getSelectorForOutletName(e);if(t){const r=getControllerAndEnsureConnectedScope(this,t,e);if(r)return r;throw new Error(`The provided outlet element is missing an outlet controller "${e}" instance for host controller "${this.identifier}"`)}throw new Error(`Missing outlet element "${e}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${r}".`)}},[`${t}Outlets`]:{get(){const t=this.outlets.findAll(e);return t.length>0?t.map((t=>{const r=getControllerAndEnsureConnectedScope(this,t,e);if(r)return r;console.warn(`The provided outlet element is missing an outlet controller "${e}" instance for host controller "${this.identifier}"`,t)})).filter((e=>e)):[]}},[`${t}OutletElement`]:{get(){const t=this.outlets.find(e);const r=this.outlets.getSelectorForOutletName(e);if(t)return t;throw new Error(`Missing outlet element "${e}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${r}".`)}},[`${t}OutletElements`]:{get(){return this.outlets.findAll(e)}},[`has${capitalize(t)}Outlet`]:{get(){return this.outlets.has(e)}}}}function TargetPropertiesBlessing(e){const t=readInheritableStaticArrayValues(e,"targets");return t.reduce(((e,t)=>Object.assign(e,propertiesForTargetDefinition(t))),{})}function propertiesForTargetDefinition(e){return{[`${e}Target`]:{get(){const t=this.targets.find(e);if(t)return t;throw new Error(`Missing target element "${e}" for "${this.identifier}" controller`)}},[`${e}Targets`]:{get(){return this.targets.findAll(e)}},[`has${capitalize(e)}Target`]:{get(){return this.targets.has(e)}}}}function ValuePropertiesBlessing(e){const t=readInheritableStaticObjectPairs(e,"values");const r={valueDescriptorMap:{get(){return t.reduce(((e,t)=>{const r=parseValueDefinitionPair(t,this.identifier);const s=this.data.getAttributeNameForKey(r.key);return Object.assign(e,{[s]:r})}),{})}}};return t.reduce(((e,t)=>Object.assign(e,propertiesForValueDefinitionPair(t))),r)}function propertiesForValueDefinitionPair(e,t){const r=parseValueDefinitionPair(e,t);const{key:s,name:n,reader:i,writer:o}=r;return{[n]:{get(){const e=this.data.get(s);return null!==e?i(e):r.defaultValue},set(e){void 0===e?this.data.delete(s):this.data.set(s,o(e))}},[`has${capitalize(n)}`]:{get(){return this.data.has(s)||r.hasCustomDefaultValue}}}}function parseValueDefinitionPair([e,t],r){return valueDescriptorForTokenAndTypeDefinition({controller:r,token:e,typeDefinition:t})}function parseValueTypeConstant(e){switch(e){case Array:return"array";case Boolean:return"boolean";case Number:return"number";case Object:return"object";case String:return"string"}}function parseValueTypeDefault(e){switch(typeof e){case"boolean":return"boolean";case"number":return"number";case"string":return"string"}return Array.isArray(e)?"array":"[object Object]"===Object.prototype.toString.call(e)?"object":void 0}function parseValueTypeObject(e){const{controller:t,token:r,typeObject:s}=e;const n=isSomething(s.type);const i=isSomething(s.default);const o=n&&i;const c=n&&!i;const l=!n&&i;const h=parseValueTypeConstant(s.type);const u=parseValueTypeDefault(e.typeObject.default);if(c)return h;if(l)return u;if(h!==u){const e=t?`${t}.${r}`:r;throw new Error(`The specified default value for the Stimulus Value "${e}" must match the defined type "${h}". The provided default value of "${s.default}" is of type "${u}".`)}return o?h:void 0}function parseValueTypeDefinition(e){const{controller:t,token:r,typeDefinition:s}=e;const n={controller:t,token:r,typeObject:s};const i=parseValueTypeObject(n);const o=parseValueTypeDefault(s);const c=parseValueTypeConstant(s);const l=i||o||c;if(l)return l;const h=t?`${t}.${s}`:r;throw new Error(`Unknown value type "${h}" for "${r}" value`)}function defaultValueForDefinition(e){const t=parseValueTypeConstant(e);if(t)return c[t];const r=hasProperty(e,"default");const s=hasProperty(e,"type");const n=e;if(r)return n.default;if(s){const{type:e}=n;const t=parseValueTypeConstant(e);if(t)return c[t]}return e}function valueDescriptorForTokenAndTypeDefinition(e){const{token:t,typeDefinition:r}=e;const s=`${dasherize(t)}-value`;const n=parseValueTypeDefinition(e);return{type:n,key:s,name:camelize(s),get defaultValue(){return defaultValueForDefinition(r)},get hasCustomDefaultValue(){return void 0!==parseValueTypeDefault(r)},reader:l[n],writer:h[n]||h.default}}const c={get array(){return[]},boolean:false,number:0,get object(){return{}},string:""};const l={array(e){const t=JSON.parse(e);if(!Array.isArray(t))throw new TypeError(`expected value of type "array" but instead got value "${e}" of type "${parseValueTypeDefault(t)}"`);return t},boolean(e){return!("0"==e||"false"==String(e).toLowerCase())},number(e){return Number(e.replace(/_/g,""))},object(e){const t=JSON.parse(e);if(null===t||"object"!=typeof t||Array.isArray(t))throw new TypeError(`expected value of type "object" but instead got value "${e}" of type "${parseValueTypeDefault(t)}"`);return t},string(e){return e}};const h={default:writeString,array:writeJSON,object:writeJSON};function writeJSON(e){return JSON.stringify(e)}function writeString(e){return`${e}`}class Controller{constructor(e){this.context=e}static get shouldLoad(){return true}static afterLoad(e,t){}get application(){return this.context.application}get scope(){return this.context.scope}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get targets(){return this.scope.targets}get outlets(){return this.scope.outlets}get classes(){return this.scope.classes}get data(){return this.scope.data}initialize(){}connect(){}disconnect(){}dispatch(e,{target:t=this.element,detail:r={},prefix:s=this.identifier,bubbles:n=true,cancelable:i=true}={}){const o=s?`${s}:${e}`:e;const c=new CustomEvent(o,{detail:r,bubbles:n,cancelable:i});t.dispatchEvent(c);return c}}Controller.blessings=[ClassPropertiesBlessing,TargetPropertiesBlessing,ValuePropertiesBlessing,OutletPropertiesBlessing];Controller.targets=[];Controller.outlets=[];Controller.values={};export{Application,AttributeObserver,Context,Controller,ElementObserver,IndexedMultimap,Multimap,SelectorObserver,StringMapObserver,TokenListObserver,ValueListObserver,add,o as defaultSchema,del,fetch,prune}; + diff --git a/vendor/javascript/@rails--activestorage.js b/vendor/javascript/@rails--activestorage.js new file mode 100644 index 0000000..e03a9f4 --- /dev/null +++ b/vendor/javascript/@rails--activestorage.js @@ -0,0 +1,2 @@ +var t={exports:{}};(function(t,e){(function(e){t.exports=e()})((function(t){var e=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];function md5cycle(t,e){var r=t[0],i=t[1],s=t[2],n=t[3];r+=(i&s|~i&n)+e[0]-680876936|0;r=(r<<7|r>>>25)+i|0;n+=(r&i|~r&s)+e[1]-389564586|0;n=(n<<12|n>>>20)+r|0;s+=(n&r|~n&i)+e[2]+606105819|0;s=(s<<17|s>>>15)+n|0;i+=(s&n|~s&r)+e[3]-1044525330|0;i=(i<<22|i>>>10)+s|0;r+=(i&s|~i&n)+e[4]-176418897|0;r=(r<<7|r>>>25)+i|0;n+=(r&i|~r&s)+e[5]+1200080426|0;n=(n<<12|n>>>20)+r|0;s+=(n&r|~n&i)+e[6]-1473231341|0;s=(s<<17|s>>>15)+n|0;i+=(s&n|~s&r)+e[7]-45705983|0;i=(i<<22|i>>>10)+s|0;r+=(i&s|~i&n)+e[8]+1770035416|0;r=(r<<7|r>>>25)+i|0;n+=(r&i|~r&s)+e[9]-1958414417|0;n=(n<<12|n>>>20)+r|0;s+=(n&r|~n&i)+e[10]-42063|0;s=(s<<17|s>>>15)+n|0;i+=(s&n|~s&r)+e[11]-1990404162|0;i=(i<<22|i>>>10)+s|0;r+=(i&s|~i&n)+e[12]+1804603682|0;r=(r<<7|r>>>25)+i|0;n+=(r&i|~r&s)+e[13]-40341101|0;n=(n<<12|n>>>20)+r|0;s+=(n&r|~n&i)+e[14]-1502002290|0;s=(s<<17|s>>>15)+n|0;i+=(s&n|~s&r)+e[15]+1236535329|0;i=(i<<22|i>>>10)+s|0;r+=(i&n|s&~n)+e[1]-165796510|0;r=(r<<5|r>>>27)+i|0;n+=(r&s|i&~s)+e[6]-1069501632|0;n=(n<<9|n>>>23)+r|0;s+=(n&i|r&~i)+e[11]+643717713|0;s=(s<<14|s>>>18)+n|0;i+=(s&r|n&~r)+e[0]-373897302|0;i=(i<<20|i>>>12)+s|0;r+=(i&n|s&~n)+e[5]-701558691|0;r=(r<<5|r>>>27)+i|0;n+=(r&s|i&~s)+e[10]+38016083|0;n=(n<<9|n>>>23)+r|0;s+=(n&i|r&~i)+e[15]-660478335|0;s=(s<<14|s>>>18)+n|0;i+=(s&r|n&~r)+e[4]-405537848|0;i=(i<<20|i>>>12)+s|0;r+=(i&n|s&~n)+e[9]+568446438|0;r=(r<<5|r>>>27)+i|0;n+=(r&s|i&~s)+e[14]-1019803690|0;n=(n<<9|n>>>23)+r|0;s+=(n&i|r&~i)+e[3]-187363961|0;s=(s<<14|s>>>18)+n|0;i+=(s&r|n&~r)+e[8]+1163531501|0;i=(i<<20|i>>>12)+s|0;r+=(i&n|s&~n)+e[13]-1444681467|0;r=(r<<5|r>>>27)+i|0;n+=(r&s|i&~s)+e[2]-51403784|0;n=(n<<9|n>>>23)+r|0;s+=(n&i|r&~i)+e[7]+1735328473|0;s=(s<<14|s>>>18)+n|0;i+=(s&r|n&~r)+e[12]-1926607734|0;i=(i<<20|i>>>12)+s|0;r+=(i^s^n)+e[5]-378558|0;r=(r<<4|r>>>28)+i|0;n+=(r^i^s)+e[8]-2022574463|0;n=(n<<11|n>>>21)+r|0;s+=(n^r^i)+e[11]+1839030562|0;s=(s<<16|s>>>16)+n|0;i+=(s^n^r)+e[14]-35309556|0;i=(i<<23|i>>>9)+s|0;r+=(i^s^n)+e[1]-1530992060|0;r=(r<<4|r>>>28)+i|0;n+=(r^i^s)+e[4]+1272893353|0;n=(n<<11|n>>>21)+r|0;s+=(n^r^i)+e[7]-155497632|0;s=(s<<16|s>>>16)+n|0;i+=(s^n^r)+e[10]-1094730640|0;i=(i<<23|i>>>9)+s|0;r+=(i^s^n)+e[13]+681279174|0;r=(r<<4|r>>>28)+i|0;n+=(r^i^s)+e[0]-358537222|0;n=(n<<11|n>>>21)+r|0;s+=(n^r^i)+e[3]-722521979|0;s=(s<<16|s>>>16)+n|0;i+=(s^n^r)+e[6]+76029189|0;i=(i<<23|i>>>9)+s|0;r+=(i^s^n)+e[9]-640364487|0;r=(r<<4|r>>>28)+i|0;n+=(r^i^s)+e[12]-421815835|0;n=(n<<11|n>>>21)+r|0;s+=(n^r^i)+e[15]+530742520|0;s=(s<<16|s>>>16)+n|0;i+=(s^n^r)+e[2]-995338651|0;i=(i<<23|i>>>9)+s|0;r+=(s^(i|~n))+e[0]-198630844|0;r=(r<<6|r>>>26)+i|0;n+=(i^(r|~s))+e[7]+1126891415|0;n=(n<<10|n>>>22)+r|0;s+=(r^(n|~i))+e[14]-1416354905|0;s=(s<<15|s>>>17)+n|0;i+=(n^(s|~r))+e[5]-57434055|0;i=(i<<21|i>>>11)+s|0;r+=(s^(i|~n))+e[12]+1700485571|0;r=(r<<6|r>>>26)+i|0;n+=(i^(r|~s))+e[3]-1894986606|0;n=(n<<10|n>>>22)+r|0;s+=(r^(n|~i))+e[10]-1051523|0;s=(s<<15|s>>>17)+n|0;i+=(n^(s|~r))+e[1]-2054922799|0;i=(i<<21|i>>>11)+s|0;r+=(s^(i|~n))+e[8]+1873313359|0;r=(r<<6|r>>>26)+i|0;n+=(i^(r|~s))+e[15]-30611744|0;n=(n<<10|n>>>22)+r|0;s+=(r^(n|~i))+e[6]-1560198380|0;s=(s<<15|s>>>17)+n|0;i+=(n^(s|~r))+e[13]+1309151649|0;i=(i<<21|i>>>11)+s|0;r+=(s^(i|~n))+e[4]-145523070|0;r=(r<<6|r>>>26)+i|0;n+=(i^(r|~s))+e[11]-1120210379|0;n=(n<<10|n>>>22)+r|0;s+=(r^(n|~i))+e[2]+718787259|0;s=(s<<15|s>>>17)+n|0;i+=(n^(s|~r))+e[9]-343485551|0;i=(i<<21|i>>>11)+s|0;t[0]=r+t[0]|0;t[1]=i+t[1]|0;t[2]=s+t[2]|0;t[3]=n+t[3]|0}function md5blk(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t.charCodeAt(e)+(t.charCodeAt(e+1)<<8)+(t.charCodeAt(e+2)<<16)+(t.charCodeAt(e+3)<<24);return r}function md5blk_array(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t[e]+(t[e+1]<<8)+(t[e+2]<<16)+(t[e+3]<<24);return r}function md51(t){var e,r,i,s,n,a,o=t.length,h=[1732584193,-271733879,-1732584194,271733878];for(e=64;e<=o;e+=64)md5cycle(h,md5blk(t.substring(e-64,e)));t=t.substring(e-64);r=t.length;i=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e>2]|=t.charCodeAt(e)<<(e%4<<3);i[e>>2]|=128<<(e%4<<3);if(e>55){md5cycle(h,i);for(e=0;e<16;e+=1)i[e]=0}s=o*8;s=s.toString(16).match(/(.*?)(.{0,8})$/);n=parseInt(s[2],16);a=parseInt(s[1],16)||0;i[14]=n;i[15]=a;md5cycle(h,i);return h}function md51_array(t){var e,r,i,s,n,a,o=t.length,h=[1732584193,-271733879,-1732584194,271733878];for(e=64;e<=o;e+=64)md5cycle(h,md5blk_array(t.subarray(e-64,e)));t=e-64>2]|=t[e]<<(e%4<<3);i[e>>2]|=128<<(e%4<<3);if(e>55){md5cycle(h,i);for(e=0;e<16;e+=1)i[e]=0}s=o*8;s=s.toString(16).match(/(.*?)(.{0,8})$/);n=parseInt(s[2],16);a=parseInt(s[1],16)||0;i[14]=n;i[15]=a;md5cycle(h,i);return h}function rhex(t){var r,i="";for(r=0;r<4;r+=1)i+=e[t>>r*8+4&15]+e[t>>r*8&15];return i}function hex(t){var e;for(e=0;eu)return new ArrayBuffer(0);i=u-h;s=new ArrayBuffer(i);n=new Uint8Array(s);a=new Uint8Array(this,h,i);n.set(a);return s}}();function toUtf8(t){/[\u0080-\uFFFF]/.test(t)&&(t=unescape(encodeURIComponent(t)));return t}function utf8Str2ArrayBuffer(t,e){var r,i=t.length,s=new ArrayBuffer(i),n=new Uint8Array(s);for(r=0;r>2]|=i.charCodeAt(e)<<(e%4<<3);this._finish(n,s);r=hex(this._hash);t&&(r=hexToBinaryString(r));this.reset();return r};SparkMD5.prototype.reset=function(){this._buff="";this._length=0;this._hash=[1732584193,-271733879,-1732584194,271733878];return this};SparkMD5.prototype.getState=function(){return{buff:this._buff,length:this._length,hash:this._hash.slice()}};SparkMD5.prototype.setState=function(t){this._buff=t.buff;this._length=t.length;this._hash=t.hash;return this};SparkMD5.prototype.destroy=function(){delete this._hash;delete this._buff;delete this._length};SparkMD5.prototype._finish=function(t,e){var r,i,s,n=e;t[n>>2]|=128<<(n%4<<3);if(n>55){md5cycle(this._hash,t);for(n=0;n<16;n+=1)t[n]=0}r=this._length*8;r=r.toString(16).match(/(.*?)(.{0,8})$/);i=parseInt(r[2],16);s=parseInt(r[1],16)||0;t[14]=i;t[15]=s;md5cycle(this._hash,t)};SparkMD5.hash=function(t,e){return SparkMD5.hashBinary(toUtf8(t),e)};SparkMD5.hashBinary=function(t,e){var r=md51(t),i=hex(r);return e?hexToBinaryString(i):i};SparkMD5.ArrayBuffer=function(){this.reset()};SparkMD5.ArrayBuffer.prototype.append=function(t){var e,r=concatenateArrayBuffers(this._buff.buffer,t,true),i=r.length;this._length+=t.byteLength;for(e=64;e<=i;e+=64)md5cycle(this._hash,md5blk_array(r.subarray(e-64,e)));this._buff=e-64>2]|=i[e]<<(e%4<<3);this._finish(n,s);r=hex(this._hash);t&&(r=hexToBinaryString(r));this.reset();return r};SparkMD5.ArrayBuffer.prototype.reset=function(){this._buff=new Uint8Array(0);this._length=0;this._hash=[1732584193,-271733879,-1732584194,271733878];return this};SparkMD5.ArrayBuffer.prototype.getState=function(){var t=SparkMD5.prototype.getState.call(this);t.buff=arrayBuffer2Utf8Str(t.buff);return t};SparkMD5.ArrayBuffer.prototype.setState=function(t){t.buff=utf8Str2ArrayBuffer(t.buff,true);return SparkMD5.prototype.setState.call(this,t)};SparkMD5.ArrayBuffer.prototype.destroy=SparkMD5.prototype.destroy;SparkMD5.ArrayBuffer.prototype._finish=SparkMD5.prototype._finish;SparkMD5.ArrayBuffer.hash=function(t,e){var r=md51_array(new Uint8Array(t)),i=hex(r);return e?hexToBinaryString(i):i};return SparkMD5}))})(t);var e=t.exports;const r=File.prototype.slice||File.prototype.mozSlice||File.prototype.webkitSlice;class FileChecksum{static create(t,e){const r=new FileChecksum(t);r.create(e)}constructor(t){this.file=t;this.chunkSize=2097152;this.chunkCount=Math.ceil(this.file.size/this.chunkSize);this.chunkIndex=0}create(t){this.callback=t;this.md5Buffer=new e.ArrayBuffer;this.fileReader=new FileReader;this.fileReader.addEventListener("load",(t=>this.fileReaderDidLoad(t)));this.fileReader.addEventListener("error",(t=>this.fileReaderDidError(t)));this.readNextChunk()}fileReaderDidLoad(t){this.md5Buffer.append(t.target.result);if(!this.readNextChunk()){const t=this.md5Buffer.end(true);const e=btoa(t);this.callback(null,e)}}fileReaderDidError(t){this.callback(`Error reading ${this.file.name}`)}readNextChunk(){if(this.chunkIndex{this.xhr.setRequestHeader(t,i[t])}));const s=getMetaValue("csrf-token");s!=void 0&&this.xhr.setRequestHeader("X-CSRF-Token",s);this.xhr.addEventListener("load",(t=>this.requestDidLoad(t)));this.xhr.addEventListener("error",(t=>this.requestDidError(t)))}get status(){return this.xhr.status}get response(){const{responseType:t,response:e}=this.xhr;return t=="json"?e:JSON.parse(e)}create(t){this.callback=t;this.xhr.send(JSON.stringify({blob:this.attributes}))}requestDidLoad(t){if(this.status>=200&&this.status<300){const{response:t}=this;const{direct_upload:e}=t;delete t.direct_upload;this.attributes=t;this.directUploadData=e;this.callback(null,this.toJSON())}else this.requestDidError(t)}requestDidError(t){this.callback(`Error creating Blob for "${this.file.name}". Status: ${this.status}`)}toJSON(){const t={};for(const e in this.attributes)t[e]=this.attributes[e];return t}}class BlobUpload{constructor(t){this.blob=t;this.file=t.file;const{url:e,headers:r}=t.directUploadData;this.xhr=new XMLHttpRequest;this.xhr.open("PUT",e,true);this.xhr.responseType="text";for(const t in r)this.xhr.setRequestHeader(t,r[t]);this.xhr.addEventListener("load",(t=>this.requestDidLoad(t)));this.xhr.addEventListener("error",(t=>this.requestDidError(t)))}create(t){this.callback=t;this.xhr.send(this.file.slice())}requestDidLoad(t){const{status:e,response:r}=this.xhr;e>=200&&e<300?this.callback(null,r):this.requestDidError(t)}requestDidError(t){this.callback(`Error storing "${this.file.name}". Status: ${this.xhr.status}`)}}let i=0;class DirectUpload{constructor(t,e,r,s={}){this.id=++i;this.file=t;this.url=e;this.delegate=r;this.customHeaders=s}create(t){FileChecksum.create(this.file,((e,r)=>{if(e){t(e);return}const i=new BlobRecord(this.file,r,this.url,this.customHeaders);notify(this.delegate,"directUploadWillCreateBlobWithXHR",i.xhr);i.create((e=>{if(e)t(e);else{const e=new BlobUpload(i);notify(this.delegate,"directUploadWillStoreFileWithXHR",e.xhr);e.create((e=>{e?t(e):t(null,i.toJSON())}))}}))}))}}function notify(t,e,...r){if(t&&typeof t[e]=="function")return t[e](...r)}class DirectUploadController{constructor(t,e){this.input=t;this.file=e;this.directUpload=new DirectUpload(this.file,this.url,this);this.dispatch("initialize")}start(t){const e=document.createElement("input");e.type="hidden";e.name=this.input.name;this.input.insertAdjacentElement("beforebegin",e);this.dispatch("start");this.directUpload.create(((r,i)=>{if(r){e.parentNode.removeChild(e);this.dispatchError(r)}else e.value=i.signed_id;this.dispatch("end");t(r)}))}uploadRequestDidProgress(t){const e=t.loaded/t.total*100;e&&this.dispatch("progress",{progress:e})}get url(){return this.input.getAttribute("data-direct-upload-url")}dispatch(t,e={}){e.file=this.file;e.id=this.directUpload.id;return dispatchEvent(this.input,`direct-upload:${t}`,{detail:e})}dispatchError(t){const e=this.dispatch("error",{error:t});e.defaultPrevented||alert(t)}directUploadWillCreateBlobWithXHR(t){this.dispatch("before-blob-request",{xhr:t})}directUploadWillStoreFileWithXHR(t){this.dispatch("before-storage-request",{xhr:t});t.upload.addEventListener("progress",(t=>this.uploadRequestDidProgress(t)))}}const s="input[type=file][data-direct-upload-url]:not([disabled])";class DirectUploadsController{constructor(t){this.form=t;this.inputs=findElements(t,s).filter((t=>t.files.length))}start(t){const e=this.createDirectUploadControllers();const startNextController=()=>{const r=e.shift();if(r)r.start((e=>{if(e){t(e);this.dispatch("end")}else startNextController()}));else{t();this.dispatch("end")}};this.dispatch("start");startNextController()}createDirectUploadControllers(){const t=[];this.inputs.forEach((e=>{toArray(e.files).forEach((r=>{const i=new DirectUploadController(e,r);t.push(i)}))}));return t}dispatch(t,e={}){return dispatchEvent(this.form,`direct-uploads:${t}`,{detail:e})}}const n="data-direct-uploads-processing";const a=new WeakMap;let o=false;function start(){if(!o){o=true;document.addEventListener("click",didClick,true);document.addEventListener("submit",didSubmitForm,true);document.addEventListener("ajax:before",didSubmitRemoteElement)}}function didClick(t){const e=t.target.closest("button, input");e&&e.type==="submit"&&e.form&&a.set(e.form,e)}function didSubmitForm(t){handleFormSubmissionEvent(t)}function didSubmitRemoteElement(t){t.target.tagName=="FORM"&&handleFormSubmissionEvent(t)}function handleFormSubmissionEvent(t){const e=t.target;if(e.hasAttribute(n)){t.preventDefault();return}const r=new DirectUploadsController(e);const{inputs:i}=r;if(i.length){t.preventDefault();e.setAttribute(n,"");i.forEach(disable);r.start((t=>{e.removeAttribute(n);t?i.forEach(enable):submitForm(e)}))}}function submitForm(t){let e=a.get(t)||findElement(t,"input[type=submit], button[type=submit]");if(e){const{disabled:t}=e;e.disabled=false;e.focus();e.click();e.disabled=t}else{e=document.createElement("input");e.type="submit";e.style.display="none";t.appendChild(e);e.click();t.removeChild(e)}a.delete(t)}function disable(t){t.disabled=true}function enable(t){t.disabled=false}function autostart(){window.ActiveStorage&&start()}setTimeout(autostart,1);export{DirectUpload,DirectUploadController,DirectUploadsController,start}; + diff --git a/vendor/javascript/@rails--request.js.js b/vendor/javascript/@rails--request.js.js new file mode 100644 index 0000000..d360734 --- /dev/null +++ b/vendor/javascript/@rails--request.js.js @@ -0,0 +1,2 @@ +class FetchResponse{constructor(t){this.response=t}get statusCode(){return this.response.status}get redirected(){return this.response.redirected}get ok(){return this.response.ok}get unauthenticated(){return 401===this.statusCode}get unprocessableEntity(){return 422===this.statusCode}get authenticationURL(){return this.response.headers.get("WWW-Authenticate")}get contentType(){const t=this.response.headers.get("Content-Type")||"";return t.replace(/;.*$/,"")}get headers(){return this.response.headers}get html(){return this.contentType.match(/^(application|text)\/(html|xhtml\+xml)$/)?this.text:Promise.reject(new Error(`Expected an HTML response but got "${this.contentType}" instead`))}get json(){return this.contentType.match(/^application\/.*json$/)?this.responseJson||(this.responseJson=this.response.json()):Promise.reject(new Error(`Expected a JSON response but got "${this.contentType}" instead`))}get text(){return this.responseText||(this.responseText=this.response.text())}get isTurboStream(){return this.contentType.match(/^text\/vnd\.turbo-stream\.html/)}async renderTurboStream(){if(!this.isTurboStream)return Promise.reject(new Error(`Expected a Turbo Stream response but got "${this.contentType}" instead`));window.Turbo?await window.Turbo.renderStreamMessage(await this.text):console.warn("You must set `window.Turbo = Turbo` to automatically process Turbo Stream events with request.js")}}class RequestInterceptor{static register(t){this.interceptor=t}static get(){return this.interceptor}static reset(){this.interceptor=void 0}}function getCookie(t){const e=document.cookie?document.cookie.split("; "):[];const n=`${encodeURIComponent(t)}=`;const s=e.find((t=>t.startsWith(n)));if(s){const t=s.split("=").slice(1).join("=");if(t)return decodeURIComponent(t)}}function compact(t){const e={};for(const n in t){const s=t[n];void 0!==s&&(e[n]=s)}return e}function metaContent(t){const e=document.head.querySelector(`meta[name="${t}"]`);return e&&e.content}function stringEntriesFromFormData(t){return[...t].reduce(((t,[e,n])=>t.concat("string"===typeof n?[[e,n]]:[])),[])}function mergeEntries(t,e){for(const[n,s]of e)if(!(s instanceof window.File))if(t.has(n)&&!n.includes("[]")){t.delete(n);t.set(n,s)}else t.append(n,s)}class FetchRequest{constructor(t,e,n={}){this.method=t;this.options=n;this.originalUrl=e.toString()}async perform(){try{const t=RequestInterceptor.get();t&&await t(this)}catch(t){console.error(t)}const t=new FetchResponse(await window.fetch(this.url,this.fetchOptions));if(t.unauthenticated&&t.authenticationURL)return Promise.reject(window.location.href=t.authenticationURL);const e=t.ok||t.unprocessableEntity;e&&t.isTurboStream&&await t.renderTurboStream();return t}addHeader(t,e){const n=this.additionalHeaders;n[t]=e;this.options.headers=n}sameHostname(){if(!this.originalUrl.startsWith("http:"))return true;try{return new URL(this.originalUrl).hostname===window.location.hostname}catch(t){return true}}get fetchOptions(){return{method:this.method.toUpperCase(),headers:this.headers,body:this.formattedBody,signal:this.signal,credentials:this.credentials,redirect:this.redirect}}get headers(){const t={"X-Requested-With":"XMLHttpRequest","Content-Type":this.contentType,Accept:this.accept};this.sameHostname()&&(t["X-CSRF-Token"]=this.csrfToken);return compact(Object.assign(t,this.additionalHeaders))}get csrfToken(){return getCookie(metaContent("csrf-param"))||metaContent("csrf-token")}get contentType(){return this.options.contentType?this.options.contentType:null==this.body||this.body instanceof window.FormData?void 0:this.body instanceof window.File?this.body.type:"application/json"}get accept(){switch(this.responseKind){case"html":return"text/html, application/xhtml+xml";case"turbo-stream":return"text/vnd.turbo-stream.html, text/html, application/xhtml+xml";case"json":return"application/json, application/vnd.api+json";default:return"*/*"}}get body(){return this.options.body}get query(){const t=(this.originalUrl.split("?")[1]||"").split("#")[0];const e=new URLSearchParams(t);let n=this.options.query;n=n instanceof window.FormData?stringEntriesFromFormData(n):n instanceof window.URLSearchParams?n.entries():Object.entries(n||{});mergeEntries(e,n);const s=e.toString();return s.length>0?`?${s}`:""}get url(){return this.originalUrl.split("?")[0].split("#")[0]+this.query}get responseKind(){return this.options.responseKind||"html"}get signal(){return this.options.signal}get redirect(){return this.options.redirect||"follow"}get credentials(){return this.options.credentials||"same-origin"}get additionalHeaders(){return this.options.headers||{}}get formattedBody(){const t="[object String]"===Object.prototype.toString.call(this.body);const e="application/json"===this.headers["Content-Type"];return e&&!t?JSON.stringify(this.body):this.body}}async function get(t,e){const n=new FetchRequest("get",t,e);return n.perform()}async function post(t,e){const n=new FetchRequest("post",t,e);return n.perform()}async function put(t,e){const n=new FetchRequest("put",t,e);return n.perform()}async function patch(t,e){const n=new FetchRequest("patch",t,e);return n.perform()}async function destroy(t,e){const n=new FetchRequest("delete",t,e);return n.perform()}export{FetchRequest,FetchResponse,RequestInterceptor,destroy,get,patch,post,put}; + diff --git a/vendor/javascript/sortablejs.js b/vendor/javascript/sortablejs.js new file mode 100644 index 0000000..b3ad462 --- /dev/null +++ b/vendor/javascript/sortablejs.js @@ -0,0 +1,134 @@ +/**! + * Sortable 1.15.2 + * @author RubaXa + * @author owenm + * @license MIT + */ +function ownKeys(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);t&&(o=o.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})));n.push.apply(n,o)}return n}function _objectSpread2(e){for(var t=1;t=0||(n[r]=e[r])}return n}function _objectWithoutProperties(e,t){if(e==null)return{};var n=_objectWithoutPropertiesLoose(e,t);var o,r;if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,o)&&(n[o]=e[o])}}return n}function _toConsumableArray(e){return _arrayWithoutHoles(e)||_iterableToArray(e)||_unsupportedIterableToArray(e)||_nonIterableSpread()}function _arrayWithoutHoles(e){if(Array.isArray(e))return _arrayLikeToArray(e)}function _iterableToArray(e){if(typeof Symbol!=="undefined"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function _unsupportedIterableToArray(e,t){if(e){if(typeof e==="string")return _arrayLikeToArray(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);n==="Object"&&e.constructor&&(n=e.constructor.name);return n==="Map"||n==="Set"?Array.from(e):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?_arrayLikeToArray(e,t):void 0}}function _arrayLikeToArray(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,o=new Array(t);n"&&(t=t.substring(1));if(e)try{if(e.matches)return e.matches(t);if(e.msMatchesSelector)return e.msMatchesSelector(t);if(e.webkitMatchesSelector)return e.webkitMatchesSelector(t)}catch(e){return false}return false}}function getParentOrHost(e){return e.host&&e!==document&&e.host.nodeType?e.host:e.parentNode}function closest(e,t,n,o){if(e){n=n||document;do{if(t!=null&&(t[0]===">"?e.parentNode===n&&matches(e,t):matches(e,t))||o&&e===n)return e;if(e===n)break}while(e=getParentOrHost(e))}return null}var s=/\s+/g;function toggleClass(e,t,n){if(e&&t)if(e.classList)e.classList[n?"add":"remove"](t);else{var o=(" "+e.className+" ").replace(s," ").replace(" "+t+" "," ");e.className=(o+(n?" "+t:"")).replace(s," ")}}function css(e,t,n){var o=e&&e.style;if(o){if(n===void 0){document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(e,""):e.currentStyle&&(n=e.currentStyle);return t===void 0?n:n[t]}t in o||t.indexOf("webkit")!==-1||(t="-webkit-"+t);o[t]=n+(typeof n==="string"?"":"px")}}function matrix(e,t){var n="";if(typeof e==="string")n=e;else do{var o=css(e,"transform");o&&o!=="none"&&(n=o+" "+n)}while(!t&&(e=e.parentNode));var r=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return r&&new r(n)}function find(e,t,n){if(e){var o=e.getElementsByTagName(t),r=0,i=o.length;if(n)for(;r=i:r<=i;if(!a)return o;if(o===getWindowScrollingElement())break;o=getParentAutoScrollElement(o,false)}return false} +/** + * Gets nth child of el, ignoring hidden children, sortable's elements (does not ignore clone if it's visible) + * and non-draggable elements + * @param {HTMLElement} el The parent element + * @param {Number} childNum The index of the child + * @param {Object} options Parent Sortable's options + * @return {HTMLElement} The child at index childNum, or null if not found + */function getChild(e,t,n,o){var r=0,i=0,a=e.children;while(i2&&arguments[2]!==void 0?arguments[2]:{},o=n.evt,r=_objectWithoutProperties(n,g);h.pluginEvent.bind(Sortable)(e,t,_objectSpread2({dragEl:v,parentEl:m,ghostEl:b,rootEl:S,nextEl:_,lastDownEl:y,cloneEl:E,cloneHidden:w,dragStarted:j,putSortable:O,activeSortable:Sortable.active,originalEvent:o,oldIndex:D,oldDraggableIndex:x,newIndex:C,newDraggableIndex:A,hideGhostForTarget:re,unhideGhostForTarget:ie,cloneNowHidden:function cloneNowHidden(){w=true},cloneNowShown:function cloneNowShown(){w=false},dispatchSortableEvent:function dispatchSortableEvent(e){_dispatchEvent({sortable:t,name:e,originalEvent:o})}},r))};function _dispatchEvent(e){dispatchEvent(_objectSpread2({putSortable:O,cloneEl:E,targetEl:v,rootEl:S,oldIndex:D,oldDraggableIndex:x,newIndex:C,newDraggableIndex:A},e))}var v,m,b,S,_,y,E,w,D,C,x,A,T,O,M,I,P,R,N,k,j,F,X,Y,B,W=false,H=false,L=[],K=false,G=false,z=[],U=false,q=[];var V=typeof document!=="undefined",$=i,Z=n||t?"cssFloat":"float",Q=V&&!a&&!i&&"draggable"in document.createElement("div"),J=function(){if(V){if(t)return false;var e=document.createElement("x");e.style.cssText="pointer-events:auto";return e.style.pointerEvents==="auto"}}(),ee=function _detectDirection(e,t){var n=css(e),o=parseInt(n.width)-parseInt(n.paddingLeft)-parseInt(n.paddingRight)-parseInt(n.borderLeftWidth)-parseInt(n.borderRightWidth),r=getChild(e,0,t),i=getChild(e,1,t),a=r&&css(r),l=i&&css(i),s=a&&parseInt(a.marginLeft)+parseInt(a.marginRight)+getRect(r).width,c=l&&parseInt(l.marginLeft)+parseInt(l.marginRight)+getRect(i).width;if(n.display==="flex")return n.flexDirection==="column"||n.flexDirection==="column-reverse"?"vertical":"horizontal";if(n.display==="grid")return n.gridTemplateColumns.split(" ").length<=1?"vertical":"horizontal";if(r&&a.float&&a.float!=="none"){var u=a.float==="left"?"left":"right";return!i||l.clear!=="both"&&l.clear!==u?"horizontal":"vertical"}return r&&(a.display==="block"||a.display==="flex"||a.display==="table"||a.display==="grid"||s>=o&&n[Z]==="none"||i&&n[Z]==="none"&&s+c>o)?"vertical":"horizontal"},te=function _dragElInRowColumn(e,t,n){var o=n?e.left:e.top,r=n?e.right:e.bottom,i=n?e.width:e.height,a=n?t.left:t.top,l=n?t.right:t.bottom,s=n?t.width:t.height;return o===a||r===l||o+i/2===a+s/2}, +/** + * Detects first nearest empty sortable to X and Y position using emptyInsertThreshold. + * @param {Number} x X position + * @param {Number} y Y position + * @return {HTMLElement} Element of the first found nearest Sortable + */ +ne=function _detectNearestEmptySortable(e,t){var n;L.some((function(o){var r=o[u].options.emptyInsertThreshold;if(r&&!lastChild(o)){var i=getRect(o),a=e>=i.left-r&&e<=i.right+r,l=t>=i.top-r&&t<=i.bottom+r;return a&&l?n=o:void 0}}));return n},oe=function _prepareGroup(e){function toFn(e,t){return function(n,o,r,i){var a=n.options.group.name&&o.options.group.name&&n.options.group.name===o.options.group.name;if(e==null&&(t||a))return true;if(e==null||e===false)return false;if(t&&e==="clone")return e;if(typeof e==="function")return toFn(e(n,o,r,i),t)(n,o,r,i);var l=(t?n:o).options.group.name;return e===true||typeof e==="string"&&e===l||e.join&&e.indexOf(l)>-1}}var t={};var n=e.group;n&&_typeof(n)=="object"||(n={name:n});t.name=n.name;t.checkPull=toFn(n.pull,true);t.checkPut=toFn(n.put);t.revertClone=n.revertClone;e.group=t},re=function _hideGhostForTarget(){!J&&b&&css(b,"display","none")},ie=function _unhideGhostForTarget(){!J&&b&&css(b,"display","")};V&&!a&&document.addEventListener("click",(function(e){if(H){e.preventDefault();e.stopPropagation&&e.stopPropagation();e.stopImmediatePropagation&&e.stopImmediatePropagation();H=false;return false}}),true);var ae=function nearestEmptyInsertDetectEvent(e){if(v){e=e.touches?e.touches[0]:e;var t=ne(e.clientX,e.clientY);if(t){var n={};for(var o in e)e.hasOwnProperty(o)&&(n[o]=e[o]);n.target=n.rootEl=t;n.preventDefault=void 0;n.stopPropagation=void 0;t[u]._onDragOver(n)}}};var le=function _checkOutsideTargetEl(e){v&&v.parentNode[u]._isOutsideThisEl(e.target)}; +/** + * @class Sortable + * @param {HTMLElement} el + * @param {Object} [options] + */function Sortable(e,t){if(!(e&&e.nodeType&&e.nodeType===1))throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(e));this.el=e;this.options=t=_extends({},t);e[u]=this;var n={group:null,sort:true,disabled:false,store:null,handle:null,draggable:/^[uo]l$/i.test(e.nodeName)?">li":">*",swapThreshold:1,invertSwap:false,invertedSwapThreshold:null,removeCloneOnHide:true,direction:function direction(){return ee(e,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:true,animation:0,easing:null,setData:function setData(e,t){e.setData("Text",t.textContent)},dropBubble:false,dragoverBubble:false,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:false,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:false,fallbackClass:"sortable-fallback",fallbackOnBody:false,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:Sortable.supportPointer!==false&&"PointerEvent"in window&&!r,emptyInsertThreshold:5};h.initializePlugins(this,e,n);for(var o in n)!(o in t)&&(t[o]=n[o]);oe(t);for(var i in this)i.charAt(0)==="_"&&typeof this[i]==="function"&&(this[i]=this[i].bind(this));this.nativeDraggable=!t.forceFallback&&Q;this.nativeDraggable&&(this.options.touchStartThreshold=1);if(t.supportPointer)on(e,"pointerdown",this._onTapStart);else{on(e,"mousedown",this._onTapStart);on(e,"touchstart",this._onTapStart)}if(this.nativeDraggable){on(e,"dragover",this);on(e,"dragenter",this)}L.push(this.el);t.store&&t.store.get&&this.sort(t.store.get(this)||[]);_extends(this,AnimationStateManager())}Sortable.prototype={constructor:Sortable,_isOutsideThisEl:function _isOutsideThisEl(e){this.el.contains(e)||e===this.el||(F=null)},_getDirection:function _getDirection(e,t){return typeof this.options.direction==="function"?this.options.direction.call(this,e,t,v):this.options.direction},_onTapStart:function _onTapStart(e){if(e.cancelable){var t=this,n=this.el,o=this.options,i=o.preventOnFilter,a=e.type,l=e.touches&&e.touches[0]||e.pointerType&&e.pointerType==="touch"&&e,s=(l||e).target,c=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||s,u=o.filter;_saveInputCheckedState(n);if(!v&&!(/mousedown|pointerdown/.test(a)&&e.button!==0||o.disabled)&&!c.isContentEditable&&(this.nativeDraggable||!r||!s||s.tagName.toUpperCase()!=="SELECT")){s=closest(s,o.draggable,n,false);if((!s||!s.animated)&&y!==s){D=index(s);x=index(s,o.draggable);if(typeof u==="function"){if(u.call(this,e,s,this)){_dispatchEvent({sortable:t,rootEl:c,name:"filter",targetEl:s,toEl:n,fromEl:n});p("filter",t,{evt:e});i&&e.cancelable&&e.preventDefault();return}}else if(u){u=u.split(",").some((function(o){o=closest(c,o.trim(),n,false);if(o){_dispatchEvent({sortable:t,rootEl:o,name:"filter",targetEl:s,fromEl:n,toEl:n});p("filter",t,{evt:e});return true}}));if(u){i&&e.cancelable&&e.preventDefault();return}}o.handle&&!closest(c,o.handle,n,false)||this._prepareDragStart(e,l,s)}}}},_prepareDragStart:function _prepareDragStart(e,r,i){var a,l=this,s=l.el,c=l.options,u=s.ownerDocument;if(i&&!v&&i.parentNode===s){var d=getRect(i);S=s;v=i;m=v.parentNode;_=v.nextSibling;y=i;T=c.group;Sortable.dragged=v;M={target:v,clientX:(r||e).clientX,clientY:(r||e).clientY};N=M.clientX-d.left;k=M.clientY-d.top;this._lastX=(r||e).clientX;this._lastY=(r||e).clientY;v.style["will-change"]="all";a=function dragStartFn(){p("delayEnded",l,{evt:e});if(Sortable.eventCanceled)l._onDrop();else{l._disableDelayedDragEvents();!o&&l.nativeDraggable&&(v.draggable=true);l._triggerDragStart(e,r);_dispatchEvent({sortable:l,name:"choose",originalEvent:e});toggleClass(v,c.chosenClass,true)}};c.ignore.split(",").forEach((function(e){find(v,e.trim(),_disableDraggable)}));on(u,"dragover",ae);on(u,"mousemove",ae);on(u,"touchmove",ae);on(u,"mouseup",l._onDrop);on(u,"touchend",l._onDrop);on(u,"touchcancel",l._onDrop);if(o&&this.nativeDraggable){this.options.touchStartThreshold=4;v.draggable=true}p("delayStart",this,{evt:e});if(!c.delay||c.delayOnTouchOnly&&!r||this.nativeDraggable&&(n||t))a();else{if(Sortable.eventCanceled){this._onDrop();return}on(u,"mouseup",l._disableDelayedDrag);on(u,"touchend",l._disableDelayedDrag);on(u,"touchcancel",l._disableDelayedDrag);on(u,"mousemove",l._delayedDragTouchMoveHandler);on(u,"touchmove",l._delayedDragTouchMoveHandler);c.supportPointer&&on(u,"pointermove",l._delayedDragTouchMoveHandler);l._dragStartTimer=setTimeout(a,c.delay)}}},_delayedDragTouchMoveHandler:function _delayedDragTouchMoveHandler(e){var t=e.touches?e.touches[0]:e;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function _disableDelayedDrag(){v&&_disableDraggable(v);clearTimeout(this._dragStartTimer);this._disableDelayedDragEvents()},_disableDelayedDragEvents:function _disableDelayedDragEvents(){var e=this.el.ownerDocument;off(e,"mouseup",this._disableDelayedDrag);off(e,"touchend",this._disableDelayedDrag);off(e,"touchcancel",this._disableDelayedDrag);off(e,"mousemove",this._delayedDragTouchMoveHandler);off(e,"touchmove",this._delayedDragTouchMoveHandler);off(e,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function _triggerDragStart(e,t){t=t||e.pointerType=="touch"&&e;if(!this.nativeDraggable||t)this.options.supportPointer?on(document,"pointermove",this._onTouchMove):on(document,t?"touchmove":"mousemove",this._onTouchMove);else{on(v,"dragend",this);on(S,"dragstart",this._onDragStart)}try{document.selection?_nextTick((function(){document.selection.empty()})):window.getSelection().removeAllRanges()}catch(e){}},_dragStarted:function _dragStarted(e,t){W=false;if(S&&v){p("dragStarted",this,{evt:t});this.nativeDraggable&&on(document,"dragover",le);var n=this.options;!e&&toggleClass(v,n.dragClass,false);toggleClass(v,n.ghostClass,true);Sortable.active=this;e&&this._appendGhost();_dispatchEvent({sortable:this,name:"start",originalEvent:t})}else this._nulling()},_emulateDragOver:function _emulateDragOver(){if(I){this._lastX=I.clientX;this._lastY=I.clientY;re();var e=document.elementFromPoint(I.clientX,I.clientY);var t=e;while(e&&e.shadowRoot){e=e.shadowRoot.elementFromPoint(I.clientX,I.clientY);if(e===t)break;t=e}v.parentNode[u]._isOutsideThisEl(e);if(t)do{if(t[u]){var n=void 0;n=t[u]._onDragOver({clientX:I.clientX,clientY:I.clientY,target:e,rootEl:t});if(n&&!this.options.dragoverBubble)break}e=t}while(t=t.parentNode);ie()}},_onTouchMove:function _onTouchMove(e){if(M){var t=this.options,n=t.fallbackTolerance,o=t.fallbackOffset,r=e.touches?e.touches[0]:e,i=b&&matrix(b,true),a=b&&i&&i.a,l=b&&i&&i.d,s=$&&B&&getRelativeScrollOffset(B),c=(r.clientX-M.clientX+o.x)/(a||1)+(s?s[0]-z[0]:0)/(a||1),u=(r.clientY-M.clientY+o.y)/(l||1)+(s?s[1]-z[1]:0)/(l||1);if(!Sortable.active&&!W){if(n&&Math.max(Math.abs(r.clientX-this._lastX),Math.abs(r.clientY-this._lastY))=0){_dispatchEvent({rootEl:m,name:"add",toEl:m,fromEl:S,originalEvent:e});_dispatchEvent({sortable:this,name:"remove",toEl:m,originalEvent:e});_dispatchEvent({rootEl:m,name:"sort",toEl:m,fromEl:S,originalEvent:e});_dispatchEvent({sortable:this,name:"sort",toEl:m,originalEvent:e})}O&&O.save()}else if(C!==D&&C>=0){_dispatchEvent({sortable:this,name:"update",toEl:m,originalEvent:e});_dispatchEvent({sortable:this,name:"sort",toEl:m,originalEvent:e})}if(Sortable.active){if(C==null||C===-1){C=D;A=x}_dispatchEvent({sortable:this,name:"end",toEl:m,originalEvent:e});this.save()}}}this._nulling()}},_nulling:function _nulling(){p("nulling",this);S=v=m=b=_=E=y=w=M=I=j=C=A=D=x=F=X=O=T=Sortable.dragged=Sortable.ghost=Sortable.clone=Sortable.active=null;q.forEach((function(e){e.checked=true}));q.length=P=R=0},handleEvent:function handleEvent(e){switch(e.type){case"drop":case"dragend":this._onDrop(e);break;case"dragenter":case"dragover":if(v){this._onDragOver(e);_globalDragOver(e)}break;case"selectstart":e.preventDefault();break}}, +/** + * Serializes the item into an array of string. + * @returns {String[]} + */ +toArray:function toArray(){var e,t=[],n=this.el.children,o=0,r=n.length,i=this.options;for(;or.right+i||e.clientY>o.bottom&&e.clientX>o.left:e.clientY>r.bottom+i||e.clientX>o.right&&e.clientY>o.top}function _getSwapDirection(e,t,n,o,r,i,a,l){var s=o?e.clientY:e.clientX,c=o?n.height:n.width,u=o?n.top:n.left,d=o?n.bottom:n.right,f=false;if(!a)if(l&&Yu+c*i/2:sd-Y)return-X}else if(s>u+c*(1-r)/2&&sd-c*i/2)?s>u+c/2?1:-1:0} +/** + * Gets the direction dragEl must be swapped relative to target in order to make it + * seem that dragEl has been "inserted" into that element's position + * @param {HTMLElement} target The target whose position dragEl is being inserted at + * @return {Number} Direction dragEl must be swapped + */function _getInsertDirection(e){return index(v)1){De.forEach((function(e){o.addAnimationState({target:e,rect:Ae?getRect(e):r});unsetRect(e);e.fromRect=r;t.removeAnimationState(e)}));Ae=false;insertMultiDragElements(!this.options.removeCloneOnHide,n)}},dragOverCompleted:function dragOverCompleted(e){var t=e.sortable,n=e.isOwner,o=e.insertion,r=e.activeSortable,i=e.parentEl,a=e.putSortable;var l=this.options;if(o){n&&r._hideClone();xe=false;if(l.animation&&De.length>1&&(Ae||!n&&!r.options.sort&&!a)){var s=getRect(ye,false,true,true);De.forEach((function(e){if(e!==ye){setRect(e,s);i.appendChild(e)}}));Ae=true}if(!n){Ae||removeMultiDragElements();if(De.length>1){var c=we;r._showClone(t);r.options.animation&&!we&&c&&Ce.forEach((function(e){r.addAnimationState({target:e,rect:Ee});e.fromRect=Ee;e.thisAnimationDuration=null}))}else r._showClone(t)}}},dragOverAnimationCapture:function dragOverAnimationCapture(e){var t=e.dragRect,n=e.isOwner,o=e.activeSortable;De.forEach((function(e){e.thisAnimationDuration=null}));if(o.options.animation&&!n&&o.multiDrag.isMultiDrag){Ee=_extends({},t);var r=matrix(ye,true);Ee.top-=r.f;Ee.left-=r.e}},dragOverAnimationComplete:function dragOverAnimationComplete(){if(Ae){Ae=false;removeMultiDragElements()}},drop:function drop(e){var t=e.originalEvent,n=e.rootEl,o=e.parentEl,r=e.sortable,i=e.dispatchSortableEvent,a=e.oldIndex,l=e.putSortable;var s=l||this.sortable;if(t){var c=this.options,d=o.children;if(!Te){c.multiDragKey&&!this.multiDragKeyDown&&this._deselectMultiDrag();toggleClass(ye,c.selectedClass,!~De.indexOf(ye));if(~De.indexOf(ye)){De.splice(De.indexOf(ye),1);Se=null;dispatchEvent({sortable:r,rootEl:n,name:"deselect",targetEl:ye,originalEvent:t})}else{De.push(ye);dispatchEvent({sortable:r,rootEl:n,name:"select",targetEl:ye,originalEvent:t});if(t.shiftKey&&Se&&r.el.contains(Se)){var f=index(Se),h=index(ye);if(~f&&~h&&f!==h){var g,p;if(h>f){p=f;g=h}else{p=h;g=f+1}for(;p1){var v=getRect(ye),m=index(ye,":not(."+this.options.selectedClass+")");!xe&&c.animation&&(ye.thisAnimationDuration=null);s.captureAnimationState();if(!xe){if(c.animation){ye.fromRect=v;De.forEach((function(e){e.thisAnimationDuration=null;if(e!==ye){var t=Ae?getRect(e):v;e.fromRect=t;s.addAnimationState({target:e,rect:t})}}))}removeMultiDragElements();De.forEach((function(e){d[m]?o.insertBefore(e,d[m]):o.appendChild(e);m++}));if(a===index(ye)){var b=false;De.forEach((function(e){e.sortableIndex===index(e)||(b=true)}));if(b){i("update");i("sort")}}}De.forEach((function(e){unsetRect(e)}));s.animateAll()}_e=s}(n===o||l&&l.lastPutMode!=="clone")&&Ce.forEach((function(e){e.parentNode&&e.parentNode.removeChild(e)}))}},nullingGlobal:function nullingGlobal(){this.isMultiDrag=Te=false;Ce.length=0},destroyGlobal:function destroyGlobal(){this._deselectMultiDrag();off(document,"pointerup",this._deselectMultiDrag);off(document,"mouseup",this._deselectMultiDrag);off(document,"touchend",this._deselectMultiDrag);off(document,"keydown",this._checkKeyDown);off(document,"keyup",this._checkKeyUp)},_deselectMultiDrag:function _deselectMultiDrag(e){if((typeof Te==="undefined"||!Te)&&_e===this.sortable&&(!e||!closest(e.target,this.options.draggable,this.sortable.el,false))&&(!e||e.button===0))while(De.length){var t=De[0];toggleClass(t,this.options.selectedClass,false);De.shift();dispatchEvent({sortable:this.sortable,rootEl:this.sortable.el,name:"deselect",targetEl:t,originalEvent:e})}},_checkKeyDown:function _checkKeyDown(e){e.key===this.options.multiDragKey&&(this.multiDragKeyDown=true)},_checkKeyUp:function _checkKeyUp(e){e.key===this.options.multiDragKey&&(this.multiDragKeyDown=false)}};return _extends(MultiDrag,{pluginName:"multiDrag",utils:{ +/** + * Selects the provided multi-drag item + * @param {HTMLElement} el The element to be selected + */ +select:function select(e){var t=e.parentNode[u];if(t&&t.options.multiDrag&&!~De.indexOf(e)){if(_e&&_e!==t){_e.multiDrag._deselectMultiDrag();_e=t}toggleClass(e,t.options.selectedClass,true);De.push(e)}}, +/** + * Deselects the provided multi-drag item + * @param {HTMLElement} el The element to be deselected + */ +deselect:function deselect(e){var t=e.parentNode[u],n=De.indexOf(e);if(t&&t.options.multiDrag&&~n){toggleClass(e,t.options.selectedClass,false);De.splice(n,1)}}},eventProperties:function eventProperties(){var e=this;var t=[],n=[];De.forEach((function(o){t.push({multiDragElement:o,index:o.sortableIndex});var r;r=Ae&&o!==ye?-1:Ae?index(o,":not(."+e.options.selectedClass+")"):index(o);n.push({multiDragElement:o,index:r})}));return{items:_toConsumableArray(De),clones:[].concat(Ce),oldIndicies:t,newIndicies:n}},optionListeners:{multiDragKey:function multiDragKey(e){e=e.toLowerCase();e==="ctrl"?e="Control":e.length>1&&(e=e.charAt(0).toUpperCase()+e.substr(1));return e}}})}function insertMultiDragElements(e,t){De.forEach((function(n,o){var r=t.children[n.sortableIndex+(e?Number(o):0)];r?t.insertBefore(n,r):t.appendChild(n)}))} +/** + * Insert multi-drag clones + * @param {[Boolean]} elementsInserted Whether the multi-drag elements are inserted + * @param {HTMLElement} rootEl + */function insertMultiDragClones(e,t){Ce.forEach((function(n,o){var r=t.children[n.sortableIndex+(e?Number(o):0)];r?t.insertBefore(n,r):t.appendChild(n)}))}function removeMultiDragElements(){De.forEach((function(e){e!==ye&&e.parentNode&&e.parentNode.removeChild(e)}))}Sortable.mount(new AutoScrollPlugin);Sortable.mount(Remove,Revert);export{MultiDragPlugin as MultiDrag,Sortable,SwapPlugin as Swap,Sortable as default}; + diff --git a/vendor/javascript/stimulus-use.js b/vendor/javascript/stimulus-use.js new file mode 100644 index 0000000..a9f00fc --- /dev/null +++ b/vendor/javascript/stimulus-use.js @@ -0,0 +1,2 @@ +import{Controller as e}from"@hotwired/stimulus";const method=(e,t)=>{const s=e[t];return typeof s=="function"?s:(...e)=>{}};const composeEventName=(e,t,s)=>{let n=e;s===true?n=`${t.identifier}:${e}`:typeof s==="string"&&(n=`${s}:${e}`);return n};const extendedEvent=(e,t,s)=>{const{bubbles:n,cancelable:o,composed:i}=t||{bubbles:true,cancelable:true,composed:true};t&&Object.assign(s,{originalEvent:t});const r=new CustomEvent(e,{bubbles:n,cancelable:o,composed:i,detail:s});return r};function isElementInViewport(e){const t=e.getBoundingClientRect();const s=window.innerHeight||document.documentElement.clientHeight;const n=window.innerWidth||document.documentElement.clientWidth;const o=t.top<=s&&t.top+t.height>0;const i=t.left<=n&&t.left+t.width>0;return o&&i}function camelize(e){return e.replace(/(?:[_-])([a-z0-9])/g,((e,t)=>t.toUpperCase()))}function __rest(e,t){var s={};for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&t.indexOf(n)<0&&(s[n]=e[n]);if(e!=null&&typeof Object.getOwnPropertySymbols==="function"){var o=0;for(n=Object.getOwnPropertySymbols(e);o{if(this.debug){this.logger.groupCollapsed(`%c${this.controller.identifier} %c#${e}`,"color: #3B82F6","color: unset");this.logger.log(Object.assign({controllerId:this.controllerId},t));this.logger.groupEnd()}};this.warn=e=>{this.logger.warn(`%c${this.controller.identifier} %c${e}`,"color: #3B82F6; font-weight: bold","color: unset")};this.dispatch=(e,t={})=>{if(this.dispatchEvent){const{event:s}=t,n=__rest(t,["event"]);const o=this.extendedEvent(e,s||null,n);this.targetElement.dispatchEvent(o);this.log("dispatchEvent",Object.assign({eventName:o.type},n))}};this.call=(e,t={})=>{const s=this.controller[e];if(typeof s=="function")return s.call(this.controller,t)};this.extendedEvent=(e,t,s)=>{const{bubbles:n,cancelable:o,composed:i}=t||{bubbles:true,cancelable:true,composed:true};t&&Object.assign(s,{originalEvent:t});const r=new CustomEvent(this.composeEventName(e),{bubbles:n,cancelable:o,composed:i,detail:s});return r};this.composeEventName=e=>{let t=e;this.eventPrefix===true?t=`${this.controller.identifier}:${e}`:typeof this.eventPrefix==="string"&&(t=`${this.eventPrefix}:${e}`);return t};this.debug=(o=(n=s===null||s===void 0?void 0:s.debug)!==null&&n!==void 0?n:e.application.stimulusUseDebug)!==null&&o!==void 0?o:t.debug;this.logger=(i=s===null||s===void 0?void 0:s.logger)!==null&&i!==void 0?i:t.logger;this.controller=e;this.controllerId=e.element.id||e.element.dataset.id;this.targetElement=(s===null||s===void 0?void 0:s.element)||e.element;const{dispatchEvent:r,eventPrefix:l}=Object.assign({},t,s);Object.assign(this,{dispatchEvent:r,eventPrefix:l});this.controllerInitialize=e.initialize.bind(e);this.controllerConnect=e.connect.bind(e);this.controllerDisconnect=e.disconnect.bind(e)}}const s={eventPrefix:true,bubbles:true,cancelable:true};class UseDispatch extends StimulusUse{constructor(e,t={}){var n,o,i,r;super(e,t);this.dispatch=(e,t={})=>{const{controller:s,targetElement:n,eventPrefix:o,bubbles:i,cancelable:r,log:l,warn:c}=this;Object.assign(t,{controller:s});const a=composeEventName(e,this.controller,o);const u=new CustomEvent(a,{detail:t,bubbles:i,cancelable:r});n.dispatchEvent(u);c("`useDispatch()` is deprecated. Please use the built-in `this.dispatch()` function from Stimulus. You can find more information on how to upgrade at: https://stimulus-use.github.io/stimulus-use/#/use-dispatch");l("dispatch",{eventName:a,detail:t,bubbles:i,cancelable:r});return u};this.targetElement=(n=t.element)!==null&&n!==void 0?n:e.element;this.eventPrefix=(o=t.eventPrefix)!==null&&o!==void 0?o:s.eventPrefix;this.bubbles=(i=t.bubbles)!==null&&i!==void 0?i:s.bubbles;this.cancelable=(r=t.cancelable)!==null&&r!==void 0?r:s.cancelable;this.enhanceController()}enhanceController(){Object.assign(this.controller,{dispatch:this.dispatch})}}const useDispatch=(e,t={})=>new UseDispatch(e,t);const n={overwriteDispatch:true};const useApplication=(e,t={})=>{const{overwriteDispatch:s}=Object.assign({},n,t);Object.defineProperty(e,"isPreview",{get(){return document.documentElement.hasAttribute("data-turbolinks-preview")||document.documentElement.hasAttribute("data-turbo-preview")}});Object.defineProperty(e,"isConnected",{get(){return!!Array.from(this.context.module.connectedContexts).find((e=>e===this.context))}});Object.defineProperty(e,"csrfToken",{get(){return this.metaValue("csrf-token")}});s&&useDispatch(e,t);Object.assign(e,{metaValue(e){const t=document.head.querySelector(`meta[name="${e}"]`);return t&&t.getAttribute("content")}})};class ApplicationController extends e{constructor(e){super(e);this.isPreview=false;this.isConnected=false;this.csrfToken="";useApplication(this,this.options)}}const o={events:["click","touchend"],onlyVisible:true,dispatchEvent:true,eventPrefix:true};const useClickOutside=(e,t={})=>{const s=e;const{onlyVisible:n,dispatchEvent:i,events:r,eventPrefix:l}=Object.assign({},o,t);const onEvent=e=>{const o=(t===null||t===void 0?void 0:t.element)||s.element;if(!(o.contains(e.target)||!isElementInViewport(o)&&n)){s.clickOutside&&s.clickOutside(e);if(i){const t=composeEventName("click:outside",s,l);const n=extendedEvent(t,e,{controller:s});o.dispatchEvent(n)}}};const observe=()=>{r===null||r===void 0?void 0:r.forEach((e=>{window.addEventListener(e,onEvent,true)}))};const unobserve=()=>{r===null||r===void 0?void 0:r.forEach((e=>{window.removeEventListener(e,onEvent,true)}))};const c=s.disconnect.bind(s);Object.assign(s,{disconnect(){unobserve();c()}});observe();return[observe,unobserve]};class ClickOutsideComposableController extends e{}class ClickOutsideController extends ClickOutsideComposableController{constructor(e){super(e);requestAnimationFrame((()=>{const[e,t]=useClickOutside(this,this.options);Object.assign(this,{observe:e,unobserve:t})}))}}class DebounceController extends e{}DebounceController.debounces=[];const i=200;const debounce=(e,t=i)=>{let s=null;return function(){const n=Array.from(arguments);const o=this;const i=n.map((e=>e.params));const callback=()=>{n.forEach(((e,t)=>e.params=i[t]));return e.apply(o,n)};s&&clearTimeout(s);s=setTimeout(callback,t)}};const useDebounce=(e,t)=>{const s=e;const n=s.constructor;n.debounces.forEach((e=>{typeof e==="string"&&(s[e]=debounce(s[e],t===null||t===void 0?void 0:t.wait));if(typeof e==="object"){const{name:n,wait:o}=e;if(!n)return;s[n]=debounce(s[n],o||(t===null||t===void 0?void 0:t.wait))}}))};class UseHover extends StimulusUse{constructor(e,t={}){super(e,t);this.observe=()=>{this.targetElement.addEventListener("mouseenter",this.onEnter);this.targetElement.addEventListener("mouseleave",this.onLeave)};this.unobserve=()=>{this.targetElement.removeEventListener("mouseenter",this.onEnter);this.targetElement.removeEventListener("mouseleave",this.onLeave)};this.onEnter=e=>{this.call("mouseEnter",e);this.log("mouseEnter",{hover:true});this.dispatch("mouseEnter",{hover:false})};this.onLeave=e=>{this.call("mouseLeave",e);this.log("mouseLeave",{hover:false});this.dispatch("mouseLeave",{hover:false})};this.controller=e;this.enhanceController();this.observe()}enhanceController(){const e=this.controller.disconnect.bind(this.controller);const disconnect=()=>{this.unobserve();e()};Object.assign(this.controller,{disconnect:disconnect})}}const useHover=(e,t={})=>{const s=e;const n=new UseHover(s,t);return[n.observe,n.unobserve]};class HoverComposableController extends e{}class HoverController extends HoverComposableController{constructor(e){super(e);requestAnimationFrame((()=>{const[e,t]=useHover(this,this.options);Object.assign(this,{observe:e,unobserve:t})}))}}const r=["mousemove","mousedown","resize","keydown","touchstart","wheel"];const l=6e4;const c={ms:l,initialState:false,events:r,dispatchEvent:true,eventPrefix:true};const useIdle=(e,t={})=>{const s=e;const{ms:n,initialState:o,events:i,dispatchEvent:r,eventPrefix:l}=Object.assign({},c,t);let a=o;let u=setTimeout((()=>{a=true;dispatchAway()}),n);const dispatchAway=e=>{const t=composeEventName("away",s,l);s.isIdle=true;method(s,"away").call(s,e);if(r){const n=extendedEvent(t,e||null,{controller:s});s.element.dispatchEvent(n)}};const dispatchBack=e=>{const t=composeEventName("back",s,l);s.isIdle=false;method(s,"back").call(s,e);if(r){const n=extendedEvent(t,e||null,{controller:s});s.element.dispatchEvent(n)}};const onEvent=e=>{a&&dispatchBack(e);a=false;clearTimeout(u);u=setTimeout((()=>{a=true;dispatchAway(e)}),n)};const onVisibility=e=>{document.hidden||onEvent(e)};a?dispatchAway():dispatchBack();const h=s.disconnect.bind(s);const observe=()=>{i.forEach((e=>{window.addEventListener(e,onEvent)}));document.addEventListener("visibilitychange",onVisibility)};const unobserve=()=>{clearTimeout(u);i.forEach((e=>{window.removeEventListener(e,onEvent)}));document.removeEventListener("visibilitychange",onVisibility)};Object.assign(s,{disconnect(){unobserve();h()}});observe();return[observe,unobserve]};class IdleComposableController extends e{constructor(){super(...arguments);this.isIdle=false}}class IdleController extends IdleComposableController{constructor(e){super(e);requestAnimationFrame((()=>{const[e,t]=useIdle(this,this.options);Object.assign(this,{observe:e,unobserve:t})}))}}const a={dispatchEvent:true,eventPrefix:true,visibleAttribute:"isVisible"};const useIntersection=(e,t={})=>{const s=e;const{dispatchEvent:n,eventPrefix:o,visibleAttribute:i}=Object.assign({},a,t);const r=(t===null||t===void 0?void 0:t.element)||s.element;s.intersectionElements||(s.intersectionElements=[]);s.intersectionElements.push(r);const callback=e=>{const[t]=e;t.isIntersecting?dispatchAppear(t):r.hasAttribute(i)&&dispatchDisappear(t)};const l=new IntersectionObserver(callback,t);const dispatchAppear=e=>{r.setAttribute(i,"true");method(s,"appear").call(s,e,l);if(n){const t=composeEventName("appear",s,o);const n=extendedEvent(t,null,{controller:s,entry:e,observer:l});r.dispatchEvent(n)}};const dispatchDisappear=e=>{r.removeAttribute(i);method(s,"disappear").call(s,e,l);if(n){const t=composeEventName("disappear",s,o);const n=extendedEvent(t,null,{controller:s,entry:e,observer:l});r.dispatchEvent(n)}};const c=s.disconnect.bind(s);const disconnect=()=>{unobserve();c()};const observe=()=>{l.observe(r)};const unobserve=()=>{l.unobserve(r)};const noneVisible=()=>s.intersectionElements.filter((e=>e.hasAttribute(i))).length===0;const oneVisible=()=>s.intersectionElements.filter((e=>e.hasAttribute(i))).length===1;const atLeastOneVisible=()=>s.intersectionElements.some((e=>e.hasAttribute(i)));const allVisible=()=>s.intersectionElements.every((e=>e.hasAttribute(i)));const u=allVisible;Object.assign(s,{isVisible:u,noneVisible:noneVisible,oneVisible:oneVisible,atLeastOneVisible:atLeastOneVisible,allVisible:allVisible,disconnect:disconnect});observe();return[observe,unobserve]};class IntersectionComposableController extends e{}class IntersectionController extends IntersectionComposableController{constructor(e){super(e);requestAnimationFrame((()=>{const[e,t]=useIntersection(this,this.options);Object.assign(this,{observe:e,unobserve:t})}))}}const useLazyLoad=(e,t)=>{const callback=t=>{const[s]=t;s.isIntersecting&&!e.isLoaded&&handleAppear()};const handleAppear=t=>{const s=e.data.get("src");if(!s)return;const n=e.element;e.isLoading=true;method(e,"loading").call(e,s);n.onload=()=>{handleLoaded(s)};n.src=s};const handleLoaded=t=>{e.isLoading=false;e.isLoaded=true;method(e,"loaded").call(e,t)};const s=e.disconnect.bind(e);const n=new IntersectionObserver(callback,t);const observe=()=>{n.observe(e.element)};const unobserve=()=>{n.unobserve(e.element)};Object.assign(e,{isVisible:false,disconnect(){unobserve();s()}});observe();return[observe,unobserve]};class LazyLoadComposableController extends e{constructor(){super(...arguments);this.isLoading=false;this.isLoaded=false}}class LazyLoadController extends LazyLoadComposableController{constructor(e){super(e);this.options={rootMargin:"10%"};requestAnimationFrame((()=>{const[e,t]=useLazyLoad(this,this.options);Object.assign(this,{observe:e,unobserve:t})}))}}const u={mediaQueries:{},dispatchEvent:true,eventPrefix:true,debug:false};class UseMatchMedia extends StimulusUse{constructor(e,t={}){var s,n,o,i;super(e,t);this.matches=[];this.callback=e=>{const t=Object.keys(this.mediaQueries).find((t=>this.mediaQueries[t]===e.media));if(!t)return;const{media:s,matches:n}=e;this.changed({name:t,media:s,matches:n,event:e})};this.changed=e=>{const{name:t}=e;if(e.event){this.call(camelize(`${t}_changed`),e);this.dispatch(`${t}:changed`,e);this.log(`media query "${t}" changed`,e)}if(e.matches){this.call(camelize(`is_${t}`),e);this.dispatch(`is:${t}`,e)}else{this.call(camelize(`not_${t}`),e);this.dispatch(`not:${t}`,e)}};this.observe=()=>{Object.keys(this.mediaQueries).forEach((e=>{const t=this.mediaQueries[e];const s=window.matchMedia(t);s.addListener(this.callback);this.matches.push(s);this.changed({name:e,media:t,matches:s.matches})}))};this.unobserve=()=>{this.matches.forEach((e=>e.removeListener(this.callback)))};this.controller=e;this.mediaQueries=(s=t.mediaQueries)!==null&&s!==void 0?s:u.mediaQueries;this.dispatchEvent=(n=t.dispatchEvent)!==null&&n!==void 0?n:u.dispatchEvent;this.eventPrefix=(o=t.eventPrefix)!==null&&o!==void 0?o:u.eventPrefix;this.debug=(i=t.debug)!==null&&i!==void 0?i:u.debug;if(window.matchMedia){this.enhanceController();this.observe()}else console.error("window.matchMedia() is not available")}enhanceController(){const e=this.controller.disconnect.bind(this.controller);const disconnect=()=>{this.unobserve();e()};Object.assign(this.controller,{disconnect:disconnect})}}const useMatchMedia=(e,t={})=>{const s=new UseMatchMedia(e,t);return[s.observe,s.unobserve]};const memoize=(e,t,s)=>{Object.defineProperty(e,t,{value:s});return s};const useMemo=e=>{var t;(t=e.constructor.memos)===null||t===void 0?void 0:t.forEach((t=>{memoize(e,t,e[t])}))};const defineMetaGetter=(e,t,s)=>{const n=s?`${camelize(t)}Meta`:camelize(t);Object.defineProperty(e,n,{get(){return typeCast(metaValue(t))}})};function metaValue(e){const t=document.head.querySelector(`meta[name="${e}"]`);return t&&t.getAttribute("content")}function typeCast(e){try{return JSON.parse(e)}catch(t){return e}}const useMeta=(e,t={suffix:true})=>{const s=e.constructor.metaNames;const n=t.suffix;s===null||s===void 0?void 0:s.forEach((t=>{defineMetaGetter(e,t,n)}));Object.defineProperty(e,"metas",{get(){const e={};s===null||s===void 0?void 0:s.forEach((t=>{const s=typeCast(metaValue(t));s!==void 0&&s!==null&&(e[camelize(t)]=s)}));return e}})};class UseMutation extends StimulusUse{constructor(e,t={}){super(e,t);this.observe=()=>{try{this.observer.observe(this.targetElement,this.options)}catch(e){this.controller.application.handleError(e,"At a minimum, one of childList, attributes, and/or characterData must be true",{})}};this.unobserve=()=>{this.observer.disconnect()};this.mutation=e=>{this.call("mutate",e);this.log("mutate",{entries:e});this.dispatch("mutate",{entries:e})};this.targetElement=(t===null||t===void 0?void 0:t.element)||e.element;this.controller=e;this.options=t;this.observer=new MutationObserver(this.mutation);this.enhanceController();this.observe()}enhanceController(){const e=this.controller.disconnect.bind(this.controller);const disconnect=()=>{this.unobserve();e()};Object.assign(this.controller,{disconnect:disconnect})}}const useMutation=(e,t={})=>{const s=new UseMutation(e,t);return[s.observe,s.unobserve]};class MutationComposableController extends e{}class MutationController extends MutationComposableController{constructor(e){super(e);requestAnimationFrame((()=>{const[e,t]=useMutation(this,this.options);Object.assign(this,{observe:e,unobserve:t})}))}}const h={dispatchEvent:true,eventPrefix:true};const useResize=(e,t={})=>{const s=e;const{dispatchEvent:n,eventPrefix:o}=Object.assign({},h,t);const i=(t===null||t===void 0?void 0:t.element)||s.element;const callback=e=>{const[t]=e;method(s,"resize").call(s,t.contentRect);if(n){const e=composeEventName("resize",s,o);const n=extendedEvent(e,null,{controller:s,entry:t});i.dispatchEvent(n)}};const r=s.disconnect.bind(s);const l=new ResizeObserver(callback);const observe=()=>{l.observe(i)};const unobserve=()=>{l.unobserve(i)};Object.assign(s,{disconnect(){unobserve();r()}});observe();return[observe,unobserve]};class ResizeComposableController extends e{}class ResizeController extends ResizeComposableController{constructor(e){super(e);requestAnimationFrame((()=>{const[e,t]=useResize(this,this.options);Object.assign(this,{observe:e,unobserve:t})}))}}class UseTargetMutation extends StimulusUse{constructor(e,t={}){super(e,t);this.observe=()=>{this.observer.observe(this.targetElement,{subtree:true,characterData:true,childList:true,attributes:true,attributeOldValue:true,attributeFilter:[this.targetSelector,this.scopedTargetSelector]})};this.unobserve=()=>{this.observer.disconnect()};this.mutation=e=>{for(const t of e)switch(t.type){case"attributes":let e=t.target.getAttribute(t.attributeName);let s=t.oldValue;if(t.attributeName===this.targetSelector||t.attributeName===this.scopedTargetSelector){let n=this.targetsUsedByThisController(s);let o=this.targetsUsedByThisController(e);let i=n.filter((e=>!o.includes(e)));let r=o.filter((e=>!n.includes(e)));i.forEach((e=>this.targetRemoved(this.stripIdentifierPrefix(e),t.target,"attributeChange")));r.forEach((e=>this.targetAdded(this.stripIdentifierPrefix(e),t.target,"attributeChange")))}break;case"characterData":let n=this.findTargetInAncestry(t.target);if(n==null)return;{let e=this.targetsUsedByThisControllerFromNode(n);e.forEach((e=>{this.targetChanged(this.stripIdentifierPrefix(e),n,"domMutation")}))}break;case"childList":let{addedNodes:o,removedNodes:i}=t;o.forEach((e=>this.processNodeDOMMutation(e,this.targetAdded)));i.forEach((e=>this.processNodeDOMMutation(e,this.targetRemoved)));break}};this.controller=e;this.options=t;this.targetElement=e.element;this.identifier=e.scope.identifier;this.identifierPrefix=`${this.identifier}.`;this.targetSelector=e.scope.schema.targetAttribute;this.scopedTargetSelector=`data-${this.identifier}-target`;this.targets=t.targets||e.constructor.targets;this.prefixedTargets=this.targets.map((e=>`${this.identifierPrefix}${e}`));this.observer=new MutationObserver(this.mutation);this.enhanceController();this.observe()}processNodeDOMMutation(e,t){let s=e;let n=t;let o=[];if(s.nodeName=="#text"||this.targetsUsedByThisControllerFromNode(s).length==0){n=this.targetChanged;s=this.findTargetInAncestry(e)}else o=this.targetsUsedByThisControllerFromNode(s);if(s!=null){o.length==0&&(o=this.targetsUsedByThisControllerFromNode(s));o.forEach((e=>{n.call(this,this.stripIdentifierPrefix(e),s,"domMutation")}))}}findTargetInAncestry(e){let t=e;let s=[];t.nodeName!="#text"&&(s=this.targetsUsedByThisControllerFromNode(t));while(t.parentNode!==null&&t.parentNode!=this.targetElement&&s.length==0){t=t.parentNode;if(t.nodeName!=="#text"){let e=this.targetsUsedByThisControllerFromNode(t);if(e.length>0)return t}}return t.nodeName=="#text"||t.parentNode==null?null:t.parentNode==this.targetElement&&this.targetsUsedByThisControllerFromNode(t).length>0?t:null}targetAdded(e,t,s){let n=`${e}TargetAdded`;this.controller[n]&&method(this.controller,n).call(this.controller,t);this.log("targetAdded",{target:e,node:t,trigger:s})}targetRemoved(e,t,s){let n=`${e}TargetRemoved`;this.controller[n]&&method(this.controller,n).call(this.controller,t);this.log("targetRemoved",{target:e,node:t,trigger:s})}targetChanged(e,t,s){let n=`${e}TargetChanged`;this.controller[n]&&method(this.controller,n).call(this.controller,t);this.log("targetChanged",{target:e,node:t,trigger:s})}targetsUsedByThisControllerFromNode(e){if(e.nodeName=="#text"||e.nodeName=="#comment")return[];let t=e;return this.targetsUsedByThisController(t.getAttribute(this.scopedTargetSelector)||t.getAttribute(this.targetSelector))}targetsUsedByThisController(e){e=e||"";let t=this.stripIdentifierPrefix(e).split(" ");return this.targets.filter((e=>t.indexOf(e)!==-1))}stripIdentifierPrefix(e){return e.replace(new RegExp(this.identifierPrefix,"g"),"")}enhanceController(){const e=this.controller.disconnect.bind(this.controller);const disconnect=()=>{this.unobserve();e()};Object.assign(this.controller,{disconnect:disconnect})}}const useTargetMutation=(e,t={})=>{const s=e;const n=new UseTargetMutation(s,t);return[n.observe,n.unobserve]};class TargetMutationComposableController extends e{}class TargetMutationController extends TargetMutationComposableController{constructor(e){super(e);requestAnimationFrame((()=>{const[e,t]=useTargetMutation(this,this.options);Object.assign(this,{observe:e,unobserve:t})}))}}class ThrottleController extends e{}ThrottleController.throttles=[];const d=200;function throttle(e,t=d){let s;return function(){const n=arguments;const o=this;if(!s){s=true;e.apply(o,n);setTimeout((()=>s=false),t)}}}const useThrottle=(e,t={})=>{var s;const n=e;const o=n.constructor;(s=o.throttles)===null||s===void 0?void 0:s.forEach((e=>{typeof e==="string"&&(n[e]=throttle(n[e],t===null||t===void 0?void 0:t.wait));if(typeof e==="object"){const{name:s,wait:o}=e;if(!s)return;n[s]=throttle(n[s],o||(t===null||t===void 0?void 0:t.wait))}}))};const b={enterFromClass:"enter",enterActiveClass:"enterStart",enterToClass:"enterEnd",leaveFromClass:"leave",leaveActiveClass:"leaveStart",leaveToClass:"leaveEnd"};const m={transitioned:false,hiddenClass:"hidden",preserveOriginalClass:true,removeToClasses:true};const useTransition=(e,t={})=>{var s,n,o;const i=e;const r=i.element.dataset.transitionTarget;let l;r&&(l=i[`${r}Target`]);const c=(t===null||t===void 0?void 0:t.element)||l||i.element;if(!(c instanceof HTMLElement||c instanceof SVGElement))return;const a=c.dataset;const u=parseInt(a.leaveAfter||"")||t.leaveAfter||0;const{transitioned:h,hiddenClass:d,preserveOriginalClass:b,removeToClasses:v}=Object.assign({},m,t);const g=(s=i.enter)===null||s===void 0?void 0:s.bind(i);const p=(n=i.leave)===null||n===void 0?void 0:n.bind(i);const f=(o=i.toggleTransition)===null||o===void 0?void 0:o.bind(i);async function enter(e){if(i.transitioned)return;i.transitioned=true;g&&g(e);const s=getAttribute("enterFrom",t,a);const n=getAttribute("enterActive",t,a);const o=getAttribute("enterTo",t,a);const r=getAttribute("leaveTo",t,a);!d||c.classList.remove(d);v||removeClasses(c,r);await transition(c,s,n,o,d,b,v);u>0&&setTimeout((()=>{leave(e)}),u)}async function leave(e){if(!i.transitioned)return;i.transitioned=false;p&&p(e);const s=getAttribute("leaveFrom",t,a);const n=getAttribute("leaveActive",t,a);const o=getAttribute("leaveTo",t,a);const r=getAttribute("enterTo",t,a);v||removeClasses(c,r);await transition(c,s,n,o,d,b,v);!d||c.classList.add(d)}function toggleTransition(e){f&&f(e);i.transitioned?leave():enter()}async function transition(e,t,s,n,o,i,r){const l=[];if(i){t.forEach((t=>e.classList.contains(t)&&t!==o&&l.push(t)));s.forEach((t=>e.classList.contains(t)&&t!==o&&l.push(t)));n.forEach((t=>e.classList.contains(t)&&t!==o&&l.push(t)))}addClasses(e,t);removeClasses(e,l);addClasses(e,s);await nextAnimationFrame();removeClasses(e,t);addClasses(e,n);await afterTransition(e);removeClasses(e,s);r&&removeClasses(e,n);addClasses(e,l)}function initialState(){i.transitioned=h;if(h){!d||c.classList.remove(d);enter()}else{!d||c.classList.add(d);leave()}}function addClasses(e,t){t.length>0&&e.classList.add(...t)}function removeClasses(e,t){t.length>0&&e.classList.remove(...t)}initialState();Object.assign(i,{enter:enter,leave:leave,toggleTransition:toggleTransition});return[enter,leave,toggleTransition]};function getAttribute(e,t,s){const n=`transition${e[0].toUpperCase()}${e.substr(1)}`;const o=b[e];const i=t[e]||s[n]||s[o]||" ";return isEmpty(i)?[]:i.split(" ")}async function afterTransition(e){return new Promise((t=>{const s=Number(getComputedStyle(e).transitionDuration.split(",")[0].replace("s",""))*1e3;setTimeout((()=>{t(s)}),s)}))}async function nextAnimationFrame(){return new Promise((e=>{requestAnimationFrame((()=>{requestAnimationFrame(e)}))}))}function isEmpty(e){return e.length===0||!e.trim()}class TransitionComposableController extends e{constructor(){super(...arguments);this.transitioned=false}}class TransitionController extends TransitionComposableController{constructor(e){super(e);requestAnimationFrame((()=>{useTransition(this,this.options)}))}}class UseVisibility extends StimulusUse{constructor(e,t={}){super(e,t);this.observe=()=>{this.controller.isVisible=!document.hidden;document.addEventListener("visibilitychange",this.handleVisibilityChange);this.handleVisibilityChange()};this.unobserve=()=>{document.removeEventListener("visibilitychange",this.handleVisibilityChange)};this.becomesInvisible=e=>{this.controller.isVisible=false;this.call("invisible",e);this.log("invisible",{isVisible:false});this.dispatch("invisible",{event:e,isVisible:false})};this.becomesVisible=e=>{this.controller.isVisible=true;this.call("visible",e);this.log("visible",{isVisible:true});this.dispatch("visible",{event:e,isVisible:true})};this.handleVisibilityChange=e=>{document.hidden?this.becomesInvisible(e):this.becomesVisible(e)};this.controller=e;this.enhanceController();this.observe()}enhanceController(){const e=this.controllerDisconnect;const disconnect=()=>{this.unobserve();e()};Object.assign(this.controller,{disconnect:disconnect})}}const useVisibility=(e,t={})=>{const s=e;const n=new UseVisibility(s,t);return[n.observe,n.unobserve]};class VisibilityComposableController extends e{constructor(){super(...arguments);this.isVisible=false}}class VisibilityController extends VisibilityComposableController{constructor(e){super(e);requestAnimationFrame((()=>{const[e,t]=useVisibility(this,this.options);Object.assign(this,{observe:e,unobserve:t})}))}}class UseWindowFocus extends StimulusUse{constructor(e,t={}){super(e,t);this.observe=()=>{document.hasFocus()?this.becomesFocused():this.becomesUnfocused();this.interval=setInterval((()=>{this.handleWindowFocusChange()}),this.intervalDuration)};this.unobserve=()=>{clearInterval(this.interval)};this.becomesUnfocused=e=>{this.controller.hasFocus=false;this.call("unfocus",e);this.log("unfocus",{hasFocus:false});this.dispatch("unfocus",{event:e,hasFocus:false})};this.becomesFocused=e=>{this.controller.hasFocus=true;this.call("focus",e);this.log("focus",{hasFocus:true});this.dispatch("focus",{event:e,hasFocus:true})};this.handleWindowFocusChange=e=>{document.hasFocus()&&!this.controller.hasFocus?this.becomesFocused(e):!document.hasFocus()&&this.controller.hasFocus&&this.becomesUnfocused(e)};this.controller=e;this.intervalDuration=t.interval||200;this.enhanceController();this.observe()}enhanceController(){const e=this.controllerDisconnect;const disconnect=()=>{this.unobserve();e()};Object.assign(this.controller,{disconnect:disconnect})}}const useWindowFocus=(e,t={})=>{const s=e;const n=new UseWindowFocus(s,t);return[n.observe,n.unobserve]};class WindowFocusComposableController extends e{constructor(){super(...arguments);this.hasFocus=false}}class WindowFocusController extends WindowFocusComposableController{constructor(e){super(e);requestAnimationFrame((()=>{const[e,t]=useWindowFocus(this,this.options);Object.assign(this,{observe:e,unobserve:t})}))}}const useWindowResize=e=>{const t=e;const callback=e=>{const{innerWidth:s,innerHeight:n}=window;const o={height:n||Infinity,width:s||Infinity,event:e};method(t,"windowResize").call(t,o)};const s=t.disconnect.bind(t);const observe=()=>{window.addEventListener("resize",callback);callback()};const unobserve=()=>{window.removeEventListener("resize",callback)};Object.assign(t,{disconnect(){unobserve();s()}});observe();return[observe,unobserve]};class WindowResizeComposableController extends e{}class WindowResizeController extends WindowResizeComposableController{constructor(e){super(e);requestAnimationFrame((()=>{const[e,t]=useWindowResize(this);Object.assign(this,{observe:e,unobserve:t})}))}}function useHotkeys(){throw"[stimulus-use] Notice: The import for `useHotkeys()` has been moved from `stimulus-use` to `stimulus-use/hotkeys`. \nPlease change the import accordingly and add `hotkey-js` as a dependency to your project. \n\nFor more information see: https://stimulus-use.github.io/stimulus-use/#/use-hotkeys?id=importing-the-behavior"}export{ApplicationController,ClickOutsideController,HoverController,IdleController,IntersectionController,LazyLoadController,MutationController,ResizeController,TargetMutationController,TransitionController,UseHover,UseMutation,UseTargetMutation,UseVisibility,UseWindowFocus,VisibilityController,WindowFocusController,WindowResizeController,debounce,useApplication,useClickOutside,useDebounce,useDispatch,useHotkeys,useHover,useIdle,useIntersection,useLazyLoad,useMatchMedia,useMemo,useMeta,useMutation,useResize,useTargetMutation,useThrottle,useTransition,useVisibility,useWindowFocus,useWindowResize}; + diff --git a/vendor/javascript/tom-select.js b/vendor/javascript/tom-select.js new file mode 100644 index 0000000..4deef33 --- /dev/null +++ b/vendor/javascript/tom-select.js @@ -0,0 +1,234 @@ +var e="undefined"!==typeof globalThis?globalThis:"undefined"!==typeof self?self:global;var t={};(function(e,s){t=s()})(0,(function(){function forEvents(e,t){e.split(/\s+/).forEach((e=>{t(e)}))}class MicroEvent{constructor(){this._events=void 0;this._events={}}on(e,t){forEvents(e,(e=>{const s=this._events[e]||[];s.push(t);this._events[e]=s}))}off(e,t){var s=arguments.length;0!==s?forEvents(e,(e=>{if(1===s){delete this._events[e];return}const i=this._events[e];if(void 0!==i){i.splice(i.indexOf(t),1);this._events[e]=i}})):this._events={}}trigger(e,...t){var s=this;forEvents(e,(e=>{const i=s._events[e];void 0!==i&&i.forEach((e=>{e.apply(s,t)}))}))}}function MicroPlugin(e){e.plugins={};return class extends e{constructor(...e){super(...e);this.plugins={names:[],settings:{},requested:{},loaded:{}}} +/** + * Registers a plugin. + * + * @param {function} fn + */static define(t,s){e.plugins[t]={name:t,fn:s}} +/** + * Initializes the listed plugins (with options). + * Acceptable formats: + * + * List (without options): + * ['a', 'b', 'c'] + * + * List (with options): + * [{'name': 'a', options: {}}, {'name': 'b', options: {}}] + * + * Hash (with options): + * {'a': { ... }, 'b': { ... }, 'c': { ... }} + * + * @param {array|object} plugins + */initializePlugins(e){var t,s;const i=this;const n=[];if(Array.isArray(e))e.forEach((e=>{if("string"===typeof e)n.push(e);else{i.plugins.settings[e.name]=e.options;n.push(e.name)}}));else if(e)for(t in e)if(e.hasOwnProperty(t)){i.plugins.settings[t]=e[t];n.push(t)}while(s=n.shift())i.require(s)}loadPlugin(t){var s=this;var i=s.plugins;var n=e.plugins[t];if(!e.plugins.hasOwnProperty(t))throw new Error('Unable to find "'+t+'" plugin');i.requested[t]=true;i.loaded[t]=n.fn.apply(s,[s.plugins.settings[t]||{}]);i.names.push(t)}require(e){var t=this;var s=t.plugins;if(!t.plugins.loaded.hasOwnProperty(e)){if(s.requested[e])throw new Error('Plugin has circular dependency ("'+e+'")');t.loadPlugin(e)}return s.loaded[e]}}} +/** + * Convert array of strings to a regular expression + * ex ['ab','a'] => (?:ab|a) + * ex ['a','b'] => [ab] + * @param {string[]} chars + * @return {string} + */const arrayToPattern=e=>{e=e.filter(Boolean);return e.length<2?e[0]||"":1==maxValueLength(e)?"["+e.join("")+"]":"(?:"+e.join("|")+")"}; +/** + * @param {string[]} array + * @return {string} + */const sequencePattern=e=>{if(!hasDuplicates(e))return e.join("");let t="";let s=0;const prev_pattern=()=>{s>1&&(t+="{"+s+"}")};e.forEach(((i,n)=>{if(i!==e[n-1]){prev_pattern();t+=i;s=1}else s++}));prev_pattern();return t}; +/** + * Convert array of strings to a regular expression + * ex ['ab','a'] => (?:ab|a) + * ex ['a','b'] => [ab] + * @param {Set} chars + * @return {string} + */const setToPattern=e=>{let t=toArray(e);return arrayToPattern(t)}; +/** + * + * https://stackoverflow.com/questions/7376598/in-javascript-how-do-i-check-if-an-array-has-duplicate-values + * @param {any[]} array + */const hasDuplicates=e=>new Set(e).size!==e.length; +/** + * https://stackoverflow.com/questions/63006601/why-does-u-throw-an-invalid-escape-error + * @param {string} str + * @return {string} + */const escape_regex=e=>(e+"").replace(/([\$\(\)\*\+\.\?\[\]\^\{\|\}\\])/gu,"\\$1"); +/** + * Return the max length of array values + * @param {string[]} array + * + */const maxValueLength=e=>e.reduce(((e,t)=>Math.max(e,unicodeLength(t))),0); +/** + * @param {string} str + */const unicodeLength=e=>toArray(e).length; +/** + * @param {any} p + * @return {any[]} + */const toArray=e=>Array.from(e) +/** + * Get all possible combinations of substrings that add up to the given string + * https://stackoverflow.com/questions/30169587/find-all-the-combination-of-substrings-that-add-up-to-the-given-string + * @param {string} input + * @return {string[][]} + */;const allSubstrings=e=>{if(1===e.length)return[[e]]; +/** @type {string[][]} */let t=[];const s=e.substring(1);const i=allSubstrings(s);i.forEach((function(s){let i=s.slice(0);i[0]=e.charAt(0)+i[0];t.push(i);i=s.slice(0);i.unshift(e.charAt(0));t.push(i)}));return t}; +/** + * @typedef {{[key:string]:string}} TUnicodeMap + * @typedef {{[key:string]:Set}} TUnicodeSets + * @typedef {[[number,number]]} TCodePoints + * @typedef {{folded:string,composed:string,code_point:number}} TCodePointObj + * @typedef {{start:number,end:number,length:number,substr:string}} TSequencePart + */ +/** @type {TCodePoints} */const t=[[0,65535]];const s="[̀-ͯ·ʾʼ]"; +/** @type {TUnicodeMap} */let i; +/** @type {RegExp} */let n;const o=3; +/** @type {TUnicodeMap} */const r={}; +/** @type {TUnicodeMap} */const l={"/":"⁄∕",0:"߀",a:"ⱥɐɑ",aa:"ꜳ",ae:"æǽǣ",ao:"ꜵ",au:"ꜷ",av:"ꜹꜻ",ay:"ꜽ",b:"ƀɓƃ",c:"ꜿƈȼↄ",d:"đɗɖᴅƌꮷԁɦ",e:"ɛǝᴇɇ",f:"ꝼƒ",g:"ǥɠꞡᵹꝿɢ",h:"ħⱨⱶɥ",i:"ɨı",j:"ɉȷ",k:"ƙⱪꝁꝃꝅꞣ",l:"łƚɫⱡꝉꝇꞁɭ",m:"ɱɯϻ",n:"ꞥƞɲꞑᴎлԉ",o:"øǿɔɵꝋꝍᴑ",oe:"œ",oi:"ƣ",oo:"ꝏ",ou:"ȣ",p:"ƥᵽꝑꝓꝕρ",q:"ꝗꝙɋ",r:"ɍɽꝛꞧꞃ",s:"ßȿꞩꞅʂ",t:"ŧƭʈⱦꞇ",th:"þ",tz:"ꜩ",u:"ʉ",v:"ʋꝟʌ",vy:"ꝡ",w:"ⱳ",y:"ƴɏỿ",z:"ƶȥɀⱬꝣ",hv:"ƕ"};for(let e in l){let t=l[e]||"";for(let s=0;s{void 0===i&&(i=generateMap(e||t))}; +/** + * Helper method for normalize a string + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize + * @param {string} str + * @param {string} form + */const normalize=(e,t="NFKD")=>e.normalize(t) +/** + * Remove accents without reordering string + * calling str.normalize('NFKD') on \u{594}\u{595}\u{596} becomes \u{596}\u{594}\u{595} + * via https://github.com/krisk/Fuse/issues/133#issuecomment-318692703 + * @param {string} str + * @return {string} + */;const asciifold=e=>toArray(e).reduce(( +/** + * @param {string} result + * @param {string} char + */ +(e,t)=>e+_asciifold(t)),""); +/** + * @param {string} str + * @return {string} + */const _asciifold=e=>{e=normalize(e).toLowerCase().replace(a,(/** @type {string} */ +e=>r[e]||""));return normalize(e,"NFC")}; +/** + * Generate a list of unicode variants from the list of code points + * @param {TCodePoints} code_points + * @yield {TCodePointObj} + */function*generator(e){for(const[t,s]of e)for(let e=t;e<=s;e++){let t=String.fromCharCode(e);let s=asciifold(t);s!=t.toLowerCase()&&(s.length>o||0!=s.length&&(yield{folded:s,composed:t,code_point:e}))}} +/** + * Generate a unicode map from the list of code points + * @param {TCodePoints} code_points + * @return {TUnicodeSets} + */const generateSets=e=>{ +/** @type {{[key:string]:Set}} */ +const t={}; +/** + * @param {string} folded + * @param {string} to_add + */const addMatching=(e,s)=>{ +/** @type {Set} */ +const i=t[e]||new Set;const n=new RegExp("^"+setToPattern(i)+"$","iu");if(!s.match(n)){i.add(escape_regex(s));t[e]=i}};for(let t of generator(e)){addMatching(t.folded,t.folded);addMatching(t.folded,t.composed)}return t}; +/** + * Generate a unicode map from the list of code points + * ae => (?:(?:ae|Æ|Ǽ|Ǣ)|(?:A|Ⓐ|A...)(?:E|ɛ|Ⓔ...)) + * + * @param {TCodePoints} code_points + * @return {TUnicodeMap} + */const generateMap=e=>{ +/** @type {TUnicodeSets} */ +const t=generateSets(e); +/** @type {TUnicodeMap} */const s={}; +/** @type {string[]} */let i=[];for(let e in t){let n=t[e];n&&(s[e]=setToPattern(n));e.length>1&&i.push(escape_regex(e))}i.sort(((e,t)=>t.length-e.length));const o=arrayToPattern(i);n=new RegExp("^"+o,"u");return s}; +/** + * Map each element of an array from it's folded value to all possible unicode matches + * @param {string[]} strings + * @param {number} min_replacement + * @return {string} + */const mapSequence=(e,t=1)=>{let s=0;e=e.map((e=>{i[e]&&(s+=e.length);return i[e]||e}));return s>=t?sequencePattern(e):""}; +/** + * Convert a short string and split it into all possible patterns + * Keep a pattern only if min_replacement is met + * + * 'abc' + * => [['abc'],['ab','c'],['a','bc'],['a','b','c']] + * => ['abc-pattern','ab-c-pattern'...] + * + * + * @param {string} str + * @param {number} min_replacement + * @return {string} + */const substringsToPattern=(e,t=1)=>{t=Math.max(t,e.length-1);return arrayToPattern(allSubstrings(e).map((e=>mapSequence(e,t))))}; +/** + * Convert an array of sequences into a pattern + * [{start:0,end:3,length:3,substr:'iii'}...] => (?:iii...) + * + * @param {Sequence[]} sequences + * @param {boolean} all + */const sequencesToPattern=(e,t=true)=>{let s=e.length>1?1:0;return arrayToPattern(e.map((e=>{let i=[];const n=t?e.length():e.length()-1;for(let t=0;t{for(const s of t){if(s.start!=e.start||s.end!=e.end)continue;if(s.substrs.join("")!==e.substrs.join(""))continue;let t=e.parts; +/** + * @param {TSequencePart} part + */const filter=e=>{for(const s of t){if(s.start===e.start&&s.substr===e.substr)return false;if(1!=e.length&&1!=s.length){if(e.starts.start)return true;if(s.starte.start)return true}}return false};let i=s.parts.filter(filter);if(!(i.length>0))return true}return false};class Sequence{constructor(){ +/** @type {TSequencePart[]} */ +this.parts=[]; +/** @type {string[]} */this.substrs=[];this.start=0;this.end=0} +/** + * @param {TSequencePart|undefined} part + */add(e){if(e){this.parts.push(e);this.substrs.push(e.substr);this.start=Math.min(e.start,this.start);this.end=Math.max(e.end,this.end)}}last(){return this.parts[this.parts.length-1]}length(){return this.parts.length} +/** + * @param {number} position + * @param {TSequencePart} last_piece + */clone(e,t){let s=new Sequence;let i=JSON.parse(JSON.stringify(this.parts));let n=i.pop();for(const e of i)s.add(e);let o=t.substr.substring(0,e-n.start);let r=o.length;s.add({start:n.start,end:n.start+r,length:r,substr:o});return s}} +/** + * Expand a regular expression pattern to include unicode variants + * eg /a/ becomes /aⓐaẚàáâầấẫẩãāăằắẵẳȧǡäǟảåǻǎȁȃạậặḁąⱥɐɑAⒶAÀÁÂẦẤẪẨÃĀĂẰẮẴẲȦǠÄǞẢÅǺǍȀȂẠẬẶḀĄȺⱯ/ + * + * Issue: + * ﺊﺋ [ 'ﺊ = \\u{fe8a}', 'ﺋ = \\u{fe8b}' ] + * becomes: ئئ [ 'ي = \\u{64a}', 'ٔ = \\u{654}', 'ي = \\u{64a}', 'ٔ = \\u{654}' ] + * + * İIJ = IIJ = ⅡJ + * + * 1/2/4 + * + * @param {string} str + * @return {string|undefined} + */const getPattern=e=>{initialize();e=asciifold(e);let t="";let s=[new Sequence];for(let i=0;i0){c=c.sort(((e,t)=>e.length()-t.length()));for(let e of c)inSequences(e,s)||s.push(e)}else if(i>0&&1==d.size&&!d.has("3")){t+=sequencesToPattern(s,false);let e=new Sequence;const i=s[0];i&&e.add(i.last());s=[e]}}t+=sequencesToPattern(s,true);return t}; +/** + * A property getter resolving dot-notation + * @param {Object} obj The root object to fetch property on + * @param {String} name The optionally dotted property name to fetch + * @return {Object} The resolved property value + */const getAttr=(e,t)=>{if(e)return e[t]}; +/** + * A property getter resolving dot-notation + * @param {Object} obj The root object to fetch property on + * @param {String} name The optionally dotted property name to fetch + * @return {Object} The resolved property value + */const getAttrNesting=(e,t)=>{if(e){var s,i=t.split(".");while((s=i.shift())&&(e=e[s]));return e}};const scoreValue=(e,t,s)=>{var i,n;if(!e)return 0;e+="";if(null==t.regex)return 0;n=e.search(t.regex);if(-1===n)return 0;i=t.string.length/e.length;0===n&&(i+=.5);return i*s};const propToArray=(e,t)=>{var s=e[t];if("function"==typeof s)return s;s&&!Array.isArray(s)&&(e[t]=[s])};const iterate$1=(e,t)=>{if(Array.isArray(e))e.forEach(t);else for(var s in e)e.hasOwnProperty(s)&&t(e[s],s)};const cmp=(e,t)=>{if("number"===typeof e&&"number"===typeof t)return e>t?1:et?1:t>e?-1:0};class Sifter{constructor(e,t){this.items=void 0;this.settings=void 0;this.items=e;this.settings=t||{diacritics:true}}tokenize(e,t,s){if(!e||!e.length)return[];const i=[];const n=e.split(/\s+/);var o;s&&(o=new RegExp("^("+Object.keys(s).map(escape_regex).join("|")+"):(.*)$"));n.forEach((e=>{let s;let n=null;let r=null;if(o&&(s=e.match(o))){n=s[1];e=s[2]}if(e.length>0){r=this.settings.diacritics?getPattern(e)||null:escape_regex(e);r&&t&&(r="\\b"+r)}i.push({string:e,regex:r?new RegExp(r,"iu"):null,field:n})}));return i} +/** + * Returns a function to be used to score individual results. + * + * Good matches will have a higher score than poor matches. + * If an item is not a match, 0 will be returned by the function. + * + * @returns {T.ScoreFn} + */getScoreFunction(e,t){var s=this.prepareSearch(e,t);return this._getScoreFunction(s)} +/** + * @returns {T.ScoreFn} + * + */_getScoreFunction(e){const t=e.tokens,s=t.length;if(!s)return function(){return 0};const i=e.options.fields,n=e.weights,o=i.length,r=e.getAttrFn;if(!o)return function(){return 1};const l=function(){return 1===o?function(e,t){const s=i[0].field;return scoreValue(r(t,s),e,n[s]||1)}:function(e,t){var s=0;if(e.field){const i=r(t,e.field);!e.regex&&i?s+=1/o:s+=scoreValue(i,e,1)}else iterate$1(n,((i,n)=>{s+=scoreValue(r(t,n),e,i)}));return s/o}}();return 1===s?function(e){return l(t[0],e)}:"and"===e.options.conjunction?function(e){var i,n=0;for(let s of t){i=l(s,e);if(i<=0)return 0;n+=i}return n/s}:function(e){var i=0;iterate$1(t,(t=>{i+=l(t,e)}));return i/s}}getSortFunction(e,t){var s=this.prepareSearch(e,t);return this._getSortFunction(s)}_getSortFunction(e){var t,s=[];const i=this,n=e.options,o=!e.query&&n.sort_empty?n.sort_empty:n.sort;if("function"==typeof o)return o.bind(this);const r=function get_field(t,s){return"$score"===t?s.score:e.getAttrFn(i.items[s.id],t)};if(o)for(let t of o)(e.query||"$score"!==t.field)&&s.push(t);if(e.query){t=true;for(let e of s)if("$score"===e.field){t=false;break}t&&s.unshift({field:"$score",direction:"desc"})}else s=s.filter((e=>"$score"!==e.field));const l=s.length;return l?function(e,t){var i,n;for(let o of s){n=o.field;let s="desc"===o.direction?-1:1;i=s*cmp(r(n,e),r(n,t));if(i)return i}return 0}:null}prepareSearch(e,t){const s={};var i=Object.assign({},t);propToArray(i,"sort");propToArray(i,"sort_empty");if(i.fields){propToArray(i,"fields");const e=[];i.fields.forEach((t=>{"string"==typeof t&&(t={field:t,weight:1});e.push(t);s[t.field]="weight"in t?t.weight:1}));i.fields=e}return{options:i,query:e.toLowerCase().trim(),tokens:this.tokenize(e,i.respect_word_boundaries,s),total:0,items:[],weights:s,getAttrFn:i.nesting?getAttrNesting:getAttr}}search(e,t){var s,i,n=this;i=this.prepareSearch(e,t);t=i.options;e=i.query;const o=t.score||n._getScoreFunction(i);e.length?iterate$1(n.items,((e,n)=>{s=o(e);(false===t.filter||s>0)&&i.items.push({score:s,id:n})})):iterate$1(n.items,((e,t)=>{i.items.push({score:1,id:t})}));const r=n._getSortFunction(i);r&&i.items.sort(r);i.total=i.items.length;"number"===typeof t.limit&&(i.items=i.items.slice(0,t.limit));return i}}const iterate=(e,t)=>{if(Array.isArray(e))e.forEach(t);else for(var s in e)e.hasOwnProperty(s)&&t(e[s],s)};const getDom=e=>{if(e.jquery)return e[0];if(e instanceof HTMLElement)return e;if(isHtmlString(e)){var t=document.createElement("template");t.innerHTML=e.trim();return t.content.firstChild}return document.querySelector(e)};const isHtmlString=e=>"string"===typeof e&&e.indexOf("<")>-1;const escapeQuery=e=>e.replace(/['"\\]/g,"\\$&");const triggerEvent=(e,t)=>{var s=document.createEvent("HTMLEvents");s.initEvent(t,true,false);e.dispatchEvent(s)};const applyCSS=(e,t)=>{Object.assign(e.style,t)};const addClasses=(e,...t)=>{var s=classesArray(t);e=castAsArray(e);e.map((e=>{s.map((t=>{e.classList.add(t)}))}))};const removeClasses=(e,...t)=>{var s=classesArray(t);e=castAsArray(e);e.map((e=>{s.map((t=>{e.classList.remove(t)}))}))};const classesArray=e=>{var t=[];iterate(e,(e=>{"string"===typeof e&&(e=e.trim().split(/[\11\12\14\15\40]/));Array.isArray(e)&&(t=t.concat(e))}));return t.filter(Boolean)};const castAsArray=e=>{Array.isArray(e)||(e=[e]);return e};const parentMatch=(e,t,s)=>{if(!s||s.contains(e))while(e&&e.matches){if(e.matches(t))return e;e=e.parentNode}};const getTail=(e,t=0)=>t>0?e[e.length-1]:e[0];const isEmptyObject=e=>0===Object.keys(e).length;const nodeIndex=(e,t)=>{if(!e)return-1;t=t||e.nodeName;var s=0;while(e=e.previousElementSibling)e.matches(t)&&s++;return s};const setAttr=(e,t)=>{iterate(t,((t,s)=>{null==t?e.removeAttribute(s):e.setAttribute(s,""+t)}))};const replaceNode=(e,t)=>{e.parentNode&&e.parentNode.replaceChild(t,e)};const highlight=(e,t)=>{if(null===t)return;if("string"===typeof t){if(!t.length)return;t=new RegExp(t,"i")}const highlightText=e=>{var s=e.data.match(t);if(s&&e.data.length>0){var i=document.createElement("span");i.className="highlight";var n=e.splitText(s.index);n.splitText(s[0].length);var o=n.cloneNode(true);i.appendChild(o);replaceNode(n,i);return 1}return 0};const highlightChildren=e=>{1!==e.nodeType||!e.childNodes||/(script|style)/i.test(e.tagName)||"highlight"===e.className&&"SPAN"===e.tagName||Array.from(e.childNodes).forEach((e=>{highlightRecursive(e)}))};const highlightRecursive=e=>{if(3===e.nodeType)return highlightText(e);highlightChildren(e);return 0};highlightRecursive(e)};const removeHighlight=e=>{var t=e.querySelectorAll("span.highlight");Array.prototype.forEach.call(t,(function(e){var t=e.parentNode;t.replaceChild(e.firstChild,e);t.normalize()}))};const c=65;const d=13;const u=27;const p=37;const h=38;const g=39;const f=40;const v=8;const m=46;const y=9;const b="undefined"!==typeof navigator&&/Mac/.test(navigator.userAgent);const O=b?"metaKey":"ctrlKey";var _={options:[],optgroups:[],plugins:[],delimiter:",",splitOn:null,persist:true,diacritics:true,create:null,createOnBlur:false,createFilter:null,highlight:true,openOnFocus:true,shouldOpen:null,maxOptions:50,maxItems:null,hideSelected:null,duplicates:false,addPrecedence:false,selectOnTab:false,preload:null,allowEmptyOption:false,refreshThrottle:300,loadThrottle:300,loadingClass:"loading",dataAttr:null,optgroupField:"optgroup",valueField:"value",labelField:"text",disabledField:"disabled",optgroupLabelField:"label",optgroupValueField:"value",lockOptgroupOrder:false,sortField:"$order",searchField:["text"],searchConjunction:"and",mode:null,wrapperClass:"ts-wrapper",controlClass:"ts-control",dropdownClass:"ts-dropdown",dropdownContentClass:"ts-dropdown-content",itemClass:"item",optionClass:"option",dropdownParent:null,controlInput:'',copyClassesToDropdown:false,placeholder:null,hidePlaceholder:null,shouldLoad:function(e){return e.length>0},render:{}};const hash_key=e=>"undefined"===typeof e||null===e?null:get_hash(e);const get_hash=e=>"boolean"===typeof e?e?"1":"0":e+"";const escape_html=e=>(e+"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""");const timeout=(e,t)=>{if(t>0)return setTimeout(e,t);e.call(null);return null};const loadDebounce=(t,s)=>{var i;return function(n,o){var r=this||e;if(i){r.loading=Math.max(r.loading-1,0);clearTimeout(i)}i=setTimeout((function(){i=null;r.loadedSearches[n]=true;t.call(r,n,o)}),s)}};const debounce_events=(e,t,s)=>{var i;var n=e.trigger;var o={};e.trigger=function(){var s=arguments[0];if(-1===t.indexOf(s))return n.apply(e,arguments);o[s]=arguments};s.apply(e,[]);e.trigger=n;for(i of t)i in o&&n.apply(e,o[i])};const getSelection=e=>({start:e.selectionStart||0,length:(e.selectionEnd||0)-(e.selectionStart||0)});const preventDefault=(e,t=false)=>{if(e){e.preventDefault();t&&e.stopPropagation()}};const addEvent=(e,t,s,i)=>{e.addEventListener(t,s,i)};const isKeyDown=(e,t)=>{if(!t)return false;if(!t[e])return false;var s=(t.altKey?1:0)+(t.ctrlKey?1:0)+(t.shiftKey?1:0)+(t.metaKey?1:0);return 1===s};const getId=(e,t)=>{const s=e.getAttribute("id");if(s)return s;e.setAttribute("id",t);return t};const addSlashes=e=>e.replace(/[\\"']/g,"\\$&");const append=(e,t)=>{t&&e.append(t)};function getSettings(e,t){var s=Object.assign({},_,t);var i=s.dataAttr;var n=s.labelField;var o=s.valueField;var r=s.disabledField;var l=s.optgroupField;var a=s.optgroupLabelField;var c=s.optgroupValueField;var d=e.tagName.toLowerCase();var u=e.getAttribute("placeholder")||e.getAttribute("data-placeholder");if(!u&&!s.allowEmptyOption){let t=e.querySelector('option[value=""]');t&&(u=t.textContent)}var p={placeholder:u,options:[],optgroups:[],items:[],maxItems:null};var init_select=()=>{var t;var d=p.options;var u={};var h=1;let g=0;var readData=e=>{var t=Object.assign({},e.dataset);var s=i&&t[i];"string"===typeof s&&s.length&&(t=Object.assign(t,JSON.parse(s)));return t};var addOption=(e,t)=>{var i=hash_key(e.value);if(null!=i&&(i||s.allowEmptyOption)){if(u.hasOwnProperty(i)){if(t){var a=u[i][l];a?Array.isArray(a)?a.push(t):u[i][l]=[a,t]:u[i][l]=t}}else{var c=readData(e);c[n]=c[n]||e.textContent;c[o]=c[o]||i;c[r]=c[r]||e.disabled;c[l]=c[l]||t;c.$option=e;c.$order=c.$order||++g;u[i]=c;d.push(c)}e.selected&&p.items.push(i)}};var addGroup=e=>{var t,s;s=readData(e);s[a]=s[a]||e.getAttribute("label")||"";s[c]=s[c]||h++;s[r]=s[r]||e.disabled;s.$order=s.$order||++g;p.optgroups.push(s);t=s[c];iterate(e.children,(e=>{addOption(e,t)}))};p.maxItems=e.hasAttribute("multiple")?null:1;iterate(e.children,(e=>{t=e.tagName.toLowerCase();"optgroup"===t?addGroup(e):"option"===t&&addOption(e)}))};var init_textbox=()=>{const t=e.getAttribute(i);if(t){p.options=JSON.parse(t);iterate(p.options,(e=>{p.items.push(e[o])}))}else{var r=e.value.trim()||"";if(!s.allowEmptyOption&&!r.length)return;const t=r.split(s.delimiter);iterate(t,(e=>{const t={};t[n]=e;t[o]=e;p.options.push(t)}));p.items=t}};"select"===d?init_select():init_textbox();return Object.assign({},_,p,t)}var w=0;class TomSelect extends(MicroPlugin(MicroEvent)){constructor(e,t){super();this.control_input=void 0;this.wrapper=void 0;this.dropdown=void 0;this.control=void 0;this.dropdown_content=void 0;this.focus_node=void 0;this.order=0;this.settings=void 0;this.input=void 0;this.tabIndex=void 0;this.is_select_tag=void 0;this.rtl=void 0;this.inputId=void 0;this._destroy=void 0;this.sifter=void 0;this.isOpen=false;this.isDisabled=false;this.isReadOnly=false;this.isRequired=void 0;this.isInvalid=false;this.isValid=true;this.isLocked=false;this.isFocused=false;this.isInputHidden=false;this.isSetup=false;this.ignoreFocus=false;this.ignoreHover=false;this.hasOptions=false;this.currentResults=void 0;this.lastValue="";this.caretPos=0;this.loading=0;this.loadedSearches={};this.activeOption=null;this.activeItems=[];this.optgroups={};this.options={};this.userOptions={};this.items=[];this.refreshTimeout=null;w++;var s;var i=getDom(e);if(i.tomselect)throw new Error("Tom Select already initialized on this element");i.tomselect=this;var n=window.getComputedStyle&&window.getComputedStyle(i,null);s=n.getPropertyValue("direction");const o=getSettings(i,t);this.settings=o;this.input=i;this.tabIndex=i.tabIndex||0;this.is_select_tag="select"===i.tagName.toLowerCase();this.rtl=/rtl/i.test(s);this.inputId=getId(i,"tomselect-"+w);this.isRequired=i.required;this.sifter=new Sifter(this.options,{diacritics:o.diacritics});o.mode=o.mode||(1===o.maxItems?"single":"multi");"boolean"!==typeof o.hideSelected&&(o.hideSelected="multi"===o.mode);"boolean"!==typeof o.hidePlaceholder&&(o.hidePlaceholder="multi"!==o.mode);var r=o.createFilter;if("function"!==typeof r){"string"===typeof r&&(r=new RegExp(r));r instanceof RegExp?o.createFilter=e=>r.test(e):o.createFilter=e=>this.settings.duplicates||!this.options[e]}this.initializePlugins(o.plugins);this.setupCallbacks();this.setupTemplates();const l=getDom("
");const a=getDom("
");const c=this._render("dropdown");const d=getDom('
');const u=this.input.getAttribute("class")||"";const p=o.mode;var h;addClasses(l,o.wrapperClass,u,p);addClasses(a,o.controlClass);append(l,a);addClasses(c,o.dropdownClass,p);o.copyClassesToDropdown&&addClasses(c,u);addClasses(d,o.dropdownContentClass);append(c,d);getDom(o.dropdownParent||l).appendChild(c);if(isHtmlString(o.controlInput)){h=getDom(o.controlInput);var g=["autocorrect","autocapitalize","autocomplete","spellcheck"];iterate$1(g,(e=>{i.getAttribute(e)&&setAttr(h,{[e]:i.getAttribute(e)})}));h.tabIndex=-1;a.appendChild(h);this.focus_node=h}else if(o.controlInput){h=getDom(o.controlInput);this.focus_node=h}else{h=getDom("");this.focus_node=a}this.wrapper=l;this.dropdown=c;this.dropdown_content=d;this.control=a;this.control_input=h;this.setup()}setup(){const e=this;const t=e.settings;const s=e.control_input;const i=e.dropdown;const n=e.dropdown_content;const o=e.wrapper;const r=e.control;const l=e.input;const a=e.focus_node;const c={passive:true};const d=e.inputId+"-ts-dropdown";setAttr(n,{id:d});setAttr(a,{role:"combobox","aria-haspopup":"listbox","aria-expanded":"false","aria-controls":d});const u=getId(a,e.inputId+"-ts-control");const p="label[for='"+escapeQuery(e.inputId)+"']";const h=document.querySelector(p);const g=e.focus.bind(e);if(h){addEvent(h,"click",g);setAttr(h,{for:u});const t=getId(h,e.inputId+"-ts-label");setAttr(a,{"aria-labelledby":t});setAttr(n,{"aria-labelledby":t})}o.style.width=l.style.width;if(e.plugins.names.length){const t="plugin-"+e.plugins.names.join(" plugin-");addClasses([o,i],t)}(null===t.maxItems||t.maxItems>1)&&e.is_select_tag&&setAttr(l,{multiple:"multiple"});t.placeholder&&setAttr(s,{placeholder:t.placeholder});!t.splitOn&&t.delimiter&&(t.splitOn=new RegExp("\\s*"+escape_regex(t.delimiter)+"+\\s*"));t.load&&t.loadThrottle&&(t.load=loadDebounce(t.load,t.loadThrottle));addEvent(i,"mousemove",(()=>{e.ignoreHover=false}));addEvent(i,"mouseenter",(t=>{var s=parentMatch(t.target,"[data-selectable]",i);s&&e.onOptionHover(t,s)}),{capture:true});addEvent(i,"click",(t=>{const s=parentMatch(t.target,"[data-selectable]");if(s){e.onOptionSelect(t,s);preventDefault(t,true)}}));addEvent(r,"click",(t=>{var i=parentMatch(t.target,"[data-ts-item]",r);if(i&&e.onItemSelect(t,i))preventDefault(t,true);else if(""==s.value){e.onClick();preventDefault(t,true)}}));addEvent(a,"keydown",(t=>e.onKeyDown(t)));addEvent(s,"keypress",(t=>e.onKeyPress(t)));addEvent(s,"input",(t=>e.onInput(t)));addEvent(a,"blur",(t=>e.onBlur(t)));addEvent(a,"focus",(t=>e.onFocus(t)));addEvent(s,"paste",(t=>e.onPaste(t)));const doc_mousedown=t=>{const n=t.composedPath()[0];if(o.contains(n)||i.contains(n))n==s&&e.isOpen?t.stopPropagation():preventDefault(t,true);else{e.isFocused&&e.blur();e.inputState()}};const win_scroll=()=>{e.isOpen&&e.positionDropdown()};addEvent(document,"mousedown",doc_mousedown);addEvent(window,"scroll",win_scroll,c);addEvent(window,"resize",win_scroll,c);this._destroy=()=>{document.removeEventListener("mousedown",doc_mousedown);window.removeEventListener("scroll",win_scroll);window.removeEventListener("resize",win_scroll);h&&h.removeEventListener("click",g)};this.revertSettings={innerHTML:l.innerHTML,tabIndex:l.tabIndex};l.tabIndex=-1;l.insertAdjacentElement("afterend",e.wrapper);e.sync(false);t.items=[];delete t.optgroups;delete t.options;addEvent(l,"invalid",(()=>{if(e.isValid){e.isValid=false;e.isInvalid=true;e.refreshState()}}));e.updateOriginalInput();e.refreshItems();e.close(false);e.inputState();e.isSetup=true;l.disabled?e.disable():l.readOnly?e.setReadOnly(true):e.enable();e.on("change",this.onChange);addClasses(l,"tomselected","ts-hidden-accessible");e.trigger("initialize");true===t.preload&&e.preload()}setupOptions(e=[],t=[]){this.addOptions(e);iterate$1(t,(e=>{this.registerOptionGroup(e)}))}setupTemplates(){var e=this;var t=e.settings.labelField;var s=e.settings.optgroupLabelField;var i={optgroup:e=>{let t=document.createElement("div");t.className="optgroup";t.appendChild(e.options);return t},optgroup_header:(e,t)=>'
'+t(e[s])+"
",option:(e,s)=>"
"+s(e[t])+"
",item:(e,s)=>"
"+s(e[t])+"
",option_create:(e,t)=>'
Add '+t(e.input)+"
",no_results:()=>'
No results found
',loading:()=>'
',not_loading:()=>{},dropdown:()=>"
"};e.settings.render=Object.assign({},i,e.settings.render)}setupCallbacks(){var e,t;var s={initialize:"onInitialize",change:"onChange",item_add:"onItemAdd",item_remove:"onItemRemove",item_select:"onItemSelect",clear:"onClear",option_add:"onOptionAdd",option_remove:"onOptionRemove",option_clear:"onOptionClear",optgroup_add:"onOptionGroupAdd",optgroup_remove:"onOptionGroupRemove",optgroup_clear:"onOptionGroupClear",dropdown_open:"onDropdownOpen",dropdown_close:"onDropdownClose",type:"onType",load:"onLoad",focus:"onFocus",blur:"onBlur"};for(e in s){t=this.settings[s[e]];t&&this.on(e,t)}}sync(e=true){const t=this;const s=e?getSettings(t.input,{delimiter:t.settings.delimiter}):t.settings;t.setupOptions(s.options,s.optgroups);t.setValue(s.items||[],true);t.lastQuery=null}onClick(){var e=this;if(e.activeItems.length>0){e.clearActiveItems();e.focus()}else e.isFocused&&e.isOpen?e.blur():e.focus()} +/** + * @deprecated v1.7 + * + */onMouseDown(){}onChange(){triggerEvent(this.input,"input");triggerEvent(this.input,"change")}onPaste(e){var t=this;t.isInputHidden||t.isLocked?preventDefault(e):t.settings.splitOn&&setTimeout((()=>{var e=t.inputValue();if(e.match(t.settings.splitOn)){var s=e.trim().split(t.settings.splitOn);iterate$1(s,(e=>{const s=hash_key(e);s&&(this.options[e]?t.addItem(e):t.createItem(e))}))}}),0)}onKeyPress(e){var t=this;if(t.isLocked)preventDefault(e);else{var s=String.fromCharCode(e.keyCode||e.which);if(t.settings.create&&"multi"===t.settings.mode&&s===t.settings.delimiter){t.createItem();preventDefault(e)}else;}}onKeyDown(e){var t=this;t.ignoreHover=true;if(t.isLocked)e.keyCode!==y&&preventDefault(e);else{switch(e.keyCode){case c:if(isKeyDown(O,e)&&""==t.control_input.value){preventDefault(e);t.selectAll();return}break;case u:if(t.isOpen){preventDefault(e,true);t.close()}t.clearActiveItems();return;case f:if(!t.isOpen&&t.hasOptions)t.open();else if(t.activeOption){let e=t.getAdjacent(t.activeOption,1);e&&t.setActiveOption(e)}preventDefault(e);return;case h:if(t.activeOption){let e=t.getAdjacent(t.activeOption,-1);e&&t.setActiveOption(e)}preventDefault(e);return;case d:if(t.canSelect(t.activeOption)){t.onOptionSelect(e,t.activeOption);preventDefault(e)}else(t.settings.create&&t.createItem()||document.activeElement==t.control_input&&t.isOpen)&&preventDefault(e);return;case p:t.advanceSelection(-1,e);return;case g:t.advanceSelection(1,e);return;case y:if(t.settings.selectOnTab){if(t.canSelect(t.activeOption)){t.onOptionSelect(e,t.activeOption);preventDefault(e)}t.settings.create&&t.createItem()&&preventDefault(e)}return;case v:case m:t.deleteSelection(e);return}t.isInputHidden&&!isKeyDown(O,e)&&preventDefault(e)}}onInput(e){if(this.isLocked)return;const t=this.inputValue();if(this.lastValue!==t){this.lastValue=t;if(""!=t){this.refreshTimeout&&clearTimeout(this.refreshTimeout);this.refreshTimeout=timeout((()=>{this.refreshTimeout=null;this._onInput()}),this.settings.refreshThrottle)}else this._onInput()}}_onInput(){const e=this.lastValue;this.settings.shouldLoad.call(this,e)&&this.load(e);this.refreshOptions();this.trigger("type",e)}onOptionHover(e,t){this.ignoreHover||this.setActiveOption(t,false)}onFocus(e){var t=this;var s=t.isFocused;if(t.isDisabled||t.isReadOnly){t.blur();preventDefault(e)}else if(!t.ignoreFocus){t.isFocused=true;"focus"===t.settings.preload&&t.preload();s||t.trigger("focus");if(!t.activeItems.length){t.inputState();t.refreshOptions(!!t.settings.openOnFocus)}t.refreshState()}}onBlur(e){if(false!==document.hasFocus()){var t=this;if(t.isFocused){t.isFocused=false;t.ignoreFocus=false;var deactivate=()=>{t.close();t.setActiveItem();t.setCaret(t.items.length);t.trigger("blur")};t.settings.create&&t.settings.createOnBlur?t.createItem(null,deactivate):deactivate()}}}onOptionSelect(e,t){var s,i=this;if(!t.parentElement||!t.parentElement.matches("[data-disabled]"))if(t.classList.contains("create"))i.createItem(null,(()=>{i.settings.closeAfterSelect&&i.close()}));else{s=t.dataset.value;if("undefined"!==typeof s){i.lastQuery=null;i.addItem(s);i.settings.closeAfterSelect&&i.close();!i.settings.hideSelected&&e.type&&/click/.test(e.type)&&i.setActiveOption(t)}}}canSelect(e){return!!(this.isOpen&&e&&this.dropdown_content.contains(e))}onItemSelect(e,t){var s=this;if(!s.isLocked&&"multi"===s.settings.mode){preventDefault(e);s.setActiveItem(t,e);return true}return false}canLoad(e){return!!this.settings.load&&!this.loadedSearches.hasOwnProperty(e)}load(e){const t=this;if(!t.canLoad(e))return;addClasses(t.wrapper,t.settings.loadingClass);t.loading++;const s=t.loadCallback.bind(t);t.settings.load.call(t,e,s)}loadCallback(e,t){const s=this;s.loading=Math.max(s.loading-1,0);s.lastQuery=null;s.clearActiveOption();s.setupOptions(e,t);s.refreshOptions(s.isFocused&&!s.isInputHidden);s.loading||removeClasses(s.wrapper,s.settings.loadingClass);s.trigger("load",e,t)}preload(){var e=this.wrapper.classList;if(!e.contains("preloaded")){e.add("preloaded");this.load("")}}setTextboxValue(e=""){var t=this.control_input;var s=t.value!==e;if(s){t.value=e;triggerEvent(t,"update");this.lastValue=e}}getValue(){return this.is_select_tag&&this.input.hasAttribute("multiple")?this.items:this.items.join(this.settings.delimiter)}setValue(e,t){var s=t?[]:["change"];debounce_events(this,s,(()=>{this.clear(t);this.addItems(e,t)}))}setMaxItems(e){0===e&&(e=null);this.settings.maxItems=e;this.refreshState()}setActiveItem(e,t){var s=this;var i;var n,o,r,l;var a;if("single"!==s.settings.mode)if(e){i=t&&t.type.toLowerCase();if("click"===i&&isKeyDown("shiftKey",t)&&s.activeItems.length){a=s.getLastActive();o=Array.prototype.indexOf.call(s.control.children,a);r=Array.prototype.indexOf.call(s.control.children,e);if(o>r){l=o;o=r;r=l}for(n=o;n<=r;n++){e=s.control.children[n];-1===s.activeItems.indexOf(e)&&s.setActiveItemClass(e)}preventDefault(t)}else if("click"===i&&isKeyDown(O,t)||"keydown"===i&&isKeyDown("shiftKey",t))e.classList.contains("active")?s.removeActiveItem(e):s.setActiveItemClass(e);else{s.clearActiveItems();s.setActiveItemClass(e)}s.inputState();s.isFocused||s.focus()}else{s.clearActiveItems();s.isFocused&&s.inputState()}}setActiveItemClass(e){const t=this;const s=t.control.querySelector(".last-active");s&&removeClasses(s,"last-active");addClasses(e,"active last-active");t.trigger("item_select",e);-1==t.activeItems.indexOf(e)&&t.activeItems.push(e)}removeActiveItem(e){var t=this.activeItems.indexOf(e);this.activeItems.splice(t,1);removeClasses(e,"active")}clearActiveItems(){removeClasses(this.activeItems,"active");this.activeItems=[]}setActiveOption(e,t=true){if(e!==this.activeOption){this.clearActiveOption();if(e){this.activeOption=e;setAttr(this.focus_node,{"aria-activedescendant":e.getAttribute("id")});setAttr(e,{"aria-selected":"true"});addClasses(e,"active");t&&this.scrollToOption(e)}}}scrollToOption(e,t){if(!e)return;const s=this.dropdown_content;const i=s.clientHeight;const n=s.scrollTop||0;const o=e.offsetHeight;const r=e.getBoundingClientRect().top-s.getBoundingClientRect().top+n;r+o>i+n?this.scroll(r-i+o,t):r{e.setActiveItemClass(t)}))}}inputState(){var e=this;if(e.control.contains(e.control_input)){setAttr(e.control_input,{placeholder:e.settings.placeholder});if(e.activeItems.length>0||!e.isFocused&&e.settings.hidePlaceholder&&e.items.length>0){e.setTextboxValue();e.isInputHidden=true}else{e.settings.hidePlaceholder&&e.items.length>0&&setAttr(e.control_input,{placeholder:""});e.isInputHidden=false}e.wrapper.classList.toggle("input-hidden",e.isInputHidden)}}inputValue(){return this.control_input.value.trim()}focus(){var e=this;if(!e.isDisabled&&!e.isReadOnly){e.ignoreFocus=true;e.control_input.offsetWidth?e.control_input.focus():e.focus_node.focus();setTimeout((()=>{e.ignoreFocus=false;e.onFocus()}),0)}}blur(){this.focus_node.blur();this.onBlur()}getScoreFunction(e){return this.sifter.getScoreFunction(e,this.getSearchOptions())}getSearchOptions(){var e=this.settings;var t=e.sortField;"string"===typeof e.sortField&&(t=[{field:e.sortField}]);return{fields:e.searchField,conjunction:e.searchConjunction,sort:t,nesting:e.nesting}}search(e){var t,s;var i=this;var n=this.getSearchOptions();if(i.settings.score){s=i.settings.score.call(i,e);if("function"!==typeof s)throw new Error('Tom Select "score" setting must be a function that returns a function')}if(e!==i.lastQuery){i.lastQuery=e;t=i.sifter.search(e,Object.assign(n,{score:s}));i.currentResults=t}else t=Object.assign({},i.currentResults);i.settings.hideSelected&&(t.items=t.items.filter((e=>{let t=hash_key(e.id);return!(t&&-1!==i.items.indexOf(t))})));return t}refreshOptions(e=true){var t,s,i,n,o,r,l,a,c;var d;const u={};const p=[];var h=this;var g=h.inputValue();const f=g===h.lastQuery||""==g&&null==h.lastQuery;var v=h.search(g);var m=null;var y=h.settings.shouldOpen||false;var b=h.dropdown_content;if(f){m=h.activeOption;m&&(c=m.closest("[data-group]"))}n=v.items.length;"number"===typeof h.settings.maxOptions&&(n=Math.min(n,h.settings.maxOptions));n>0&&(y=true);const getGroupFragment=(e,t)=>{let s=u[e];if(void 0!==s){let e=p[s];if(void 0!==e)return[s,e.fragment]}let i=document.createDocumentFragment();s=p.length;p.push({fragment:i,order:t,optgroup:e});return[s,i]};for(t=0;t0){d=d.cloneNode(true);setAttr(d,{id:l.$id+"-clone-"+s,"aria-selected":null});d.classList.add("ts-cloned");removeClasses(d,"active");h.activeOption&&h.activeOption.dataset.value==n&&c&&c.dataset.group===o.toString()&&(m=d)}a.appendChild(d);""!=o&&(u[o]=i)}}h.settings.lockOptgroupOrder&&p.sort(((e,t)=>e.order-t.order));l=document.createDocumentFragment();iterate$1(p,(e=>{let t=e.fragment;let s=e.optgroup;if(!t||!t.children.length)return;let i=h.optgroups[s];if(void 0!==i){let e=document.createDocumentFragment();let s=h.render("optgroup_header",i);append(e,s);append(e,t);let n=h.render("optgroup",{group:i,options:e});append(l,n)}else append(l,t)}));b.innerHTML="";append(b,l);if(h.settings.highlight){removeHighlight(b);v.query.length&&v.tokens.length&&iterate$1(v.tokens,(e=>{highlight(b,e.regex)}))}var add_template=e=>{let t=h.render(e,{input:g});if(t){y=true;b.insertBefore(t,b.firstChild)}return t};h.loading?add_template("loading"):h.settings.shouldLoad.call(h,g)?0===v.items.length&&add_template("no_results"):add_template("not_loading");a=h.canCreate(g);a&&(d=add_template("option_create"));h.hasOptions=v.items.length>0||a;if(y){if(v.items.length>0){m||"single"!==h.settings.mode||void 0==h.items[0]||(m=h.getOption(h.items[0]));if(!b.contains(m)){let e=0;d&&!h.settings.addPrecedence&&(e=1);m=h.selectable()[e]}}else d&&(m=d);if(e&&!h.isOpen){h.open();h.scrollToOption(m,"auto")}h.setActiveOption(m)}else{h.clearActiveOption();e&&h.isOpen&&h.close(false)}}selectable(){return this.dropdown_content.querySelectorAll("[data-selectable]")}addOption(e,t=false){const s=this;if(Array.isArray(e)){s.addOptions(e,t);return false}const i=hash_key(e[s.settings.valueField]);if(null===i||s.options.hasOwnProperty(i))return false;e.$order=e.$order||++s.order;e.$id=s.inputId+"-opt-"+e.$order;s.options[i]=e;s.lastQuery=null;if(t){s.userOptions[i]=t;s.trigger("option_add",i,e)}return i}addOptions(e,t=false){iterate$1(e,(e=>{this.addOption(e,t)}))} +/** + * @deprecated 1.7.7 + */registerOption(e){return this.addOption(e)}registerOptionGroup(e){var t=hash_key(e[this.settings.optgroupValueField]);if(null===t)return false;e.$order=e.$order||++this.order;this.optgroups[t]=e;return t}addOptionGroup(e,t){var s;t[this.settings.optgroupValueField]=e;(s=this.registerOptionGroup(t))&&this.trigger("optgroup_add",s,t)}removeOptionGroup(e){if(this.optgroups.hasOwnProperty(e)){delete this.optgroups[e];this.clearCache();this.trigger("optgroup_remove",e)}}clearOptionGroups(){this.optgroups={};this.clearCache();this.trigger("optgroup_clear")}updateOption(e,t){const s=this;var i;var n;const o=hash_key(e);const r=hash_key(t[s.settings.valueField]);if(null===o)return;const l=s.options[o];if(void 0==l)return;if("string"!==typeof r)throw new Error("Value must be set in option data");const a=s.getOption(o);const c=s.getItem(o);t.$order=t.$order||l.$order;delete s.options[o];s.uncacheValue(r);s.options[r]=t;if(a){if(s.dropdown_content.contains(a)){const e=s._render("option",t);replaceNode(a,e);s.activeOption===a&&s.setActiveOption(e)}a.remove()}if(c){n=s.items.indexOf(o);-1!==n&&s.items.splice(n,1,r);i=s._render("item",t);c.classList.contains("active")&&addClasses(i,"active");replaceNode(c,i)}s.lastQuery=null}removeOption(e,t){const s=this;e=get_hash(e);s.uncacheValue(e);delete s.userOptions[e];delete s.options[e];s.lastQuery=null;s.trigger("option_remove",e);s.removeItem(e,t)}clearOptions(e){const t=(e||this.clearFilter).bind(this);this.loadedSearches={};this.userOptions={};this.clearCache();const s={};iterate$1(this.options,((e,i)=>{t(e,i)&&(s[i]=e)}));this.options=this.sifter.items=s;this.lastQuery=null;this.trigger("option_clear")}clearFilter(e,t){return this.items.indexOf(t)>=0}getOption(e,t=false){const s=hash_key(e);if(null===s)return null;const i=this.options[s];if(void 0!=i){if(i.$div)return i.$div;if(t)return this._render("option",i)}return null}getAdjacent(e,t,s="option"){var i,n=this;if(!e)return null;i="item"==s?n.controlChildren():n.dropdown_content.querySelectorAll("[data-selectable]");for(let s=0;s0?i[s+1]:i[s-1];return null}getItem(e){if("object"==typeof e)return e;var t=hash_key(e);return null!==t?this.control.querySelector(`[data-value="${addSlashes(t)}"]`):null}addItems(e,t){var s=this;var i=Array.isArray(e)?e:[e];i=i.filter((e=>-1===s.items.indexOf(e)));const n=i[i.length-1];i.forEach((e=>{s.isPending=e!==n;s.addItem(e,t)}))}addItem(e,t){var s=t?[]:["change","dropdown_close"];debounce_events(this,s,(()=>{var s,i;const n=this;const o=n.settings.mode;const r=hash_key(e);if(r&&-1!==n.items.indexOf(r)){"single"===o&&n.close();if("single"===o||!n.settings.duplicates)return}if(null!==r&&n.options.hasOwnProperty(r)){"single"===o&&n.clear(t);if("multi"!==o||!n.isFull()){s=n._render("item",n.options[r]);n.control.contains(s)&&(s=s.cloneNode(true));i=n.isFull();n.items.splice(n.caretPos,0,r);n.insertAtCaret(s);if(n.isSetup){if(!n.isPending&&n.settings.hideSelected){let e=n.getOption(r);let t=n.getAdjacent(e,1);t&&n.setActiveOption(t)}n.isPending||n.settings.closeAfterSelect||n.refreshOptions(n.isFocused&&"single"!==o);false!=n.settings.closeAfterSelect&&n.isFull()?n.close():n.isPending||n.positionDropdown();n.trigger("item_add",r,s);n.isPending||n.updateOriginalInput({silent:t})}if(!n.isPending||!i&&n.isFull()){n.inputState();n.refreshState()}}}}))}removeItem(e=null,t){const s=this;e=s.getItem(e);if(!e)return;var i,n;const o=e.dataset.value;i=nodeIndex(e);e.remove();if(e.classList.contains("active")){n=s.activeItems.indexOf(e);s.activeItems.splice(n,1);removeClasses(e,"active")}s.items.splice(i,1);s.lastQuery=null;!s.settings.persist&&s.userOptions.hasOwnProperty(o)&&s.removeOption(o,t);i{})){3===arguments.length&&(t=arguments[2]);"function"!=typeof t&&(t=()=>{});var s=this;var i=s.caretPos;var n;e=e||s.inputValue();if(!s.canCreate(e)){t();return false}s.lock();var o=false;var create=e=>{s.unlock();if(!e||"object"!==typeof e)return t();var n=hash_key(e[s.settings.valueField]);if("string"!==typeof n)return t();s.setTextboxValue();s.addOption(e,true);s.setCaret(i);s.addItem(n);t(e);o=true};n="function"===typeof s.settings.create?s.settings.create.call(this,e,create):{[s.settings.labelField]:e,[s.settings.valueField]:e};o||create(n);return true}refreshItems(){var e=this;e.lastQuery=null;e.isSetup&&e.addItems(e.items);e.updateOriginalInput();e.refreshState()}refreshState(){const e=this;e.refreshValidityState();const t=e.isFull();const s=e.isLocked;e.wrapper.classList.toggle("rtl",e.rtl);const i=e.wrapper.classList;i.toggle("focus",e.isFocused);i.toggle("disabled",e.isDisabled);i.toggle("readonly",e.isReadOnly);i.toggle("required",e.isRequired);i.toggle("invalid",!e.isValid);i.toggle("locked",s);i.toggle("full",t);i.toggle("input-active",e.isFocused&&!e.isInputHidden);i.toggle("dropdown-active",e.isOpen);i.toggle("has-options",isEmptyObject(e.options));i.toggle("has-items",e.items.length>0)}refreshValidityState(){var e=this;if(e.input.validity){e.isValid=e.input.validity.valid;e.isInvalid=!e.isValid}} +/** + * Determines whether or not more items can be added + * to the control without exceeding the user-defined maximum. + * + * @returns {boolean} + */isFull(){return null!==this.settings.maxItems&&this.items.length>=this.settings.maxItems}updateOriginalInput(e={}){const t=this;var s,i;const n=t.input.querySelector('option[value=""]');if(t.is_select_tag){const o=[];const r=t.input.querySelectorAll("option:checked").length;function AddSelected(e,s,i){e||(e=getDom('"));e!=n&&t.input.append(e);o.push(e);(e!=n||r>0)&&(e.selected=true);return e}t.input.querySelectorAll("option:checked").forEach((e=>{e.selected=false}));0==t.items.length&&"single"==t.settings.mode?AddSelected(n,"",""):t.items.forEach((e=>{s=t.options[e];i=s[t.settings.labelField]||"";if(o.includes(s.$option)){const s=t.input.querySelector(`option[value="${addSlashes(e)}"]:not(:checked)`);AddSelected(s,e,i)}else s.$option=AddSelected(s.$option,e,i)}))}else t.input.value=t.getValue();t.isSetup&&(e.silent||t.trigger("change",t.getValue()))}open(){var e=this;if(!(e.isLocked||e.isOpen||"multi"===e.settings.mode&&e.isFull())){e.isOpen=true;setAttr(e.focus_node,{"aria-expanded":"true"});e.refreshState();applyCSS(e.dropdown,{visibility:"hidden",display:"block"});e.positionDropdown();applyCSS(e.dropdown,{visibility:"visible",display:"block"});e.focus();e.trigger("dropdown_open",e.dropdown)}}close(e=true){var t=this;var s=t.isOpen;if(e){t.setTextboxValue();"single"===t.settings.mode&&t.items.length&&t.inputState()}t.isOpen=false;setAttr(t.focus_node,{"aria-expanded":"false"});applyCSS(t.dropdown,{display:"none"});t.settings.hideSelected&&t.clearActiveOption();t.refreshState();s&&t.trigger("dropdown_close",t.dropdown)}positionDropdown(){if("body"===this.settings.dropdownParent){var e=this.control;var t=e.getBoundingClientRect();var s=e.offsetHeight+t.top+window.scrollY;var i=t.left+window.scrollX;applyCSS(this.dropdown,{width:t.width+"px",top:s+"px",left:i+"px"})}}clear(e){var t=this;if(t.items.length){var s=t.controlChildren();iterate$1(s,(e=>{t.removeItem(e,true)}));t.inputState();e||t.updateOriginalInput();t.trigger("clear")}}insertAtCaret(e){const t=this;const s=t.caretPos;const i=t.control;i.insertBefore(e,i.children[s]||null);t.setCaret(s+1)}deleteSelection(e){var t,s,i,n;var o=this;t=e&&e.keyCode===v?-1:1;s=getSelection(o.control_input);const r=[];if(o.activeItems.length){n=getTail(o.activeItems,t);i=nodeIndex(n);t>0&&i++;iterate$1(o.activeItems,(e=>r.push(e)))}else if((o.isFocused||"single"===o.settings.mode)&&o.items.length){const e=o.controlChildren();let i;t<0&&0===s.start&&0===s.length?i=e[o.caretPos-1]:t>0&&s.start===o.inputValue().length&&(i=e[o.caretPos]);void 0!==i&&r.push(i)}if(!o.shouldDelete(r,e))return false;preventDefault(e,true);"undefined"!==typeof i&&o.setCaret(i);while(r.length)o.removeItem(r.pop());o.inputState();o.positionDropdown();o.refreshOptions(false);return true}shouldDelete(e,t){const s=e.map((e=>e.dataset.value));return!(!s.length||"function"===typeof this.settings.onDelete&&false===this.settings.onDelete(s,t))}advanceSelection(e,t){var s,i,n=this;n.rtl&&(e*=-1);if(!n.inputValue().length)if(isKeyDown(O,t)||isKeyDown("shiftKey",t)){s=n.getLastActive(e);i=s?s.classList.contains("active")?n.getAdjacent(s,e,"item"):s:e>0?n.control_input.nextElementSibling:n.control_input.previousElementSibling;if(i){i.classList.contains("active")&&n.removeActiveItem(s);n.setActiveItemClass(i)}}else n.moveCaret(e)}moveCaret(e){}getLastActive(e){let t=this.control.querySelector(".last-active");if(t)return t;var s=this.control.querySelectorAll(".active");return s?getTail(s,e):void 0}setCaret(e){this.caretPos=this.items.length}controlChildren(){return Array.from(this.control.querySelectorAll("[data-ts-item]"))}lock(){this.setLocked(true)}unlock(){this.setLocked(false)}setLocked(e=this.isReadOnly||this.isDisabled){this.isLocked=e;this.refreshState()}disable(){this.setDisabled(true);this.close()}enable(){this.setDisabled(false)}setDisabled(e){this.focus_node.tabIndex=e?-1:this.tabIndex;this.isDisabled=e;this.input.disabled=e;this.control_input.disabled=e;this.setLocked()}setReadOnly(e){this.isReadOnly=e;this.input.readOnly=e;this.control_input.readOnly=e;this.setLocked()}destroy(){var e=this;var t=e.revertSettings;e.trigger("destroy");e.off();e.wrapper.remove();e.dropdown.remove();e.input.innerHTML=t.innerHTML;e.input.tabIndex=t.tabIndex;removeClasses(e.input,"tomselected","ts-hidden-accessible");e._destroy();delete e.input.tomselect}render(e,t){var s,i;const n=this;if("function"!==typeof this.settings.render[e])return null;i=n.settings.render[e].call(this,t,escape_html);if(!i)return null;i=getDom(i);if("option"===e||"option_create"===e)t[n.settings.disabledField]?setAttr(i,{"aria-disabled":"true"}):setAttr(i,{"data-selectable":""});else if("optgroup"===e){s=t.group[n.settings.optgroupValueField];setAttr(i,{"data-group":s});t.group[n.settings.disabledField]&&setAttr(i,{"data-disabled":""})}if("option"===e||"item"===e){const s=get_hash(t[n.settings.valueField]);setAttr(i,{"data-value":s});if("item"===e){addClasses(i,n.settings.itemClass);setAttr(i,{"data-ts-item":""})}else{addClasses(i,n.settings.optionClass);setAttr(i,{role:"option",id:t.$id});t.$div=i;n.options[s]=t}}return i}_render(e,t){const s=this.render(e,t);if(null==s)throw"HTMLElement expected";return s}clearCache(){iterate$1(this.options,(e=>{if(e.$div){e.$div.remove();delete e.$div}}))}uncacheValue(e){const t=this.getOption(e);t&&t.remove()}canCreate(e){return this.settings.create&&e.length>0&&this.settings.createFilter.call(this,e)}hook(e,t,s){var i=this;var n=i[t];i[t]=function(){var t,o;"after"===e&&(t=n.apply(i,arguments));o=s.apply(i,arguments);if("instead"===e)return o;"before"===e&&(t=n.apply(i,arguments));return t}}}function change_listener(){addEvent((this||e).input,"change",(()=>{this.sync()}))}function checkbox_options(t){var s=this||e;var i=s.onOptionSelect;s.settings.hideSelected=false;const n=Object.assign({className:"tomselect-checkbox",checkedClassNames:void 0,uncheckedClassNames:void 0},t);var o=function UpdateChecked(e,t){if(t){e.checked=true;n.uncheckedClassNames&&e.classList.remove(...n.uncheckedClassNames);n.checkedClassNames&&e.classList.add(...n.checkedClassNames)}else{e.checked=false;n.checkedClassNames&&e.classList.remove(...n.checkedClassNames);n.uncheckedClassNames&&e.classList.add(...n.uncheckedClassNames)}};var r=function UpdateCheckbox(e){setTimeout((()=>{var t=e.querySelector("input."+n.className);t instanceof HTMLInputElement&&o(t,e.classList.contains("selected"))}),1)};s.hook("after","setupTemplates",(()=>{var e=s.settings.render.option;s.settings.render.option=(t,i)=>{var r=getDom(e.call(s,t,i));var l=document.createElement("input");n.className&&l.classList.add(n.className);l.addEventListener("click",(function(e){preventDefault(e)}));l.type="checkbox";const a=hash_key(t[s.settings.valueField]);o(l,!!(a&&s.items.indexOf(a)>-1));r.prepend(l);return r}}));s.on("item_remove",(e=>{var t=s.getOption(e);if(t){t.classList.remove("selected");r(t)}}));s.on("item_add",(e=>{var t=s.getOption(e);t&&r(t)}));s.hook("instead","onOptionSelect",((e,t)=>{if(t.classList.contains("selected")){t.classList.remove("selected");s.removeItem(t.dataset.value);s.refreshOptions();preventDefault(e,true)}else{i.call(s,e,t);r(t)}}))}function clear_button(t){const s=this||e;const i=Object.assign({className:"clear-button",title:"Clear All",html:e=>`
`},t);s.on("initialize",(()=>{var e=getDom(i.html(i));e.addEventListener("click",(e=>{if(!s.isLocked){s.clear();"single"===s.settings.mode&&s.settings.allowEmptyOption&&s.addItem("");e.preventDefault();e.stopPropagation()}}));s.control.appendChild(e)}))}const insertAfter=(e,t)=>{var s;null==(s=e.parentNode)||s.insertBefore(t,e.nextSibling)};const insertBefore=(e,t)=>{var s;null==(s=e.parentNode)||s.insertBefore(t,e)};const isBefore=(e,t)=>{do{var s;t=null==(s=t)?void 0:s.previousElementSibling;if(e==t)return true}while(t&&t.previousElementSibling);return false};function drag_drop(){var t=this||e;if("multi"!==t.settings.mode)return;var s=t.lock;var i=t.unlock;let n=true;let o;t.hook("after","setupTemplates",(()=>{var e=t.settings.render.item;t.settings.render.item=(s,i)=>{const r=getDom(e.call(t,s,i));setAttr(r,{draggable:"true"});const mousedown=e=>{n||preventDefault(e);e.stopPropagation()};const dragStart=e=>{o=r;setTimeout((()=>{r.classList.add("ts-dragging")}),0)};const dragOver=e=>{e.preventDefault();r.classList.add("ts-drag-over");moveitem(r,o)};const dragLeave=()=>{r.classList.remove("ts-drag-over")};const moveitem=(e,t)=>{void 0!==t&&(isBefore(t,r)?insertAfter(e,t):insertBefore(e,t))};const dragend=()=>{var e;document.querySelectorAll(".ts-drag-over").forEach((e=>e.classList.remove("ts-drag-over")));null==(e=o)||e.classList.remove("ts-dragging");o=void 0;var s=[];t.control.querySelectorAll("[data-value]").forEach((e=>{if(e.dataset.value){let t=e.dataset.value;t&&s.push(t)}}));t.setValue(s)};addEvent(r,"mousedown",mousedown);addEvent(r,"dragstart",dragStart);addEvent(r,"dragenter",dragOver);addEvent(r,"dragover",dragOver);addEvent(r,"dragleave",dragLeave);addEvent(r,"dragend",dragend);return r}}));t.hook("instead","lock",(()=>{n=false;return s.call(t)}));t.hook("instead","unlock",(()=>{n=true;return i.call(t)}))}function dropdown_header(t){const s=this||e;const i=Object.assign({title:"Untitled",headerClass:"dropdown-header",titleRowClass:"dropdown-header-title",labelClass:"dropdown-header-label",closeClass:"dropdown-header-close",html:e=>'
'+e.title+'×
'},t);s.on("initialize",(()=>{var e=getDom(i.html(i));var t=e.querySelector("."+i.closeClass);t&&t.addEventListener("click",(e=>{preventDefault(e,true);s.close()}));s.dropdown.insertBefore(e,s.dropdown.firstChild)}))}function caret_position(){var t=this||e;t.hook("instead","setCaret",(e=>{if("single"!==t.settings.mode&&t.control.contains(t.control_input)){e=Math.max(0,Math.min(t.items.length,e));e==t.caretPos||t.isPending||t.controlChildren().forEach(((s,i)=>{i{if(!t.isFocused)return;const s=t.getLastActive(e);if(s){const i=nodeIndex(s);t.setCaret(e>0?i+1:i);t.setActiveItem();removeClasses(s,"last-active")}else t.setCaret(t.caretPos+e)}))}function dropdown_input(){const t=this||e;t.settings.shouldOpen=true;t.hook("before","setup",(()=>{t.focus_node=t.control;addClasses(t.control_input,"dropdown-input");const e=getDom('