diff --git a/.ruby-version b/.ruby-version index be94e6f5..86fb6504 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.2 +3.3.7 diff --git a/CHANGELOG.md b/CHANGELOG.md index f4726054..78db54a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,14 @@ -# 4.0.0 / upcoming +# 4.0.0 / 2025-08-25 + +* [BREAKING-CHANGE] Drop support for Rails < 7.2 +* [BREAKING-CHANGE] Drop support for Ruby < 3.3.7 +* [BREAKING-CHANGE] Replace sass-rails with sassc-rails for better SCSS compilation +* [ENHANCEMENT] Update to Rails 7.2.2.2 compatibility +* [ENHANCEMENT] Update development dependencies to latest versions +* [FIX] Fix SCSS compilation issues with modern Rails versions +* [FIX] Remove deprecated cache format version configuration + +# 4.0.0.rc1 / upcoming * [BREAKING-CHANGE] Drop support for Rails < 6.1 * [BREAKING-CHANGE] Drop support for Ruby < 3.0 diff --git a/Gemfile b/Gemfile index a005be66..c340ea36 100644 --- a/Gemfile +++ b/Gemfile @@ -2,4 +2,4 @@ source "https://rubygems.org" gemspec gem "thin" -gem "rails", "~> 7.1.1" +gem "rails", "~> 7.2.0" diff --git a/lib/recurring_select/version.rb b/lib/recurring_select/version.rb index da46c1e5..2db9857e 100644 --- a/lib/recurring_select/version.rb +++ b/lib/recurring_select/version.rb @@ -1,3 +1,3 @@ module RecurringSelect - VERSION = "4.0.0.rc1" + VERSION = "4.0.0" end diff --git a/recurring_select.gemspec b/recurring_select.gemspec index 7b40b6b1..280824dd 100644 --- a/recurring_select.gemspec +++ b/recurring_select.gemspec @@ -14,14 +14,16 @@ Gem::Specification.new do |s| s.files = Dir["{app,config,db,lib}/**/*"] + ["MIT-LICENSE", "Rakefile", "README.md"] s.test_files = Dir["test/**/*"] - s.add_dependency "rails", ">= 6.1" + s.required_ruby_version = ">= 3.3.7" + + s.add_dependency "rails", ">= 7.2" s.add_dependency "ice_cube", ">= 0.11" - s.add_dependency "sass-rails", ">= 5.0" + s.add_dependency "sassc-rails", ">= 2.1" - s.add_development_dependency "bundler", ">= 1.3.5" - s.add_development_dependency "rspec-rails", ">= 2.14" - s.add_development_dependency "rspec", ">= 2.14" - s.add_development_dependency "rake", ">= 0.9.6" + s.add_development_dependency "bundler", ">= 2.0" + s.add_development_dependency "rspec-rails", ">= 7.0" + s.add_development_dependency "rspec", ">= 3.0" + s.add_development_dependency "rake", ">= 13.0" s.license = 'MIT' end diff --git a/spec/dummy/public/assets/.sprockets-manifest-81faefa25879e722e49a58e82e7bfa51.json b/spec/dummy/public/assets/.sprockets-manifest-81faefa25879e722e49a58e82e7bfa51.json new file mode 100644 index 00000000..42f0f7f9 --- /dev/null +++ b/spec/dummy/public/assets/.sprockets-manifest-81faefa25879e722e49a58e82e7bfa51.json @@ -0,0 +1 @@ +{"files":{"manifest-b4bf6e57a53c2bdb55b8998cc94cd00883793c1c37c5e5aea3ef6749b4f6d92b.js":{"logical_path":"manifest.js","mtime":"2025-08-25T13:06:57+05:30","size":2,"digest":"75a11da44c802486bc6f65640aa48a730f0f684c5c07a42ba3cd1735eb3fb070","integrity":"sha256-daEdpEyAJIa8b2VkCqSKcw8PaExcB6Qro80XNes/sHA="},"application-15d9ca8b822c59208f5e52287a40ba44e0bb9f93bdfab5a16eba9fbd4f2e0e02.css":{"logical_path":"application.css","mtime":"2025-08-25T13:06:57+05:30","size":11583,"digest":"f0d3d6561e861131caa0c9f42189ad3c2ae7343762c87e2d399cf660483ee3d9","integrity":"sha256-8NPWVh6GETHKoMn0IYmtPCrnNDdiyH4tOZz2YEg+49k="},"recurring_select/cancel-471db6625d94439b9a9232192800d7155e5ca562d1b9b7c7e33aeda8fc3c31ec.png":{"logical_path":"recurring_select/cancel.png","mtime":"2025-08-25T13:06:57+05:30","size":1091,"digest":"9bd65bf935897e779ac39efbecc48e16546b64316d60eccded14782a95451637","integrity":"sha256-m9Zb+TWJfneaw5777MSOFlRrZDFtYOzN7RR4KpVFFjc="},"recurring_select/throbber_13x13-d78857ce33850fa1e233bcef159f2da7d7e119014f06755b97c90b0186c75d12.gif":{"logical_path":"recurring_select/throbber_13x13.gif","mtime":"2025-08-25T13:06:57+05:30","size":1842,"digest":"49e4e220162277d5957d78bc2e4bd858c3fec383e91e14fee3453147c129453c","integrity":"sha256-SeTiIBYid9WVfXi8LkvYWMP+w4PpHhT+40UxR8EpRTw="},"application-81bb3ff6b16d16196438b0ba2ad6afdb00f686b69daf037cca65a72de08db3ca.js":{"logical_path":"application.js","mtime":"2025-08-25T13:06:57+05:30","size":24628,"digest":"d7cb89fad747bd3c6e789811b43518482cebbe776204e4f7c528ef5e84d5fab4","integrity":"sha256-18uJ+tdHvTxueJgRtDUYSCzrvndiBOT3xSjvXoTV+rQ="}},"assets":{"manifest.js":"manifest-b4bf6e57a53c2bdb55b8998cc94cd00883793c1c37c5e5aea3ef6749b4f6d92b.js","application.css":"application-15d9ca8b822c59208f5e52287a40ba44e0bb9f93bdfab5a16eba9fbd4f2e0e02.css","recurring_select/cancel.png":"recurring_select/cancel-471db6625d94439b9a9232192800d7155e5ca562d1b9b7c7e33aeda8fc3c31ec.png","recurring_select/throbber_13x13.gif":"recurring_select/throbber_13x13-d78857ce33850fa1e233bcef159f2da7d7e119014f06755b97c90b0186c75d12.gif","application.js":"application-81bb3ff6b16d16196438b0ba2ad6afdb00f686b69daf037cca65a72de08db3ca.js"}} \ No newline at end of file diff --git a/spec/dummy/public/assets/application-15d9ca8b822c59208f5e52287a40ba44e0bb9f93bdfab5a16eba9fbd4f2e0e02.css b/spec/dummy/public/assets/application-15d9ca8b822c59208f5e52287a40ba44e0bb9f93bdfab5a16eba9fbd4f2e0e02.css new file mode 100644 index 00000000..ef804005 --- /dev/null +++ b/spec/dummy/public/assets/application-15d9ca8b822c59208f5e52287a40ba44e0bb9f93bdfab5a16eba9fbd4f2e0e02.css @@ -0,0 +1,258 @@ +/* + * This is a manifest file that'll automatically include all the stylesheets available in this directory + * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at + * the top of the compiled file, but it's generally better to create a new file per style scope. + + +*/ +body { + background-color: #ccc; + padding: 100px; } + +form, section { + padding: 50px; + background-color: #fff; } +/* -------- resets ---------------*/ +.rs_dialog_holder { + font-size: 14px; + color: black; } + .rs_dialog_holder a { + color: black; } + .rs_dialog_holder input[type=button] { + font: small/normal Arial,sans-serif; + background: #F5F5F5; + color: #444; + border: 1px solid #ccc; + font-size: 11px; + font-weight: bold; + height: 27px; + line-height: 27px; + outline: none; + padding: 0 8px; + text-align: center; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + border-radius: 2px; + position: relative; + background-image: -webkit-linear-gradient(top, #f5f5f5, #f1f1f1); + /* Chrome 10+, Saf5.1+ */ + background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#f5f5f5), to(#f1f1f1)); + /* Saf4+, Chrome */ + background-image: -moz-linear-gradient(top, #f5f5f5, #f1f1f1); + /* FF3.6 */ + background-image: -ms-linear-gradient(top, #f5f5f5, #f1f1f1); + /* IE10 */ + background-image: -o-linear-gradient(top, #f5f5f5, #f1f1f1); + /* Opera 11.10+ */ + filter: progid:DXImageTransform.Microsoft.gradient(startColorStr='#f5f5f5',EndColorStr='#f1f1f1'); } + .rs_dialog_holder input[type=button]:hover { + border-color: #aaa; + color: #222; + background-color: #f9f9f9; + -webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.2); + -ms-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.2); + -o-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.2); + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.2); } + .rs_dialog_holder input[type=button]:focus { + border-color: #1E90FF; } + .rs_dialog_holder input[type=button]:active { + border-color: #1E90FF; } + +/*------- defaults ------------ */ +.rs_dialog_holder { + font-family: helvetica, arial, 'san-serif'; + color: #222; + font-size: 12px; } + +/*------- specifics ------------ */ +select option.bold { + font-weight: bold; + color: red; } + +.rs_dialog_holder { + display: flex; + justify-content: center; + align-items: center; + position: fixed; + left: 0px; + right: 0px; + top: 0px; + bottom: 0px; + background-color: rgba(255, 255, 255, 0.2); + z-index: 50; } + .rs_dialog_holder .rs_dialog { + background-color: #f6f6f6; + border: 1px solid #acacac; + -webkit-box-shadow: 1px 3px 8px rgba(0, 0, 0, 0.25); + -moz-box-shadow: 1px 3px 8px rgba(0, 0, 0, 0.25); + -ms-box-shadow: 1px 3px 8px rgba(0, 0, 0, 0.25); + -o-box-shadow: 1px 3px 8px rgba(0, 0, 0, 0.25); + box-shadow: 1px 3px 8px rgba(0, 0, 0, 0.25); + -moz-border-radius: 7px; + -webkit-border-radius: 7px; + border-radius: 7px; + min-width: 200px; + overflow: hidden; } + .rs_dialog_holder .rs_dialog .rs_dialog_content { + padding: 10px; } + .rs_dialog_holder .rs_dialog .rs_dialog_content h1 { + font-size: 16px; + padding: 0px; + margin: 0 0 10px 0; } + .rs_dialog_holder .rs_dialog .rs_dialog_content h1 a { + float: right; + display: inline-block; + height: 16px; + width: 16px; + background-image: url(/assets/recurring_select/cancel-471db6625d94439b9a9232192800d7155e5ca562d1b9b7c7e33aeda8fc3c31ec.png); + background-position: center; + background-repeat: no-repeat; } + .rs_dialog_holder .rs_dialog .rs_dialog_content p { + padding: 5px 0; + margin: 0; + line-height: 14px; } + .rs_dialog_holder .rs_dialog .rs_dialog_content p label { + margin-right: 10px; } + .rs_dialog_holder .rs_dialog .rs_dialog_content a { + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section { + display: none; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section label { + font-weight: bold; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section .rs_interval { + width: 30px; + text-align: center; + display: inline-block; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section .day_holder { + border-left: 1px solid #ccc; + position: relative; + margin-top: 5px; + height: 26px; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section .day_holder a { + display: block; + padding: 5px 7px; + font-size: 14px; + border-style: solid; + border-color: #ccc; + border-width: 1px 1px 1px 0px; + float: left; + text-decoration: none; + font-weight: bold; + -webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1) inset; + -moz-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1) inset; + -ms-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1) inset; + -o-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1) inset; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1) inset; + background-color: #fff; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section .day_holder a.selected { + background-color: #89a; + color: #fff; + -webkit-box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2) inset; + -moz-box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2) inset; + -ms-box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2) inset; + -o-box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2) inset; + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2) inset; + position: relative; + background-image: -webkit-linear-gradient(top, #9ab, #789); + /* Chrome 10+, Saf5.1+ */ + background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#9ab), to(#789)); + /* Saf4+, Chrome */ + background-image: -moz-linear-gradient(top, #9ab, #789); + /* FF3.6 */ + background-image: -ms-linear-gradient(top, #9ab, #789); + /* IE10 */ + background-image: -o-linear-gradient(top, #9ab, #789); + /* Opera 11.10+ */ } + .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section .day_holder a:hover { + cursor: pointer; + background-color: #dde; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section .rs_calendar_day, .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section .rs_calendar_week { + width: 155px; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section .rs_calendar_day a, .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section .rs_calendar_week a { + display: inline-block; + text-align: center; + width: 15px; + padding: 5px 3px; + font-size: 12px; + border-style: solid; + border-color: #ccc; + border-width: 1px 1px 1px 1px; + margin: -1px 0 0 -1px; + line-height: 10px; + background-color: #fff; + font-weight: bold; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section .rs_calendar_day a.selected, .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section .rs_calendar_week a.selected { + background-color: #89a; + color: #fff; + position: relative; + background-image: -webkit-linear-gradient(top, #9ab, #789); + /* Chrome 10+, Saf5.1+ */ + background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#9ab), to(#789)); + /* Saf4+, Chrome */ + background-image: -moz-linear-gradient(top, #9ab, #789); + /* FF3.6 */ + background-image: -ms-linear-gradient(top, #9ab, #789); + /* IE10 */ + background-image: -o-linear-gradient(top, #9ab, #789); + /* Opera 11.10+ */ + -webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.2) inset; + -moz-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.2) inset; + -ms-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.2) inset; + -o-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.2) inset; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.2) inset; + text-shadow: 0 1px 1px #333; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section .rs_calendar_day a:hover, .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section .rs_calendar_week a:hover { + cursor: pointer; + background-color: #dde; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section .rs_calendar_day a.end_of_month, .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section .rs_calendar_week a.end_of_month { + width: 81px; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section .rs_calendar_week { + width: 191px; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section .rs_calendar_week span { + display: inline-block; + width: 35px; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .freq_option_section .monthly_rule_type span { + margin-right: 15px; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .rs_summary { + padding: 0px; + margin-top: 15px; + border-top: 1px solid #ccc; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .rs_summary span { + font-weight: bold; + border-top: 1px solid #fff; + display: block; + padding: 10px 0 5px 0; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .rs_summary.fetching { + color: #999; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .rs_summary.fetching span { + background-image: url(/assets/recurring_select/throbber_13x13-d78857ce33850fa1e233bcef159f2da7d7e119014f06755b97c90b0186c75d12.gif); + background-position: center; + background-repeat: no-repeat; + display: inline-block; + height: 13px; + width: 13px; + margin-top: -4px; + padding-right: 5px; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .rs_summary label { + font-weight: normal; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .controls { + padding: 10px 0px 5px 0px; + min-width: 170px; + text-align: center; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .controls input[type=button] { + margin: 0px 5px; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .controls input.rs_save[type=button] { + color: #333; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .controls input.rs_cancel[type=button] { + color: #666; } + .rs_dialog_holder .rs_dialog .rs_dialog_content .controls input.disabled[type=button] { + color: #aaa; } + .rs_dialog_holder .rs_dialog.animated .controls { + position: absolute; + bottom: 10px; + left: 10px; } + .rs_dialog_holder .rs_dialog.animated .rs_summary, .rs_dialog_holder .rs_dialog.animated .freq_option_section { + display: none; } diff --git a/spec/dummy/public/assets/application-15d9ca8b822c59208f5e52287a40ba44e0bb9f93bdfab5a16eba9fbd4f2e0e02.css.gz b/spec/dummy/public/assets/application-15d9ca8b822c59208f5e52287a40ba44e0bb9f93bdfab5a16eba9fbd4f2e0e02.css.gz new file mode 100644 index 00000000..66d71611 Binary files /dev/null and b/spec/dummy/public/assets/application-15d9ca8b822c59208f5e52287a40ba44e0bb9f93bdfab5a16eba9fbd4f2e0e02.css.gz differ diff --git a/spec/dummy/public/assets/application-81bb3ff6b16d16196438b0ba2ad6afdb00f686b69daf037cca65a72de08db3ca.js b/spec/dummy/public/assets/application-81bb3ff6b16d16196438b0ba2ad6afdb00f686b69daf037cca65a72de08db3ca.js new file mode 100644 index 00000000..e05c4956 --- /dev/null +++ b/spec/dummy/public/assets/application-81bb3ff6b16d16196438b0ba2ad6afdb00f686b69daf037cca65a72de08db3ca.js @@ -0,0 +1,656 @@ +function css(el, styles) { + for (let rule in styles) { + el.style[rule] = styles[rule] + } +} + +function trigger(el, eventName) { + el.dispatchEvent(new CustomEvent(eventName)) +} + +function isPlainObject(obj) { + return obj && obj.toString() === "[object Object]" +} + +const eventHandlerRefsExpando = '__recurring_select_events' + +function on(el, events, sel, handler) { + let eventHandler = sel + if (handler) { + eventHandler = (e) => { + if (e.target.matches(sel)) { + if (handler.call(this, e) === false) { + e.preventDefault() + e.stopPropagation() + } + } + } + } + + el[eventHandlerRefsExpando] = el[eventHandlerRefsExpando] || [] + + events.trim().split(/ +/).forEach(type => { + el[eventHandlerRefsExpando].push([ type, eventHandler ]) + el.addEventListener(type, eventHandler) + }) +} + +function off(el, events) { + const types = events.trim().split(/ +/) + + el[eventHandlerRefsExpando] = (el[eventHandlerRefsExpando] || []) + .filter(([t, h], i) => { + if (types.includes(t)) { + el.removeEventListener(t, h) + return false + } + return true + }) +} + +function serialize(params, prefix) { + const query = Object.keys(params).map((key) => { + const value = params[key]; + + if (params.constructor === Array) + key = `${prefix}[]`; + else if (params.constructor === Object) + key = (prefix ? `${prefix}[${key}]` : key); + + if (value === null) + return `${key}=` + + if (typeof value === 'object') + return serialize(value, key); + else + return `${key}=${encodeURIComponent(value)}`; + }); + + return [].concat.apply([], query).join('&'); +}; +const defaultConfig = { + options: { + monthly: { + show_week: [true, true, true, true, false, false] + } + }, + texts: { + locale_iso_code: "en", + repeat: "Repeat", + last_day: "Last Day", + frequency: "Frequency", + daily: "Daily", + weekly: "Weekly", + monthly: "Monthly", + yearly: "Yearly", + every: "Every", + days: "day(s)", + weeks_on: "week(s) on", + months: "month(s)", + years: "year(s)", + day_of_month: "Day of month", + day_of_week: "Day of week", + cancel: "Cancel", + ok: "OK", + summary: "Summary", + first_day_of_week: 0, + days_first_letter: ["S", "M", "T", "W", "T", "F", "S" ], + order: ["1st", "2nd", "3rd", "4th", "5th", "Last"], + show_week: [true, true, true, true, false, false] + } +}; + + + +class RecurringSelectDialog { + constructor(recurring_selector) { + this.config = this.constructor.config + this.cancel = this.cancel.bind(this); + this.outerCancel = this.outerCancel.bind(this); + this.save = this.save.bind(this); + this.summaryUpdate = this.summaryUpdate.bind(this); + this.summaryFetchSuccess = this.summaryFetchSuccess.bind(this); + this.init_calendar_days = this.init_calendar_days.bind(this); + this.init_calendar_weeks = this.init_calendar_weeks.bind(this); + this.toggle_month_view = this.toggle_month_view.bind(this); + this.freqChanged = this.freqChanged.bind(this); + this.intervalChanged = this.intervalChanged.bind(this); + this.daysChanged = this.daysChanged.bind(this); + this.dateOfMonthChanged = this.dateOfMonthChanged.bind(this); + this.weekOfMonthChanged = this.weekOfMonthChanged.bind(this); + this.recurring_selector = recurring_selector; + this.current_rule = this.recurring_selector.recurring_select('current_rule'); + this.initDialogBox(); + if ((this.current_rule.hash == null) || (this.current_rule.hash.rule_type == null)) { + this.freqChanged(); + } + } + + initDialogBox() { + document.querySelectorAll(".rs_dialog_holder").forEach(el => el.parentNode.removeChild(el)) + + const uiPage = document.querySelector('.ui-page-active') + const anchor = uiPage ? uiPage : document.body + + const div = document.createElement("div") + div.innerHTML = this.template() + anchor.appendChild(div.children[0]) + + this.outer_holder = document.querySelector(".rs_dialog_holder"); + this.inner_holder = this.outer_holder.querySelector(".rs_dialog"); + this.content = this.outer_holder.querySelector(".rs_dialog_content"); + + this.mainEventInit(); + this.freqInit(); + this.summaryInit(); + trigger(this.outer_holder, "recurring_select:dialog_opened"); + this.freq_select.focus(); + } + + cancel() { + this.outer_holder.remove(); + this.recurring_selector.recurring_select('cancel'); + } + + outerCancel(event) { + if (event.target.classList.contains("rs_dialog_holder")) { + this.cancel(); + } + } + + save() { + if ((this.current_rule.str == null)) { return; } + this.outer_holder.remove(); + this.recurring_selector.recurring_select('save', this.current_rule); + } + +// ========================= Init Methods =============================== + + mainEventInit() { + // Tap hooks are for jQueryMobile + on(this.outer_holder, 'click tap', this.outerCancel); + on(this.content, 'click tap', 'h1 a', this.cancel); + this.save_button = this.content.querySelector('input.rs_save') + on(this.save_button, "click tap", this.save) + on(this.content.querySelector('input.rs_cancel'), "click tap", this.cancel) + } + + freqInit() { + this.freq_select = this.outer_holder.querySelector(".rs_frequency"); + const rule_type = this.current_rule.hash && this.current_rule.hash.rule_type + if (this.current_rule.hash != null && rule_type != null) { + if (rule_type.search(/Weekly/) !== -1) { + this.freq_select.selectedIndex = 1 + this.initWeeklyOptions(); + } else if (rule_type.search(/Monthly/) !== -1) { + this.freq_select.selectedIndex = 2 + this.initMonthlyOptions(); + } else if (rule_type.search(/Yearly/) !== -1) { + this.freq_select.selectedIndex = 3 + this.initYearlyOptions(); + } else { + this.initDailyOptions(); + } + } + on(this.freq_select, "change", this.freqChanged); + } + + initDailyOptions() { + const section = this.content.querySelector('.daily_options') + const interval_input = section.querySelector('.rs_daily_interval') + interval_input.value = this.current_rule.hash.interval + on(interval_input, "change keyup", this.intervalChanged); + section.style.display = 'block' + } + + initWeeklyOptions() { + const section = this.content.querySelector('.weekly_options'); + + // connect the interval field + const interval_input = section.querySelector('.rs_weekly_interval'); + interval_input.value = this.current_rule.hash.interval + on(interval_input, "change keyup", this.intervalChanged); + + // clear selected days + section.querySelectorAll(".day_holder a").forEach(el => + el.classList.remove("selected") + ) + + // connect the day fields + if ((this.current_rule.hash.validations != null) && (this.current_rule.hash.validations.day != null)) { + Array.from(this.current_rule.hash.validations.day).forEach((val) => + section.querySelector(".day_holder a[data-value='"+val+"']").classList.add("selected") + ) + } + + off(section, "click") + on(section, "click", ".day_holder a", this.daysChanged) + + section.style.display = 'block' + } + + initMonthlyOptions() { + const section = this.content.querySelector('.monthly_options') + const interval_input = section.querySelector('.rs_monthly_interval') + interval_input.value = this.current_rule.hash.interval + on(interval_input, "change keyup", this.intervalChanged) + + if (!this.current_rule.hash.validations) { this.current_rule.hash.validations = {} }; + if (!this.current_rule.hash.validations.day_of_month) { this.current_rule.hash.validations.day_of_month = [] }; + if (!this.current_rule.hash.validations.day_of_week) { this.current_rule.hash.validations.day_of_week = {} }; + this.init_calendar_days(section); + this.init_calendar_weeks(section); + + const in_week_mode = Object.keys(this.current_rule.hash.validations.day_of_week).length > 0; + section.querySelector(".monthly_rule_type_week").checked = in_week_mode + section.querySelector(".monthly_rule_type_day").checked = !in_week_mode; + this.toggle_month_view(); + section.querySelectorAll("input[name=monthly_rule_type]").forEach((el) => on(el, "change", this.toggle_month_view)) + section.style.display = 'block' + } + + initYearlyOptions() { + const section = this.content.querySelector('.yearly_options'); + const interval_input = section.querySelector('.rs_yearly_interval'); + interval_input.value = this.current_rule.hash.interval + on(interval_input, "change keyup", this.intervalChanged) + section.style.display = 'block' + } + + + summaryInit() { + this.summary = this.outer_holder.querySelector(".rs_summary"); + this.summaryUpdate(); + } + +// ========================= render methods =============================== + + summaryUpdate(new_string) { + // this.summary.style.width = `${this.content.getBoundingClientRect().width}px`; + if ((this.current_rule.hash != null) && (this.current_rule.str != null)) { + this.summary.classList.remove("fetching"); + this.save_button.classList.remove("disabled"); + let rule_str = this.current_rule.str.replace("*", ""); + if (rule_str.length < 20) { + rule_str = `${this.config.texts["summary"]}: `+rule_str; + } + this.summary.querySelector("span").textContent = rule_str + } else { + this.summary.classList.add("fetching"); + this.save_button.classList.add("disabled"); + this.summary.querySelector("span").textContent = "" + this.summaryFetch(); + } + } + + summaryFetch() { + if (!(this.current_rule.hash != null && this.current_rule.hash.rule_type != null)) { return; } + this.current_rule.hash['week_start'] = this.config.texts["first_day_of_week"]; + + const url = `/recurring_select/translate/${this.config.texts["locale_iso_code"]}` + const headers = { 'X-Requested-With' : 'XMLHttpRequest', 'Content-Type' : 'application/x-www-form-urlencoded' } + const body = serialize(this.current_rule.hash) + console.log(this.current_rule.hash, body) + + fetch(url, { method: "POST", body, headers }) + .then(r => r.text()) + .then(this.summaryFetchSuccess) + } + + summaryFetchSuccess(data) { + this.current_rule.str = data + this.summaryUpdate() + css(this.content, { width: "auto" }) + } + + init_calendar_days(section) { + const monthly_calendar = section.querySelector(".rs_calendar_day"); + monthly_calendar.innerHTML = ""; + for (let num = 1; num <= 31; num++) { + const day_link = document.createElement("a") + day_link.innerText = num + monthly_calendar.appendChild(day_link) + if (Array.from(this.current_rule.hash.validations.day_of_month).includes(num)) { + day_link.classList.add("selected"); + } + }; + + // add last day of month button + const end_of_month_link = document.createElement("a") + end_of_month_link.innerText = this.config.texts["last_day"] + monthly_calendar.appendChild(end_of_month_link); + end_of_month_link.classList.add("end_of_month"); + if (Array.from(this.current_rule.hash.validations.day_of_month).includes(-1)) { + end_of_month_link.classList.add("selected"); + } + + off(monthly_calendar, "click tap") + on(monthly_calendar, "click tap", "a", this.dateOfMonthChanged) + } + + init_calendar_weeks(section) { + const monthly_calendar = section.querySelector(".rs_calendar_week") + monthly_calendar.innerHTML = "" + const row_labels = this.config.texts["order"]; + const show_row = this.config.options["monthly"]["show_week"]; + const cell_str = this.config.texts["days_first_letter"]; + + const iterable = [1, 2, 3, 4, 5, -1] + for (let index = 0; index < iterable.length; index++) { + const num = iterable[index]; + if (show_row[index]) { + const el = document.createElement("span") + el.innerText = row_labels[index] + monthly_calendar.appendChild(el); + for (let i = this.config.texts["first_day_of_week"], day_of_week = i, end = 7 + this.config.texts["first_day_of_week"], asc = this.config.texts["first_day_of_week"] <= end; asc ? i < end : i > end; asc ? i++ : i--, day_of_week = i) { + day_of_week = day_of_week % 7; + const day_link = document.createElement("a") + day_link.innerText = cell_str[day_of_week] + day_link.setAttribute("day", day_of_week); + day_link.setAttribute("instance", num); + monthly_calendar.appendChild(day_link); + }; + } + }; + + Object.entries(this.current_rule.hash.validations.day_of_week).forEach(([key, value]) => { + Array.from(value).forEach((instance, index) => { + section.querySelector(`a[day='${key}'][instance='${instance}']`).classList.add("selected") + }) + }) + + off(monthly_calendar, "click tap") + on(monthly_calendar, "click tap", "a", this.weekOfMonthChanged) + } + + toggle_month_view() { + const week_mode = this.content.querySelector(".monthly_rule_type_week").checked + if (week_mode) { + this.content.querySelector(".rs_calendar_week").style.display = "block" + this.content.querySelector(".rs_calendar_day").style.display = "none" + } else { + this.content.querySelector(".rs_calendar_week").style.display = "none" + this.content.querySelector(".rs_calendar_day").style.display = "block" + } + } + +// ========================= Change callbacks =============================== + + freqChanged() { + if (!isPlainObject(this.current_rule.hash)) { this.current_rule.hash = null; } // for custom values + + if (!this.current_rule.hash) { this.current_rule.hash = {} }; + this.current_rule.hash.interval = 1; + this.current_rule.hash.until = null; + this.current_rule.hash.count = null; + this.current_rule.hash.validations = null; + this.content.querySelectorAll(".freq_option_section").forEach(el => el.style.display = 'none') + this.content.querySelector("input[type=radio], input[type=checkbox]").checked = false + switch (this.freq_select.value) { + case "Weekly": + this.current_rule.hash.rule_type = "IceCube::WeeklyRule"; + this.current_rule.str = this.config.texts["weekly"]; + this.initWeeklyOptions(); + break + case "Monthly": + this.current_rule.hash.rule_type = "IceCube::MonthlyRule"; + this.current_rule.str = this.config.texts["monthly"]; + this.initMonthlyOptions(); + break + case "Yearly": + this.current_rule.hash.rule_type = "IceCube::YearlyRule"; + this.current_rule.str = this.config.texts["yearly"]; + this.initYearlyOptions(); + break + default: + this.current_rule.hash.rule_type = "IceCube::DailyRule"; + this.current_rule.str = this.config.texts["daily"]; + this.initDailyOptions(); + }; + this.summaryUpdate(); + } + + intervalChanged(event) { + this.current_rule.str = null; + if (!this.current_rule.hash) { this.current_rule.hash = {} }; + this.current_rule.hash.interval = parseInt(event.currentTarget.value); + if ((this.current_rule.hash.interval < 1) || isNaN(this.current_rule.hash.interval)) { + this.current_rule.hash.interval = 1; + } + this.summaryUpdate(); + } + + daysChanged(event) { + event.target.classList.toggle("selected"); + this.current_rule.str = null; + if (!this.current_rule.hash) { this.current_rule.hash = {} }; + this.current_rule.hash.validations = {}; + const raw_days = Array.from(this.content.querySelectorAll(".day_holder a.selected")) + .map(el => parseInt(el.dataset.value)) + this.current_rule.hash.validations.day = raw_days + this.summaryUpdate(); + return false; + } + + dateOfMonthChanged(event) { + event.target.classList.toggle("selected"); + this.current_rule.str = null; + if (!this.current_rule.hash) { this.current_rule.hash = {} }; + this.current_rule.hash.validations = {}; + const raw_days = Array.from(this.content.querySelectorAll(".monthly_options .rs_calendar_day a.selected")) + .map(el => { + return el.innerText === this.config.texts["last_day"] ? -1 : parseInt(el.innerText) + }) + this.current_rule.hash.validations.day_of_week = {}; + this.current_rule.hash.validations.day_of_month = raw_days; + this.summaryUpdate(); + return false; + } + + weekOfMonthChanged(event) { + event.target.classList.toggle("selected"); + this.current_rule.str = null; + if (!this.current_rule.hash) { this.current_rule.hash = {} }; + this.current_rule.hash.validations = {}; + this.current_rule.hash.validations.day_of_month = []; + this.current_rule.hash.validations.day_of_week = {}; + this.content.querySelectorAll(".monthly_options .rs_calendar_week a.selected") + .forEach((elm, index) => { + const day = parseInt(elm.getAttribute("day")); + const instance = parseInt(elm.getAttribute("instance")); + if (!this.current_rule.hash.validations.day_of_week[day]) { this.current_rule.hash.validations.day_of_week[day] = [] }; + return this.current_rule.hash.validations.day_of_week[day].push(instance); + }) + this.summaryUpdate(); + return false; + } + +// ========================= Change callbacks =============================== + + template() { + let str = `\ +
\ + `; + + return str; + } +} + +RecurringSelectDialog.config = defaultConfig + +window.RecurringSelectDialog = RecurringSelectDialog; + + + +document.addEventListener("DOMContentLoaded", () => { + document.addEventListener("focusin", (e) => { + if (e.target.matches(".recurring_select")) { + recurring_select.call(e.target, "set_initial_values") + } + }) + + document.addEventListener("input", (e) => { + if (e.target.matches(".recurring_select")) { + recurring_select.call(e.target, "changed") + } + }) +}) + +const methods = { + set_initial_values() { + const str = this.querySelectorAll('option')[this.selectedIndex].textContent + this.setAttribute('data-initial-value-hash', this.value); + this.setAttribute('data-initial-value-str', str); + }, + + changed() { + if (this.value == "custom") { + methods.open.call(this); + } else { + methods.set_initial_values.call(this); + } + }, + + open() { + this.setAttribute("data-recurring-select-active", true); + new RecurringSelectDialog(this); + this.blur(); + }, + + save(new_rule) { + this.querySelectorAll("option[data-custom]").forEach((el) => el.parentNode.removeChild(el) ) + const new_json_val = JSON.stringify(new_rule.hash) + + // TODO: check for matching name, and replace that value if found + + const options = Array.from(this.querySelectorAll("option")).map(() => this.value) + if (!options.includes(new_json_val)) { + methods.insert_option.apply(this, [new_rule.str, new_json_val]) + } + + this.value = new_json_val + methods.set_initial_values.apply(this) + this.dispatchEvent(new CustomEvent("recurring_select:save")) + }, + + current_rule() { + return { + str: this.getAttribute("data-initial-value-str"), + hash: JSON.parse(this.getAttribute("data-initial-value-hash")) + }; + }, + + cancel() { + this.value = this.getAttribute("data-initial-value-hash") + this.setAttribute("data-recurring-select-active", false); + this.dispatchEvent(new CustomEvent("recurring_select:cancel")) + }, + + + insert_option(new_rule_str, new_rule_json) { + let separator = this.querySelectorAll("option[disabled]"); + if (separator.length === 0) { + separator = this.querySelectorAll("option"); + } + separator = separator[separator.length-1] + + const new_option = document.createElement("option") + new_option.setAttribute("data-custom", true); + + if (new_rule_str.substr(new_rule_str.length - 1) !== "*") { + new_rule_str+="*"; + } + + new_option.textContent = new_rule_str + new_option.value = new_rule_json + separator.parentNode.insertBefore(new_option, separator) + } +}; + +function recurring_select(method) { + this['recurring_select'] = this['recurring_select'] || recurring_select.bind(this) + if (method in methods) { + return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ) ); + } else { + throw new Error( `Method ${method} does not exist on recurring_select` ); + } +}; +// This is a manifest file that'll be compiled into including all the files listed below. +// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +// be included in the compiled file accessible from http://example.com/assets/application.js +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// the compiled file. +// + + + +RecurringSelectDialog.config.options.monthly = { + show_week: [true, true, true, true, true, true] +}; diff --git a/spec/dummy/public/assets/application-81bb3ff6b16d16196438b0ba2ad6afdb00f686b69daf037cca65a72de08db3ca.js.gz b/spec/dummy/public/assets/application-81bb3ff6b16d16196438b0ba2ad6afdb00f686b69daf037cca65a72de08db3ca.js.gz new file mode 100644 index 00000000..695ff7fa Binary files /dev/null and b/spec/dummy/public/assets/application-81bb3ff6b16d16196438b0ba2ad6afdb00f686b69daf037cca65a72de08db3ca.js.gz differ diff --git a/spec/dummy/public/assets/manifest-b4bf6e57a53c2bdb55b8998cc94cd00883793c1c37c5e5aea3ef6749b4f6d92b.js b/spec/dummy/public/assets/manifest-b4bf6e57a53c2bdb55b8998cc94cd00883793c1c37c5e5aea3ef6749b4f6d92b.js new file mode 100644 index 00000000..139597f9 --- /dev/null +++ b/spec/dummy/public/assets/manifest-b4bf6e57a53c2bdb55b8998cc94cd00883793c1c37c5e5aea3ef6749b4f6d92b.js @@ -0,0 +1,2 @@ + + diff --git a/spec/dummy/public/assets/manifest-b4bf6e57a53c2bdb55b8998cc94cd00883793c1c37c5e5aea3ef6749b4f6d92b.js.gz b/spec/dummy/public/assets/manifest-b4bf6e57a53c2bdb55b8998cc94cd00883793c1c37c5e5aea3ef6749b4f6d92b.js.gz new file mode 100644 index 00000000..08f4d241 Binary files /dev/null and b/spec/dummy/public/assets/manifest-b4bf6e57a53c2bdb55b8998cc94cd00883793c1c37c5e5aea3ef6749b4f6d92b.js.gz differ diff --git a/spec/dummy/public/assets/recurring_select/cancel-471db6625d94439b9a9232192800d7155e5ca562d1b9b7c7e33aeda8fc3c31ec.png b/spec/dummy/public/assets/recurring_select/cancel-471db6625d94439b9a9232192800d7155e5ca562d1b9b7c7e33aeda8fc3c31ec.png new file mode 100644 index 00000000..afd91524 Binary files /dev/null and b/spec/dummy/public/assets/recurring_select/cancel-471db6625d94439b9a9232192800d7155e5ca562d1b9b7c7e33aeda8fc3c31ec.png differ diff --git a/spec/dummy/public/assets/recurring_select/throbber_13x13-d78857ce33850fa1e233bcef159f2da7d7e119014f06755b97c90b0186c75d12.gif b/spec/dummy/public/assets/recurring_select/throbber_13x13-d78857ce33850fa1e233bcef159f2da7d7e119014f06755b97c90b0186c75d12.gif new file mode 100644 index 00000000..1ae1d65c Binary files /dev/null and b/spec/dummy/public/assets/recurring_select/throbber_13x13-d78857ce33850fa1e233bcef159f2da7d7e119014f06755b97c90b0186c75d12.gif differ diff --git a/spec/gemfiles/rails-7.0 b/spec/gemfiles/rails-7.0 index c6a3a43a..1783f57c 100644 --- a/spec/gemfiles/rails-7.0 +++ b/spec/gemfiles/rails-7.0 @@ -2,4 +2,4 @@ source "https://rubygems.org" gemspec path: File.expand_path("../../..", __FILE__) gem "thin" -gem "rails", "~> 7.0.0" +gem "rails", "~> 7.2.0" diff --git a/spec/gemfiles/rails-7.2 b/spec/gemfiles/rails-7.2 new file mode 100644 index 00000000..1783f57c --- /dev/null +++ b/spec/gemfiles/rails-7.2 @@ -0,0 +1,5 @@ +source "https://rubygems.org" +gemspec path: File.expand_path("../../..", __FILE__) + +gem "thin" +gem "rails", "~> 7.2.0"