From 01940f9dfdbb7bbdf82fcabf5bde24e15c8393a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=8B=E3=81=84=E3=81=B2=E3=81=B3=E3=81=8D?= <67856090+kenchasonakai@users.noreply.github.com> Date: Wed, 25 Jan 2023 10:13:47 +0900 Subject: [PATCH 1/9] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 64d25a2f7..fe7cb8dde 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## 環境構築 ``` -$ bundle install +$ bundle install --without=production $ bin/rails db:setup $ yarn install $ bin/rails s From 3164f1d24901e25c805a781e2fc966ea7c00f178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=8B=E3=81=84=E3=81=B2=E3=81=B3=E3=81=8D?= <67856090+kenchasonakai@users.noreply.github.com> Date: Wed, 25 Jan 2023 10:18:22 +0900 Subject: [PATCH 2/9] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index fe7cb8dde..3ae9af23e 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ $ bundle install --without=production $ bin/rails db:setup $ yarn install $ bin/rails s +$ bin/webpack-dev-server ``` ## 事業をエンジニアリングしよう提案編の回答は以下に記述してください From 597f7ac9baa571f470d6c8b6857830340fd3c923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=8B=E3=81=84=E3=81=B2=E3=81=B3=E3=81=8D?= <67856090+kenchasonakai@users.noreply.github.com> Date: Wed, 25 Jan 2023 10:26:27 +0900 Subject: [PATCH 3/9] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ae9af23e..55e470590 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ $ bundle install --without=production $ bin/rails db:setup $ yarn install +$ bin/webpack $ bin/rails s -$ bin/webpack-dev-server ``` ## 事業をエンジニアリングしよう提案編の回答は以下に記述してください From f534e8c88241bf588bb0d5e12e340c385049879d Mon Sep 17 00:00:00 2001 From: kenchaso Date: Wed, 25 Jan 2023 10:27:25 +0900 Subject: [PATCH 4/9] =?UTF-8?q?add:=20node=E3=81=AE=E3=83=90=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=83=A7=E3=83=B3=E3=82=92=E6=8C=87=E5=AE=9A=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .node-version | 1 + 1 file changed, 1 insertion(+) create mode 100644 .node-version diff --git a/.node-version b/.node-version new file mode 100644 index 000000000..431076a94 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +16.16.0 From 3f6c5d68853ab00efe29a7b79b959f6a860ee380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9C=9F=E6=9C=A8=E6=99=B4=E7=94=9F?= Date: Mon, 16 Oct 2023 23:00:53 +0900 Subject: [PATCH 5/9] CreateTagAndModal --- .DS_Store | Bin 0 -> 8196 bytes Gemfile | 5 +- Gemfile.lock | 20 +- app/.DS_Store | Bin 0 -> 6148 bytes app/assets/.DS_Store | Bin 0 -> 6148 bytes app/assets/stylesheets/application.scss | 2 + app/assets/stylesheets/jquery.tagit.css | 69 ++ app/assets/stylesheets/tagit.ui-zendesk.css | 98 +++ app/assets/stylesheets/theme.css | 1 + app/controllers/events_controller.rb | 2 +- app/javascript/.DS_Store | Bin 0 -> 6148 bytes app/javascript/channels/tag-it.js | 604 ++++++++++++++++++ app/javascript/packs/application.js | 5 + app/models/event.rb | 2 + app/views/events/_event.html.erb | 5 + app/views/events/_form.html.erb | 4 + app/views/events/show.html.erb | 3 + app/views/shared/_header.html.erb | 24 + ...on_migration.acts_as_taggable_on_engine.rb | 34 + ...ique_indices.acts_as_taggable_on_engine.rb | 24 + ...ache_to_tags.acts_as_taggable_on_engine.rb | 17 + ...ggable_index.acts_as_taggable_on_engine.rb | 13 + ...or_tag_names.acts_as_taggable_on_engine.rb | 13 + ..._on_taggings.acts_as_taggable_on_engine.rb | 25 + ..._to_taggings.acts_as_taggable_on_engine.rb | 14 + db/schema.rb | 34 +- package.json | 5 +- yarn.lock | 25 +- 28 files changed, 1037 insertions(+), 11 deletions(-) create mode 100644 .DS_Store create mode 100644 app/.DS_Store create mode 100644 app/assets/.DS_Store create mode 100644 app/assets/stylesheets/jquery.tagit.css create mode 100644 app/assets/stylesheets/tagit.ui-zendesk.css create mode 100644 app/javascript/.DS_Store create mode 100644 app/javascript/channels/tag-it.js create mode 100644 db/migrate/20231015122601_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb create mode 100644 db/migrate/20231015122602_add_missing_unique_indices.acts_as_taggable_on_engine.rb create mode 100644 db/migrate/20231015122603_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb create mode 100644 db/migrate/20231015122604_add_missing_taggable_index.acts_as_taggable_on_engine.rb create mode 100644 db/migrate/20231015122605_change_collation_for_tag_names.acts_as_taggable_on_engine.rb create mode 100644 db/migrate/20231015122606_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb create mode 100644 db/migrate/20231015122607_add_tenant_to_taggings.acts_as_taggable_on_engine.rb diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..87096887d1ae9d0606fa7e481e0b050189c8d2af GIT binary patch literal 8196 zcmeI1L2uJA6vv<2vZm^QB7xWiDH7MIjBVP4xRlZfap1BdH~=b58%nk;u9|igs)~As z@4#2!$|vEwaDxA{y(vytA?<)b*i-xx=l`DVpWURdBO=k9^zRdG5Rr$2ZLNyxg2MOt zSSkh6avN5_pJXqHWq_;NlGZgi0*-(q;0QPZj=_S`h=s=}X0N4_mRl&LX@(=5C18l?Cg|=ux5!w}2y9$$H2$MSq?T-5mqhDyN z-ASmP5y$i_Ook#%>LGk3C(+Q>)sBE8uuOn+_ab#^NW-VGmA^m2o%&Bzl$%in({yOE z{BOZ9j?(eg)^|}TmaeRoy|P#J-n8G#X?xrqPts<0^op;ZOBn}e=iT5%6iz#}n~!BO z?nX&CQVvl+gyi+hDCx^-Q%;h8s(jn}hF9?_o!Z81w!6Q->2L4t%s2hngYB(Ne|KkZ zKCgJU?%sQN*g1|*68T9_QmV_+ zkUAJQpG^=w%&&-G8n)jM>xr?V9zd^Fkyq93PZ7%c-N5HTQc7d^ruh5)UH~<5EkkV$ z5hmD4%%ErYMj&NrJvzkrjxj#oYfCG_%q)Ec`U9*}i=M&OL8Nc@!2B$a66P>O{M<;G zm!&JKIZNy_#mkP$lq_`-d`>QUZitT-iBt5PV9gNMj;KgvwH5k+o<~rx*ky`E*2BI3 zvE^NH1TI0KsC_o7um`Ju|Gz|J=i&%B0{;pDQEIhYO^o*J3v+(poNN0y-s0fGx`npN m1(iz2VO2T~d;SkYocoZnPs7-SwrD|dKLjWYu5twaD1l$mN?3OQ literal 0 HcmV?d00001 diff --git a/Gemfile b/Gemfile index 059b8fabb..814f9501a 100644 --- a/Gemfile +++ b/Gemfile @@ -8,7 +8,7 @@ ruby '3.0.2' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main' gem 'rails', '~> 6.1.4', '>= 6.1.4.1' # Use Puma as the app server -gem 'puma', '~> 5.0' +gem 'puma' # Use SCSS for stylesheets gem 'sass-rails', '>= 6' # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker @@ -38,6 +38,9 @@ gem 'factory_bot_rails' gem 'faker' gem 'enum_help' +gem 'acts-as-taggable-on' +gem 'jquery-ui-rails' +gem 'jquery-rails' group :development, :test do # Use sqlite3 as the database for Active Record diff --git a/Gemfile.lock b/Gemfile.lock index 4d19f0151..3eebb42af 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -81,6 +81,8 @@ GEM minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) + acts-as-taggable-on (10.0.0) + activerecord (>= 6.1, < 7.2) addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) ast (2.4.2) @@ -154,6 +156,12 @@ GEM jbuilder (2.11.3) activesupport (>= 5.0.0) jmespath (1.5.0) + jquery-rails (4.6.0) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) + jquery-ui-rails (6.0.1) + railties (>= 3.2.16) jwt (2.3.0) launchy (2.5.0) addressable (~> 2.7) @@ -180,7 +188,9 @@ GEM multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.1.1) - nio4r (2.5.8) + nio4r (2.5.9) + nokogiri (1.12.5-arm64-darwin) + racc (~> 1.4) nokogiri (1.12.5-x86_64-darwin) racc (~> 1.4) nokogiri (1.12.5-x86_64-linux) @@ -197,7 +207,7 @@ GEM ast (~> 2.4.1) pg (1.2.3) public_suffix (4.0.6) - puma (5.5.2) + puma (6.4.0) nio4r (~> 2.0) racc (1.6.0) rack (2.2.3) @@ -329,10 +339,12 @@ GEM zeitwerk (2.5.1) PLATFORMS + arm64-darwin-22 x86_64-darwin-19 x86_64-linux DEPENDENCIES + acts-as-taggable-on aws-sdk-s3 bootsnap (>= 1.4.4) byebug @@ -342,11 +354,13 @@ DEPENDENCIES faker jb jbuilder (~> 2.7) + jquery-rails + jquery-ui-rails kaminari! letter_opener_web listen (~> 3.3) pg - puma (~> 5.0) + puma rack-mini-profiler (~> 2.0) rails (~> 6.1.4, >= 6.1.4.1) rails-i18n diff --git a/app/.DS_Store b/app/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..85a555b07df7a4102a32995cfa026cb174784177 GIT binary patch literal 6148 zcmeHKO>fgM7=GP#ZQ2Ud14z3dMdDgP*``g1OXbwlC&IlL8$T@*^gttPi#N_iit?|=aVNyZ6fjz7^_W0Ta3#&Y*@wS z+yyH6Mn>t&SdW8P2OHil!ztht_}>)ZwOgYRrFcRru3z31o+CW^EhbGb_y&t~YS;RY zU>Zkh*6sdOmA&eIt?t#mmiJ+Bp%;T}l+DxrX!efZUTYl(H|bIECJGnB_PsNmWTPkv zXF`Z3Aza?SjgpC8^z}TMq{1DT00!6yA0M5pRt@j| z!^h8F4Zp;fiT*0BCWZY;?2g47xJIJTr>ns%iFNV``ONWXOc&56!4pzk*xy*6?+AH) zTJYEK&1eRU@q|d_cSW$&gA%Jr6ZkQ6hPpDrGKX85uc?l9af_x~Yxq_2ZqjFb`ASB5 zj9Mb_vfN&cR#mvq`5Lxzc4W-A_N_GbRcNLuD36}3iEp_zQKw7j8f_nInIU=a8oafP z_r`3Jd6D6yC%HWt(lJC6mSacx&pjE_y~+Wi)(|jbs&-} z0I-T|F{JreWR7pKXK`&1B`{^EKtombilGc0`M&k*SzH@5bW-;6p=`~{-cXd(j_>>8 zPO4|n#ZCdIKv97`^V;U~|Ha?$|3#8}a|$>Gc1i(NJs+I+(ItDfZgh^%S|8yf0vr3S m4GIO3J&q;8NAUrI7|MK30DBhK29X1EKLR3yi<|=gRDnOKQV{z9 literal 0 HcmV?d00001 diff --git a/app/assets/.DS_Store b/app/assets/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..1b44e0509762a97f10aa6d11debf94a0336c677e GIT binary patch literal 6148 zcmeHKL2KJE6n;w5#32-NXd#z@VArsvNogs)xLJ1CbuQgu8>yYRsi|v~*l8#s;Pdul zcHJ-O-)Y~Il(IA{><~s<-h-!4dU~I1f3hVD07Q2(+ySTqfP+q0sABOKqki%gE7-^e zqR?YRF!~g$K@h88C7T1!Q30B}G7LaL0%x#1fAmrup})J+rfgR3}7vsUX?6pE#da>c1QHRro`uBKk* zXOpz+kH1puSgANz)cxQj3a9=0)(4ejew2h`O^AjerhNMnB||mss!1|THPy6oLcYnVr+k2gPQ_c?Bt)|@V?9Jy@XZ!8@!_WPT_%cyHv_c^8ThO*?aREQE@`NVW z-Z+U>GQx}nd%_HQTifE|99x;TWV_e!i*J5ta&_US4hp<-`_Jktz)p2~O(?Pff zxnu=cfn^1Xrdg-`|Kro=|78-lSOHexRVg4!J+IfrN3whC%EQrKE74EU$tW*3_&~vm i-ik4ow&Di5HjGQEAi5TFgBU^M9|0``7p%a4Rp1`WXLnx! literal 0 HcmV?d00001 diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 586f4fd9d..6cdee7e24 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -1 +1,3 @@ @import 'theme.css'; +@import 'tag-it/css/jquery.tagit.css'; // 追記 +@import 'tag-it/css/tagit.ui-zendesk.css'; // 追記 \ No newline at end of file diff --git a/app/assets/stylesheets/jquery.tagit.css b/app/assets/stylesheets/jquery.tagit.css new file mode 100644 index 000000000..f18650d91 --- /dev/null +++ b/app/assets/stylesheets/jquery.tagit.css @@ -0,0 +1,69 @@ +ul.tagit { + padding: 1px 5px; + overflow: auto; + margin-left: inherit; /* usually we don't want the regular ul margins. */ + margin-right: inherit; +} +ul.tagit li { + display: block; + float: left; + margin: 2px 5px 2px 0; +} +ul.tagit li.tagit-choice { + position: relative; + line-height: inherit; +} +input.tagit-hidden-field { + display: none; +} +ul.tagit li.tagit-choice-read-only { + padding: .2em .5em .2em .5em; +} + +ul.tagit li.tagit-choice-editable { + padding: .2em 18px .2em .5em; +} + +ul.tagit li.tagit-new { + padding: .25em 4px .25em 0; +} + +ul.tagit li.tagit-choice a.tagit-label { + cursor: pointer; + text-decoration: none; +} +ul.tagit li.tagit-choice .tagit-close { + cursor: pointer; + position: absolute; + right: .1em; + top: 50%; + margin-top: -8px; + line-height: 17px; +} + +/* used for some custom themes that don't need image icons */ +ul.tagit li.tagit-choice .tagit-close .text-icon { + display: none; +} + +ul.tagit li.tagit-choice input { + display: block; + float: left; + margin: 2px 5px 2px 0; +} +ul.tagit input[type="text"] { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; + + border: none; + margin: 0; + padding: 0; + width: inherit; + background-color: inherit; + outline: none; +} diff --git a/app/assets/stylesheets/tagit.ui-zendesk.css b/app/assets/stylesheets/tagit.ui-zendesk.css new file mode 100644 index 000000000..b91181bf3 --- /dev/null +++ b/app/assets/stylesheets/tagit.ui-zendesk.css @@ -0,0 +1,98 @@ + +/* Optional scoped theme for tag-it which mimics the zendesk widget. */ + + +ul.tagit { + border-style: solid; + border-width: 1px; + border-color: #C6C6C6; + background: inherit; +} +ul.tagit li.tagit-choice { + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-border-radius: 6px; + border: 1px solid #CAD8F3; + + background: none; + background-color: #DEE7F8; + + font-weight: normal; +} +ul.tagit li.tagit-choice .tagit-label:not(a) { + color: #555; +} +ul.tagit li.tagit-choice a.tagit-close { + text-decoration: none; +} +ul.tagit li.tagit-choice .tagit-close { + right: .4em; +} +ul.tagit li.tagit-choice .ui-icon { + display: none; +} +ul.tagit li.tagit-choice .tagit-close .text-icon { + display: inline; + font-family: arial, sans-serif; + font-size: 16px; + line-height: 16px; + color: #777; +} +ul.tagit li.tagit-choice:hover, ul.tagit li.tagit-choice.remove { + background-color: #bbcef1; + border-color: #6d95e0; +} +ul.tagit li.tagit-choice a.tagLabel:hover, +ul.tagit li.tagit-choice a.tagit-close .text-icon:hover { + color: #222; +} +ul.tagit input[type="text"] { + color: #333333; + background: none; +} +.ui-widget { + font-size: 1.1em; +} + +/* Forked from a jQuery UI theme, so that we don't require the jQuery UI CSS as a dependency. */ +.tagit-autocomplete.ui-autocomplete { position: absolute; cursor: default; } +* html .tagit-autocomplete.ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */ +.tagit-autocomplete.ui-menu { + list-style:none; + padding: 2px; + margin: 0; + display:block; + float: left; +} +.tagit-autocomplete.ui-menu .ui-menu { + margin-top: -3px; +} +.tagit-autocomplete.ui-menu .ui-menu-item { + margin:0; + padding: 0; + zoom: 1; + float: left; + clear: left; + width: 100%; +} +.tagit-autocomplete.ui-menu .ui-menu-item a { + text-decoration:none; + display:block; + padding:.2em .4em; + line-height:1.5; + zoom:1; +} +.tagit-autocomplete .ui-menu .ui-menu-item a.ui-state-hover, +.tagit-autocomplete .ui-menu .ui-menu-item a.ui-state-active { + font-weight: normal; + margin: -1px; +} +.tagit-autocomplete.ui-widget-content { border: 1px solid #aaaaaa; background: #ffffff 50% 50% repeat-x; color: #222222; } +.tagit-autocomplete.ui-corner-all, .tagit-autocomplete .ui-corner-all { -moz-border-radius: 4px; -webkit-border-radius: 4px; -khtml-border-radius: 4px; border-radius: 4px; } +.tagit-autocomplete .ui-state-hover, .tagit-autocomplete .ui-state-focus { border: 1px solid #999999; background: #dadada; font-weight: normal; color: #212121; } +.tagit-autocomplete .ui-state-active { border: 1px solid #aaaaaa; } + +.tagit-autocomplete .ui-widget-content { border: 1px solid #aaaaaa; } +.tagit .ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px,1px,1px,1px); } + + diff --git a/app/assets/stylesheets/theme.css b/app/assets/stylesheets/theme.css index 39ff42c5f..363fd662c 100644 --- a/app/assets/stylesheets/theme.css +++ b/app/assets/stylesheets/theme.css @@ -4186,6 +4186,7 @@ textarea.form-control-lg { font-size: 0.875rem; border-radius: 0.25rem; transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + white-space: nowrap; } @media (prefers-reduced-motion: reduce) { .btn { diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 235325908..6265504a5 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -60,6 +60,6 @@ def update private def event_params - params.require(:event).permit(:title, :content, :held_at, :prefecture_id, :thumbnail) + params.require(:event).permit(:title, :content, :held_at, :prefecture_id, :thumbnail, :tag_list) end end diff --git a/app/javascript/.DS_Store b/app/javascript/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..7e4ac4bd60b034c14476cdeacdc9e575eb45301f GIT binary patch literal 6148 zcmeHKL2uJA6n^eH>Z(G@0i<1!B5|#@vQ3*1mr%xmD?#kAp^`Mw7M8_TlP-sXBXG9(mNjPJxi{cjJe)ctM*+>T{ z^cV#t6w#En>C;-a3;c@;@ZR-kj9tqqrRDuA-{fi>=PE`9KOQgP+ds9T#*f69&cQ42 zOTk{v|N5u=xGpDl{BQ9j&&p~r_%2!-?ac=r*^xc@Av{-gSVh&W97fX0Lgg4P$S3JacnIa5-F9Z4};dh5tPR`Stw<`i%W z+*1YYS55Zr>H6KWodQmQyA|N?gAZp6EH(!9rvrt)0sz|xYeSv?EHcNp7+7o!q6a1n z6=|a5m0s4F0B|qK{(a@=?46*M_*y6<}bo TF^CAv{SnYIxWXy$R~7gLZ6lDR literal 0 HcmV?d00001 diff --git a/app/javascript/channels/tag-it.js b/app/javascript/channels/tag-it.js new file mode 100644 index 000000000..13f904542 --- /dev/null +++ b/app/javascript/channels/tag-it.js @@ -0,0 +1,604 @@ +/* +* jQuery UI Tag-it! +* +* @version v2.0 (06/2011) +* +* Copyright 2011, Levy Carneiro Jr. +* Released under the MIT license. +* https://github.com/aehlke/tag-it/blob/master/LICENSE +* +* Homepage: +* https://github.com/aehlke/tag-it +* +* Authors: +* Levy Carneiro Jr. +* Martin Rehfeld +* Tobias Schmidt +* Skylar Challand +* Alex Ehlke +* +* Maintainer: +* Alex Ehlke - Twitter: @aehlke +* +* Dependencies: +* jQuery v1.4+ +* jQuery UI v1.8+ +*/ +(function($) { + + $.widget('ui.tagit', { + options: { + allowDuplicates : false, + caseSensitive : true, + fieldName : 'tags', + placeholderText : null, // Sets `placeholder` attr on input field. + readOnly : false, // Disables editing. + removeConfirmation: false, // Require confirmation to remove tags. + tagLimit : null, // Max number of tags allowed (null for unlimited). + + // Used for autocomplete, unless you override `autocomplete.source`. + availableTags : [], + + // Use to override or add any options to the autocomplete widget. + // + // By default, autocomplete.source will map to availableTags, + // unless overridden. + autocomplete: {}, + + // Shows autocomplete before the user even types anything. + showAutocompleteOnFocus: false, + + // When enabled, quotes are unneccesary for inputting multi-word tags. + allowSpaces: false, + + // The below options are for using a single field instead of several + // for our form values. + // + // When enabled, will use a single hidden field for the form, + // rather than one per tag. It will delimit tags in the field + // with singleFieldDelimiter. + // + // The easiest way to use singleField is to just instantiate tag-it + // on an INPUT element, in which case singleField is automatically + // set to true, and singleFieldNode is set to that element. This + // way, you don't need to fiddle with these options. + singleField: false, + + // This is just used when preloading data from the field, and for + // populating the field with delimited tags as the user adds them. + singleFieldDelimiter: ',', + + // Set this to an input DOM node to use an existing form field. + // Any text in it will be erased on init. But it will be + // populated with the text of tags as they are created, + // delimited by singleFieldDelimiter. + // + // If this is not set, we create an input node for it, + // with the name given in settings.fieldName. + singleFieldNode: null, + + // Whether to animate tag removals or not. + animate: true, + + // Optionally set a tabindex attribute on the input that gets + // created for tag-it. + tabIndex: null, + + // Event callbacks. + beforeTagAdded : null, + afterTagAdded : null, + + beforeTagRemoved : null, + afterTagRemoved : null, + + onTagClicked : null, + onTagLimitExceeded : null, + + + // DEPRECATED: + // + // /!\ These event callbacks are deprecated and WILL BE REMOVED at some + // point in the future. They're here for backwards-compatibility. + // Use the above before/after event callbacks instead. + onTagAdded : null, + onTagRemoved: null, + // `autocomplete.source` is the replacement for tagSource. + tagSource: null + // Do not use the above deprecated options. + }, + + _create: function() { + // for handling static scoping inside callbacks + var that = this; + + // There are 2 kinds of DOM nodes this widget can be instantiated on: + // 1. UL, OL, or some element containing either of these. + // 2. INPUT, in which case 'singleField' is overridden to true, + // a UL is created and the INPUT is hidden. + if (this.element.is('input')) { + this.tagList = $('
    ').insertAfter(this.element); + this.options.singleField = true; + this.options.singleFieldNode = this.element; + this.element.addClass('tagit-hidden-field'); + } else { + this.tagList = this.element.find('ul, ol').andSelf().last(); + } + + this.tagInput = $('').addClass('ui-widget-content'); + + if (this.options.readOnly) this.tagInput.attr('disabled', 'disabled'); + + if (null != this.options.tabIndex) { + this.tagInput.attr('tabindex', this.options.tabIndex); + } + + if (this.options.placeholderText) { + this.tagInput.attr('placeholder', this.options.placeholderText); + } + + if (!this.options.autocomplete.source) { + this.options.autocomplete.source = function(search, showChoices) { + var filter = search.term.toLowerCase(); + var choices = $.grep(this.options.availableTags, function(element) { + // Only match autocomplete options that begin with the search term. + // (Case insensitive.) + return (element.toLowerCase().indexOf(filter) === 0); + }); + if (!this.options.allowDuplicates) { + choices = this._subtractArray(choices, this.assignedTags()); + } + showChoices(choices); + }; + } + + if (this.options.showAutocompleteOnFocus) { + this.tagInput.focus(function(event, ui) { + that._showAutocomplete(); + }); + + if (typeof this.options.autocomplete.minLength === 'undefined') { + this.options.autocomplete.minLength = 0; + } + } + + // Bind autocomplete.source callback functions to this context. + if ($.isFunction(this.options.autocomplete.source)) { + this.options.autocomplete.source = $.proxy(this.options.autocomplete.source, this); + } + + // DEPRECATED. + if ($.isFunction(this.options.tagSource)) { + this.options.tagSource = $.proxy(this.options.tagSource, this); + } + + this.tagList + .addClass('tagit') + .addClass('ui-widget ui-widget-content ui-corner-all') + // Create the input field. + .append($('
  • ').append(this.tagInput)) + .click(function(e) { + var target = $(e.target); + if (target.hasClass('tagit-label')) { + var tag = target.closest('.tagit-choice'); + if (!tag.hasClass('removed')) { + that._trigger('onTagClicked', e, {tag: tag, tagLabel: that.tagLabel(tag)}); + } + } else { + // Sets the focus() to the input field, if the user + // clicks anywhere inside the UL. This is needed + // because the input field needs to be of a small size. + that.tagInput.focus(); + } + }); + + // Single field support. + var addedExistingFromSingleFieldNode = false; + if (this.options.singleField) { + if (this.options.singleFieldNode) { + // Add existing tags from the input field. + var node = $(this.options.singleFieldNode); + var tags = node.val().split(this.options.singleFieldDelimiter); + node.val(''); + $.each(tags, function(index, tag) { + that.createTag(tag, null, true); + addedExistingFromSingleFieldNode = true; + }); + } else { + // Create our single field input after our list. + this.options.singleFieldNode = $(''); + this.tagList.after(this.options.singleFieldNode); + } + } + + // Add existing tags from the list, if any. + if (!addedExistingFromSingleFieldNode) { + this.tagList.children('li').each(function() { + if (!$(this).hasClass('tagit-new')) { + that.createTag($(this).text(), $(this).attr('class'), true); + $(this).remove(); + } + }); + } + + // Events. + this.tagInput + .keydown(function(event) { + // Backspace is not detected within a keypress, so it must use keydown. + if (event.which == $.ui.keyCode.BACKSPACE && that.tagInput.val() === '') { + var tag = that._lastTag(); + if (!that.options.removeConfirmation || tag.hasClass('remove')) { + // When backspace is pressed, the last tag is deleted. + that.removeTag(tag); + } else if (that.options.removeConfirmation) { + tag.addClass('remove ui-state-highlight'); + } + } else if (that.options.removeConfirmation) { + that._lastTag().removeClass('remove ui-state-highlight'); + } + + // Comma/Space/Enter are all valid delimiters for new tags, + // except when there is an open quote or if setting allowSpaces = true. + // Tab will also create a tag, unless the tag input is empty, + // in which case it isn't caught. + if ( + (event.which === $.ui.keyCode.COMMA && event.shiftKey === false) || + event.which === $.ui.keyCode.ENTER || + ( + event.which == $.ui.keyCode.TAB && + that.tagInput.val() !== '' + ) || + ( + event.which == $.ui.keyCode.SPACE && + that.options.allowSpaces !== true && + ( + $.trim(that.tagInput.val()).replace( /^s*/, '' ).charAt(0) != '"' || + ( + $.trim(that.tagInput.val()).charAt(0) == '"' && + $.trim(that.tagInput.val()).charAt($.trim(that.tagInput.val()).length - 1) == '"' && + $.trim(that.tagInput.val()).length - 1 !== 0 + ) + ) + ) + ) { + // Enter submits the form if there's no text in the input. + if (!(event.which === $.ui.keyCode.ENTER && that.tagInput.val() === '')) { + event.preventDefault(); + } + + // Autocomplete will create its own tag from a selection and close automatically. + if (!(that.options.autocomplete.autoFocus && that.tagInput.data('autocomplete-open'))) { + that.tagInput.autocomplete('close'); + that.createTag(that._cleanedInput()); + } + } + }).blur(function(e){ + // Create a tag when the element loses focus. + // If autocomplete is enabled and suggestion was clicked, don't add it. + if (!that.tagInput.data('autocomplete-open')) { + that.createTag(that._cleanedInput()); + } + }); + + // Autocomplete. + if (this.options.availableTags || this.options.tagSource || this.options.autocomplete.source) { + var autocompleteOptions = { + select: function(event, ui) { + that.createTag(ui.item.value); + // Preventing the tag input to be updated with the chosen value. + return false; + } + }; + $.extend(autocompleteOptions, this.options.autocomplete); + + // tagSource is deprecated, but takes precedence here since autocomplete.source is set by default, + // while tagSource is left null by default. + autocompleteOptions.source = this.options.tagSource || autocompleteOptions.source; + + this.tagInput.autocomplete(autocompleteOptions).bind('autocompleteopen.tagit', function(event, ui) { + that.tagInput.data('autocomplete-open', true); + }).bind('autocompleteclose.tagit', function(event, ui) { + that.tagInput.data('autocomplete-open', false); + }); + + this.tagInput.autocomplete('widget').addClass('tagit-autocomplete'); + } + }, + + destroy: function() { + $.Widget.prototype.destroy.call(this); + + this.element.unbind('.tagit'); + this.tagList.unbind('.tagit'); + + this.tagInput.removeData('autocomplete-open'); + + this.tagList.removeClass([ + 'tagit', + 'ui-widget', + 'ui-widget-content', + 'ui-corner-all', + 'tagit-hidden-field' + ].join(' ')); + + if (this.element.is('input')) { + this.element.removeClass('tagit-hidden-field'); + this.tagList.remove(); + } else { + this.element.children('li').each(function() { + if ($(this).hasClass('tagit-new')) { + $(this).remove(); + } else { + $(this).removeClass([ + 'tagit-choice', + 'ui-widget-content', + 'ui-state-default', + 'ui-state-highlight', + 'ui-corner-all', + 'remove', + 'tagit-choice-editable', + 'tagit-choice-read-only' + ].join(' ')); + + $(this).text($(this).children('.tagit-label').text()); + } + }); + + if (this.singleFieldNode) { + this.singleFieldNode.remove(); + } + } + + return this; + }, + + _cleanedInput: function() { + // Returns the contents of the tag input, cleaned and ready to be passed to createTag + return $.trim(this.tagInput.val().replace(/^"(.*)"$/, '$1')); + }, + + _lastTag: function() { + return this.tagList.find('.tagit-choice:last:not(.removed)'); + }, + + _tags: function() { + return this.tagList.find('.tagit-choice:not(.removed)'); + }, + + assignedTags: function() { + // Returns an array of tag string values + var that = this; + var tags = []; + if (this.options.singleField) { + tags = $(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter); + if (tags[0] === '') { + tags = []; + } + } else { + this._tags().each(function() { + tags.push(that.tagLabel(this)); + }); + } + return tags; + }, + + _updateSingleTagsField: function(tags) { + // Takes a list of tag string values, updates this.options.singleFieldNode.val to the tags delimited by this.options.singleFieldDelimiter + $(this.options.singleFieldNode).val(tags.join(this.options.singleFieldDelimiter)).trigger('change'); + }, + + _subtractArray: function(a1, a2) { + var result = []; + for (var i = 0; i < a1.length; i++) { + if ($.inArray(a1[i], a2) == -1) { + result.push(a1[i]); + } + } + return result; + }, + + tagLabel: function(tag) { + // Returns the tag's string label. + if (this.options.singleField) { + return $(tag).find('.tagit-label:first').text(); + } else { + return $(tag).find('input:first').val(); + } + }, + + _showAutocomplete: function() { + this.tagInput.autocomplete('search', ''); + }, + + _findTagByLabel: function(name) { + var that = this; + var tag = null; + this._tags().each(function(i) { + if (that._formatStr(name) == that._formatStr(that.tagLabel(this))) { + tag = $(this); + return false; + } + }); + return tag; + }, + + _isNew: function(name) { + return !this._findTagByLabel(name); + }, + + _formatStr: function(str) { + if (this.options.caseSensitive) { + return str; + } + return $.trim(str.toLowerCase()); + }, + + _effectExists: function(name) { + return Boolean($.effects && ($.effects[name] || ($.effects.effect && $.effects.effect[name]))); + }, + + createTag: function(value, additionalClass, duringInitialization) { + var that = this; + + value = $.trim(value); + + if(this.options.preprocessTag) { + value = this.options.preprocessTag(value); + } + + if (value === '') { + return false; + } + + if (!this.options.allowDuplicates && !this._isNew(value)) { + var existingTag = this._findTagByLabel(value); + if (this._trigger('onTagExists', null, { + existingTag: existingTag, + duringInitialization: duringInitialization + }) !== false) { + if (this._effectExists('highlight')) { + existingTag.effect('highlight'); + } + } + return false; + } + + if (this.options.tagLimit && this._tags().length >= this.options.tagLimit) { + this._trigger('onTagLimitExceeded', null, {duringInitialization: duringInitialization}); + return false; + } + + var label = $(this.options.onTagClicked ? '' : '').text(value); + + // Create tag. + var tag = $('
  • ') + .addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all') + .addClass(additionalClass) + .append(label); + + if (this.options.readOnly){ + tag.addClass('tagit-choice-read-only'); + } else { + tag.addClass('tagit-choice-editable'); + // Button for removing the tag. + var removeTagIcon = $('') + .addClass('ui-icon ui-icon-close'); + var removeTag = $('\xd7') // \xd7 is an X + .attr('role', 'button') + .attr('aria-label', `${label.text()} close`) + .addClass('tagit-close') + .append(removeTagIcon) + .click(function(e) { + // Removes a tag when the little 'x' is clicked. + that.removeTag(tag); + }); + tag.append(removeTag); + } + + // user should be able to remove tags via keyboard navigating with tab when tabindex is set + if (null != this.options.tabIndex) { + removeTag.attr("tabindex", this.options.tabIndex); + removeTag.keypress(function(e) { + if (e.which === $.ui.keyCode.ENTER || e.which === $.ui.keyCode.SPACE) { + that.removeTag(tag); + that.tagInput.focus(); // after removing the tag, set focus back to input + } + }); + } + + // Unless options.singleField is set, each tag has a hidden input field inline. + if (!this.options.singleField) { + var escapedValue = label.html(); + tag.append(''); + } + + if (this._trigger('beforeTagAdded', null, { + tag: tag, + tagLabel: this.tagLabel(tag), + duringInitialization: duringInitialization + }) === false) { + return; + } + + if (this.options.singleField) { + var tags = this.assignedTags(); + tags.push(value); + this._updateSingleTagsField(tags); + } + + // DEPRECATED. + this._trigger('onTagAdded', null, tag); + + this.tagInput.val(''); + + // Insert tag. + this.tagInput.parent().before(tag); + + this._trigger('afterTagAdded', null, { + tag: tag, + tagLabel: this.tagLabel(tag), + duringInitialization: duringInitialization + }); + + if (this.options.showAutocompleteOnFocus && !duringInitialization) { + setTimeout(function () { that._showAutocomplete(); }, 0); + } + }, + + removeTag: function(tag, animate) { + animate = typeof animate === 'undefined' ? this.options.animate : animate; + + tag = $(tag); + + // DEPRECATED. + this._trigger('onTagRemoved', null, tag); + + if (this._trigger('beforeTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)}) === false) { + return; + } + + if (this.options.singleField) { + var tags = this.assignedTags(); + var removedTagLabel = this.tagLabel(tag); + tags = $.grep(tags, function(el){ + return el != removedTagLabel; + }); + this._updateSingleTagsField(tags); + } + + if (animate) { + tag.addClass('removed'); // Excludes this tag from _tags. + var hide_args = this._effectExists('blind') ? ['blind', {direction: 'horizontal'}, 'fast'] : ['fast']; + + var thisTag = this; + hide_args.push(function() { + tag.remove(); + thisTag._trigger('afterTagRemoved', null, {tag: tag, tagLabel: thisTag.tagLabel(tag)}); + }); + + tag.fadeOut('fast').hide.apply(tag, hide_args).dequeue(); + } else { + tag.remove(); + this._trigger('afterTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)}); + } + + }, + + removeTagByLabel: function(tagLabel, animate) { + var toRemove = this._findTagByLabel(tagLabel); + if (!toRemove) { + throw "No such tag exists with the name '" + tagLabel + "'"; + } + this.removeTag(toRemove, animate); + }, + + removeAll: function() { + // Removes all tags. + var that = this; + this._tags().each(function(index, tag) { + that.removeTag(tag, false); + }); + } + + }); +})(jQuery); + diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 20979145f..8c5ef4ad3 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -5,6 +5,11 @@ import Rails from "@rails/ujs" import * as ActiveStorage from "@rails/activestorage" +import { TurboLinks } from "turbolinks" // 追記 +import $ from 'jquery' // 追記 +import 'jquery-ui/ui/widgets/autocomplete.js'; // 追記 +import 'bootstrap'; // 追記 +import 'tag-it'; // 追記 import "channels" import "calendar.js" import "theme.js" diff --git a/app/models/event.rb b/app/models/event.rb index 7bc3051a6..536a347d5 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -11,6 +11,8 @@ class Event < ApplicationRecord has_many :bookmarks, dependent: :destroy has_one_attached :thumbnail + acts_as_taggable + scope :future, -> { where('held_at > ?', Time.current) } scope :past, -> { where('held_at <= ?', Time.current) } diff --git a/app/views/events/_event.html.erb b/app/views/events/_event.html.erb index 4725d15fa..e439f7824 100644 --- a/app/views/events/_event.html.erb +++ b/app/views/events/_event.html.erb @@ -35,6 +35,11 @@ <% end %> +
    + <% event.tag_list.each do |tag| %> + <%= link_to tag, events_path(tags: tag) %> + <% end %> +
    diff --git a/app/views/events/_form.html.erb b/app/views/events/_form.html.erb index 8b97ef4e0..ba0f4d870 100644 --- a/app/views/events/_form.html.erb +++ b/app/views/events/_form.html.erb @@ -21,5 +21,9 @@ <%= f.file_field :thumbnail, class: 'form-control js-file-select-preview mb-3', accept: 'image/*', data: { target: '#preview-target' } %> <%= image_tag f.object.decorate.thumbnail, id: 'preview-target', class: 'w-100 border' %> +
    + <%= f.label :tag_list, 'タグ' %> + <%= f.text_field :tag_list, value: @event.tag_list.join(','), id: 'tags' %> +
    <%= f.submit '登録', class: 'btn btn-primary' %> <% end %> diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb index 6c7ac91d2..3bc68f29f 100644 --- a/app/views/events/show.html.erb +++ b/app/views/events/show.html.erb @@ -45,6 +45,9 @@
    <%= simple_format @event.content %> + <% @event.tag_list.each do |tag| %> + <%= link_to tag, events_path(tags: tag) %> + <% end %>
    diff --git a/app/views/shared/_header.html.erb b/app/views/shared/_header.html.erb index fc9fb960e..a91e7fde2 100644 --- a/app/views/shared/_header.html.erb +++ b/app/views/shared/_header.html.erb @@ -7,6 +7,30 @@ <% end %> -
    - <% event.tag_list.each do |tag| %> - <%= link_to tag, events_path(tags: tag) %> - <% end %> -
    diff --git a/app/views/events/_form.html.erb b/app/views/events/_form.html.erb index ba0f4d870..8b97ef4e0 100644 --- a/app/views/events/_form.html.erb +++ b/app/views/events/_form.html.erb @@ -21,9 +21,5 @@ <%= f.file_field :thumbnail, class: 'form-control js-file-select-preview mb-3', accept: 'image/*', data: { target: '#preview-target' } %> <%= image_tag f.object.decorate.thumbnail, id: 'preview-target', class: 'w-100 border' %> -
    - <%= f.label :tag_list, 'タグ' %> - <%= f.text_field :tag_list, value: @event.tag_list.join(','), id: 'tags' %> -
    <%= f.submit '登録', class: 'btn btn-primary' %> <% end %> diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb index 3bc68f29f..6c7ac91d2 100644 --- a/app/views/events/show.html.erb +++ b/app/views/events/show.html.erb @@ -45,9 +45,6 @@
    <%= simple_format @event.content %> - <% @event.tag_list.each do |tag| %> - <%= link_to tag, events_path(tags: tag) %> - <% end %>
    diff --git a/app/views/shared/_header.html.erb b/app/views/shared/_header.html.erb index a91e7fde2..fc9fb960e 100644 --- a/app/views/shared/_header.html.erb +++ b/app/views/shared/_header.html.erb @@ -7,30 +7,6 @@