1import { Controller } from "@hotwired/stimulus" 2import { ceilToTen, periodIndexOf } from "utils/rainy_periods" 3 4// é¨ã®æ¥ãã¤ã³ãã®ç¢ºèªç»é¢ï¼æ©è½ãã©ã° :rainy_day_points ã®åºï¼ã 5// 6// ã仿¥ã®é¨ã®æé帯ããå ¥ããã¨ããã®éã«æ¥ã人ã«èªåã§ãã§ãã¯ãå ¥ããï¼9/29 ttyï¼ã 7// æé帯ã¯ãã®ç«¯æ«ã®ãã©ã¦ã¶ï¼localStorageï¼ã«ãåºï¼å¶æ¥æ¥ã®ãã¼ã§ä¿åããããµã¼ãã¼ã«ã¯éããªã 8// ï¼ãã¤ã°ã¬ã¼ã·ã§ã³ãé¿ãããç¾å ´ã«ã¯ãåã端æ«ã§æä½ãã¦ãã¨ä¼ããåæï¼ã 9// äºéä»ä¸ãæ¢ããã®ã¯ãµã¼ãã¼å´ãªã®ã§ã端æ«ãã¨ã«æé帯ãéã£ã¦ãäºéã«ã¯ä»ããªãã 10// 11// æå»ã¯ãã¹ã¦ãå¶æ¥æ¥ã® 0:00 ããã®åãã§æã¤ï¼æ¥ä»ãè·¨ãã 1:00 㯠25:00 = 1500 åï¼ã 12// çµãããæªå®ï¼ã¾ã éã£ã¦ããï¼ã®æé帯ã¯ãå§ã以éã«åä»ãã人ãå ¨å¡å ¥ããã 13// å§ãã»çµããã¡ããã©ã«æ¥ã人ãé¨ã®éã«å ¥ããï¼ã客ããã«æå©ãªå´ã9/29 ttyï¼ã 14// æ¥åºæå»ã¯10ååä½ã§å¾ãã¸ããããå¤ãªã®ã§ãããã¾æ¢ãã ãããã¾ãã®çµããã¯æ¬¡ã®10åã«åãä¸ããï¼utils/rainy_periods.jsï¼ã 15// 16// æã§è¶³ãããå¤ããããããã§ãã¯ã¯ãæé帯ãå¤ããã¾ã§ãã®ã¾ã¾ãæé帯ãå¤ãããèªåã®ãã§ãã¯ãããç´ãã 17// ä»ä¸ã§æ¢ã¾ã£ã¦åºãç´ããç»é¢ï¼keepChecksï¼ã¯ãéã£ãã¨ãã®ãã§ãã¯ãæ®ãï¼å¤ããäººã«æ¼ãç´ãã§ä»ããªãããã«ï¼ 18export default class extends Controller { 19 static targets = ["panel", "summary", "row", "grantedRow", "checkbox", "tag", "submit", "cancelArea"] 20 static values = { 21 storageKey: String, 22 storagePrefix: String, 23 dayStart: Number, 24 openMinutes: Number, 25 keepChecks: Boolean, 26 } 27 28 connect() { 29 this.periods = this.load() 30 this.editing = null 31 this.cleanupOldDays() 32 this.renderPanel() 33 if (this.keepChecksValue) { 34 this.refresh() 35 } else { 36 this.applyPeriods() 37 } 38 } 39 40 // ---- ä¿åï¼å¤±æãã¦ãç»é¢ã¯åãããã®å ´ãããã®æé帯ã«ãªãã ãï¼ ---- 41 42 load() { 43 try { 44 const periods = JSON.parse(window.localStorage.getItem(this.storageKeyValue) || "[]") 45 return Array.isArray(periods) ? periods.filter((p) => Number.isInteger(p.start)) : [] 46 } catch (_e) { 47 return [] 48 } 49 } 50 51 save() { 52 this.periods.sort((a, b) => a.start - b.start) 53 try { 54 window.localStorage.setItem(this.storageKeyValue, JSON.stringify(this.periods)) 55 } catch (_e) { 56 // ã·ã¼ã¯ã¬ããã¢ã¼ããªã©ã§ä¿åã§ããªãã¦ãç¶ãã 57 } 58 } 59 60 // åã®å¶æ¥æ¥ã®æé帯ã¯è¦ããªãã®ã§æ¶ãï¼åãåºã®ãã¼ã ãï¼ 61 cleanupOldDays() { 62 try { 63 const storage = window.localStorage 64 for (let i = storage.length - 1; i >= 0; i--) { 65 const key = storage.key(i) 66 if (key && key.startsWith(this.storagePrefixValue) && key !== this.storageKeyValue) storage.removeItem(key) 67 } 68 } catch (_e) { 69 // åä¸ 70 } 71 } 72 73 // ---- æå» ---- 74 75 nowMinutes() { 76 return Math.floor((Date.now() - this.dayStartValue) / 60000) 77 } 78 79 format(minutes) { 80 const h = Math.floor(minutes / 60) % 24 81 const m = minutes % 60 82 return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}` 83 } 84 85 // ãéåºãããå§ã¾ãæéå¸¯ã¯æå»ãåºããªããéåºã®æå»ï¼å¶æ¥æ¥ãéããæå»ï¼14:44 ãªã©ï¼ã§ã 86 // å®éã®éåºï¼15:00ï¼ã¨é£ãéã£ã¦è¦ããããï¼9/29 ttyï¼ãé¨ã®ç¯å²ã¯ãã®æå»ããã§å¤ãããªã 87 startLabel(minutes) { 88 return minutes === this.openMinutesValue ? "éåº" : this.format(minutes) 89 } 90 91 // ä½çªç®ã®é¨ãï¼â â¡â¢â¦ï¼ 92 mark(index) { 93 return String.fromCharCode(0x2460 + index) 94 } 95 96 periodIndexOf(minutes) { 97 return periodIndexOf(this.periods, minutes) 98 } 99 100 // çµããã®ããã¾ãï¼æ¬¡ã®10åã«åãä¸ãããã¾ï¼13:05 â 13:10ï¼ 101 nowEndMinutes() { 102 return ceilToTen(this.nowMinutes()) 103 } 104 105 get openPeriod() { 106 return this.periods.find((p) => p.end === null) 107 } 108 109 // ---- æéå¸¯ã®æä½ ---- 110 111 startFromOpen() { 112 this.addPeriod({ start: this.openMinutesValue, end: null }) 113 } 114 115 startNow() { 116 this.addPeriod({ start: this.nowMinutes(), end: null }) 117 } 118 119 addPeriod(period) { 120 this.periods.push(period) 121 this.save() 122 this.editing = null 123 this.renderPanel() 124 this.applyPeriods() 125 } 126 127 stopNow(event) { 128 const period = this.periods[Number(event.currentTarget.dataset.index)] 129 period.end = Math.max(this.nowEndMinutes(), period.start) 130 this.save() 131 this.renderPanel() 132 this.applyPeriods() 133 } 134 135 removePeriod(event) { 136 this.periods.splice(Number(event.currentTarget.dataset.index), 1) 137 this.save() 138 this.editing = null 139 this.renderPanel() 140 this.applyPeriods() 141 } 142 143 // æ°ããè¶³ãï¼index = nullï¼ããæ¢åãç´ã 144 openEditor(event) { 145 const index = event.currentTarget.dataset.index 146 if (index === undefined) { 147 this.editing = { index: null, start: this.nowMinutes(), end: null, error: null } 148 } else { 149 const period = this.periods[Number(index)] 150 this.editing = { index: Number(index), start: period.start, end: period.end, error: null } 151 } 152 this.renderPanel() 153 } 154 155 closeEditor() { 156 this.editing = null 157 this.renderPanel() 158 } 159 160 // ãéåºãããã¾ããã¾ã éã£ã¦ãããã®1ã¿ãã 161 quick(event) { 162 const { field, preset } = event.currentTarget.dataset 163 const now = field === "end" ? this.nowEndMinutes() : this.nowMinutes() 164 const value = { open: this.openMinutesValue, now, none: null }[preset] 165 this.editing[field] = value 166 this.editing.error = null 167 this.renderPanel() 168 } 169 170 // æã»åã®ã»ã¬ã¯ããçµããã®æã ãé¸ãã§åã空ãªã 00 åã«ãã 171 pick() { 172 const read = (field) => { 173 const hour = this.panelTarget.querySelector(`[data-select="${field}-hour"]`).value 174 const minute = this.panelTarget.querySelector(`[data-select="${field}-minute"]`).value 175 if (hour === "") return null 176 return Number(hour) * 60 + Number(minute || 0) 177 } 178 this.editing.start = read("start") 179 this.editing.end = read("end") 180 this.editing.error = null 181 this.renderPanel() 182 } 183 184 commitEditor() { 185 const { index, start, end } = this.editing 186 if (start === null) { 187 this.editing.error = "å§ãã®æå»ãé¸ãã§ãã ãã" 188 } else if (end !== null && end < start) { 189 this.editing.error = "çµããã¯å§ãããå¾ã®æå»ã«ãã¦ãã ãã" 190 } else if (end === null && this.periods.some((p, i) => p.end === null && i !== index)) { 191 this.editing.error = "ãã¾ã éã£ã¦ãããæé帯ããã1ã¤ããã¾ããå ã«ãã¡ãã®ããã¾æ¢ãã ããæ¼ãã¦ãã ãã" 192 } 193 if (this.editing.error) { 194 this.renderPanel() 195 return 196 } 197 198 if (index === null) { 199 this.periods.push({ start, end }) 200 } else { 201 this.periods[index] = { start, end } 202 } 203 this.save() 204 this.editing = null 205 this.renderPanel() 206 this.applyPeriods() 207 } 208 209 // ---- ãã§ã㯠---- 210 211 // æé帯ããèªåã§ãã§ãã¯ãå ¥ãç´ãï¼æã§å¤ãããã§ãã¯ã䏿¸ãããï¼ 212 applyPeriods() {
213 this.rowTargets.forEach((row) => { 214 const checkbox = row.querySelector("input[type=checkbox]") 215 checkbox.checked = this.periodIndexOf(Number(row.dataset.entryMinutes)) >= 0 216 }) 217 this.refresh() 218 } 219 220 // ã¿ã°ã»è¡ã®è²ã»ä»¶æ°ã»ãã¿ã³ã®è¡¨ç¤ºãããã¾ã®ãã§ãã¯ã«åããã 221 refresh() { 222 let checked = 0 223 let inRain = 0 224 this.rowTargets.forEach((row) => { 225 const checkbox = row.querySelector("input[type=checkbox]") 226 const index = this.periodIndexOf(Number(row.dataset.entryMinutes)) 227 const tag = row.querySelector("[data-rainy-day-points-target=tag]") 228 tag.innerHTML = index >= 0 ? `<span class="rainy-points-tag">é¨${this.mark(index)}ã«æ¥åº</span>` : "" 229 row.classList.toggle("is-off", !checkbox.checked) 230 if (checkbox.checked) checked++ 231 if (index >= 0) inRain++ 232 }) 233 const grantedInRain = this.grantedRowTargets.filter((row) => this.periodIndexOf(Number(row.dataset.entryMinutes)) >= 0).length 234 235 if (this.hasSummaryTarget) { 236 this.summaryTarget.innerHTML = this.periods.length === 0 237 ? "é¨ã®æé帯ãå ¥ããã¨ããã®éã«æ¥ã人ã«èªåã§ãã§ãã¯ãå ¥ãã¾ãï¼æã§ãã§ãã¯ãã¦ãä»ãããã¾ãï¼ã" 238 : `é¨ã®éã«æ¥ã人 <b>${inRain + grantedInRain}人</b>ï¼ãã¡ä»ä¸æ¸ã¿ ${grantedInRain}人ï¼â <b>${checked}人</b>ã«ãã§ãã¯ãå ¥ã£ã¦ãã¾ãããã§ãã¯ã¯æã§è¶³ãããå¤ãããã§ãã¾ãã` 239 } 240 if (this.hasSubmitTarget) { 241 this.submitTarget.textContent = `ãã§ãã¯ãã${checked}人ã«ä»ãã` 242 this.submitTarget.disabled = checked === 0 243 } 244 } 245 246 // ---- åãæ¶ãï¼è¡ã®ä¸ã§ç¢ºãããï¼ ---- 247 248 askCancel(event) { 249 const cell = event.currentTarget.closest("td") 250 cell.querySelector("[data-rainy-day-points-target=cancelArea]").hidden = true 251 cell.querySelector(".rainy-points-cancel-confirm").hidden = false 252 } 253 254 closeCancel(event) { 255 const cell = event.currentTarget.closest("td") 256 cell.querySelector("[data-rainy-day-points-target=cancelArea]").hidden = false 257 cell.querySelector(".rainy-points-cancel-confirm").hidden = true 258 } 259 260 // ---- ããã«ã®æç» ---- 261 262 renderPanel() { 263 const head = `<div class="rainy-periods-head">â 仿¥ã®é¨ã®æé帯<small>ãã®ç«¯æ«ã«ä¿åãã¾ã</small></div>` 264 const now = this.format(this.nowMinutes()) 265 266 if (this.periods.length === 0 && !this.editing) { 267 this.panelTarget.innerHTML = `${head} 268 <div>ãã®ç«¯æ«ã«ã¯ã仿¥ã®é¨ã®æé帯ãã¾ã ããã¾ããããã¾ã®ç¶æ³ãé¸ãã§ãã ããã</div> 269 <div class="rainy-periods-choices"> 270 <button type="button" class="stores-btn stores-btn-secondary stores-btn-size-medium" data-action="rainy-day-points#startFromOpen">â éåºããéã£ã¦ãã</button> 271 <button type="button" class="stores-btn stores-btn-secondary stores-btn-size-medium" data-action="rainy-day-points#startNow">â ãã¾éãå§ããï¼${now}ãï¼</button> 272 <button type="button" class="stores-btn stores-btn-secondary stores-btn-size-medium" data-action="rainy-day-points#openEditor">æå»ãé¸ãã§å ¥ãã</button> 273 </div>` 274 return 275 } 276 277 const rows = this.periods.map((p, i) => { 278 if (this.editing && this.editing.index === i) return "" 279 const live = p.end === null 280 return `<div class="rainy-periods-row"> 281 <span class="rainy-periods-no">${this.mark(i)}</span> 282 <span class="rainy-periods-time">${this.startLabel(p.start)} ã ${live ? "" : this.format(p.end)}${live ? '<span class="rainy-periods-live">â ãã¾ãéã£ã¦ãã</span>' : ""}</span> 283 <span class="rainy-periods-spacer"></span> 284 ${live ? `<button type="button" class="stores-btn stores-btn-secondary stores-btn-size-small" data-action="rainy-day-points#stopNow" data-index="${i}">ãã¾æ¢ãã ï¼${now}ï¼</button>` : ""} 285 <button type="button" class="stores-btn-text stores-btn-text-edit" data-action="rainy-day-points#openEditor" data-index="${i}">ç´ã</button> 286 <button type="button" class="stores-btn-text stores-btn-text-delete" data-action="rainy-day-points#removePeriod" data-index="${i}">æ¶ã</button> 287 </div>` 288 }).join("") 289 290 let footer = "" 291 if (this.editing) { 292 footer = this.editorHtml(now) 293 } else if (!this.openPeriod) { 294 footer = `<div class="rainy-periods-add"><button type="button" class="stores-btn stores-btn-secondary stores-btn-size-small" data-action="rainy-day-points#openEditor">ï¼ ã¾ãéãå§ããï¼æé帯ãè¶³ãï¼</button></div>` 295 } 296 this.panelTarget.innerHTML = `${head}${rows}${footer}` 297 } 298 299 editorHtml(now) { 300 const { start, end, error } = this.editing 301 const on = (active) => (active ? " is-on" : "") 302 const nowMinutes = this.nowMinutes() 303 return `<div class="rainy-periods-editor"> 304 <div class="rainy-periods-editor-line"><b>å§ã</b> 305 <button type="button" class="rainy-periods-quick${on(start === this.openMinutesValue)}" data-action="rainy-day-points#quick" data-field="start" data-preset="open">éåº</button> 306 <button type="button" class="rainy-periods-quick${on(start === nowMinutes)}" data-action="rainy-day-points#quick" data-field="start" data-preset="now">ãã¾ï¼${now}ï¼</button> 307 ${this.timeSelects("start", start)} 308 </div> 309 <div class="rainy-periods-editor-line"><b>çµãã</b> 310 <button type="button" class="rainy-periods-quick${on(end === null)}" data-action="rainy-day-points#quick" data-field="end" data-preset="none">ã¾ã éã£ã¦ãã</button> 311 <button type="button" class="rainy-periods-quick${on(end !== null && end === this.nowEndMinutes())}" data-action="rainy-day-points#quick" data-field="end" data-preset="now">ãã¾ï¼${now}ï¼</button> 312 ${this.timeSelects("end", end)} 313 </div> 314 ${error ? `<div class="stores-flash-message stores-flash-warning rainy-periods-editor-note">${error}</div>` : ""} 315 <button type="button" class="stores-btn stores-btn-primary stores-btn-size-small" data-action="rainy-day-points#commitEditor">ãã®æé帯ã«ãã</button> 316 <button type="button" class="stores-btn stores-btn-tertiary stores-btn-size-small" data-action="rainy-day-points#closeEditor">ããã</button> 317 </div>` 318 } 319 320 // æã¯éåºã®æãã 14 æéåï¼æ¥ä»ãè·¨ãã ã 24 ãè¶³ããå¤ã§æã¤ï¼ãå㯠5 åå»ã¿ï¼ãã¾ã®æå»ãªã
320©ã®å端ãªåãåºãï¼ 321 timeSelects(field, value) { 322 const openHour = Math.floor(this.openMinutesValue / 60) 323 const hours = Array.from({ length: 15 }, (_, i) => openHour + i) 324 const minutes = Array.from({ length: 12 }, (_, i) => i * 5) 325 if (value !== null && !minutes.includes(value % 60)) minutes.push(value % 60) 326 minutes.sort((a, b) => a - b) 327 const hourValue = value === null ? "" : Math.floor(value / 60) 328 const minuteValue = value === null ? "" : value % 60 329 const option = (v, label, selected) => `<option value="${v}"${selected ? " selected" : ""}>${label}</option>` 330 const hourOptions = [option("", "--", hourValue === ""), ...hours.map((h) => option(h, String(h % 24).padStart(2, "0"), h === hourValue))].join("") 331 const minuteOptions = [option("", "--", minuteValue === ""), ...minutes.map((m) => option(m, String(m).padStart(2, "0"), m === minuteValue))].join("") 332 return `<select class="form-select form-select-sm" data-select="${field}-hour" data-action="rainy-day-points#pick">${hourOptions}</select> : 333 <select class="form-select form-select-sm" data-select="${field}-minute" data-action="rainy-day-points#pick">${minuteOptions}</select>` 334 } 335};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.