PageSourceSearch

https://www.gamestrat.com/assets/controllers/form_image_controller…dcd7d07e58b8f1c2661eba197e4fe40bf802a0a5.js

js gamestrat.com collected 2026-09-24 18:04:42 UTC 6,382 bytes, 187 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2import _ from "lodash"
3
4const croppieScriptURL = "https://cdnjs.cloudflare.com/ajax/libs/croppie/2.6.5/croppie.min.js"
5const croppieStyleURL = "https://cdnjs.cloudflare.com/ajax/libs/croppie/2.6.5/croppie.min.css"
6const imageTypes = new Set(["image/png", "image/jpg", "image/jpeg", "image/webp"])
7
8export default class extends Controller {
9  static targets = ["errorLabel", "imageInput", "purgedInput", "previewImage", "placeholder"]
10
11  static values = {
12    maxSize: String,
13    cropShape: String,
14  }
15
16  get isCroppieLoaded() {
17    return document.head.querySelector(`script[src="${croppieScriptURL}"]`) !== null
18  }
19
20  get maxSize() {
21    console.log("MAX SIZE", this.maxSizeValue)
22    // Default to 5 MB
23    if (!this.hasMaxSizeValue) return 5 * Math.pow(2, 20)
24    // Parse value
25    const max = this.maxSizeValue
26    const maxNumber = parseInt(max)
27    // Invalid value, use default
28    if (isNaN(maxNumber)) return 5 * Math.pow(2, 20)
29    // Check suffix
30    const suffix = max.toLowerCase()[max.length - 1]
31    switch (suffix) {
32      case "k":
33        return maxNumber * Math.pow(2, 10) // Kilo
34      case "m":
35        return maxNumber * Math.pow(2, 20) // Mega
36      case "g":
37        return maxNumber * Math.pow(2, 30) // Giga
38      default:
39        return maxNumber // exact value
40    }
41  }
42
43  get maxSizeLabel() {
44    if (!this.hasMaxSizeValue) return "5 MB"
45    return this.maxSizeValue.toUpperCase().replace(/([KMG])/, " $1B")
46  }
47
48  get cropShape() {
49    if (!this.hasCropShapeValue) return "circle"
50    if (!["circle", "square"].includes(this.cropShapeValue)) return "circle"
51    return this.cropShapeValue
52  }
53
54  async loadCroppie() {
55    if (this.isCroppieLoaded) return
56    // Create script and link elements
57    const script = document.createElement("script")
58    const link = document.createElement("link")
59    link.rel = "stylesheet"
60    link.type = "text/css"
61    // Load element asynchronously
62    const loadElement = async (elem, src) => {
63      return new Promise((resolve, reject) => {
64        elem.onload = resolve
65        elem.onerror = (error) => reject(error)
66        document.head.appendChild(elem)
67        if (elem instanceof HTMLScriptElement) elem.src = src
68        else if (elem instanceof HTMLLinkElement) elem.href = src
69      })
70    }
71    // Await all resources
72    await Promise.all([loadElement(script, croppieScriptURL), loadElement(link, croppieStyleURL)])
73  }
74
75  browseFiles() {
76    this.imageInputTarget.click()
77  }
78
79  onchange(event) {
80    // Update file name
81    let image = event.target.files[0]
82    this.imageName = undefined
83    // No file selected
84    if (image == undefined) return
85    // Ensure valid file type
86    let isValid = imageTypes.has(image.type)
87    this.errorLabelTarget.classList.toggle("hidden", isValid)
88    this.errorLabelTarget.querySelector("span").innerHTML = "Invalid file type"
89    if (!isValid) return
90    // Ensure valid file size
91    isValid = image.size < this.maxSize
92    this.errorLabelTarget.classList.toggle("hidden", isValid)
93    this.errorLabelTarget.querySelector(
94      "span"
95    ).innerHTML = `File size too large. Max ${this.maxSizeLabel}.`
96    if (!isValid) return
97    // Set purged value
98    if (this.hasPurgedInputTarget) this.purgedInputTarget.value = true
99    this.imageName = image.name
100    // Update preview image
101    let reader = new FileReader()
102    reader.onload = async () => {
103      this.previewImageTarget.src = reader.result
104      this.previewImageTarget.classList.toggle("hidden", false)
105      this.placeholderTarget.classList.toggle("hidden", true)
106      // Load croppie
107      await this.loadCroppie()
108      this.removeCroppie()
109      let width = this.previewImageTarget.parentElement.clientWidth - 4
110      let height = this.previewImageTarget.parentElement.clientHeight - 4
111      const imageWidth = this.previewImageTarget.naturalWidth
112      const imageHeight = this.previewImageTarget.naturalHeight
113      if (this.cropShape == "circle") {
114        width = Math.min(width, height)
115        height = Math.min(width, height)
116      } else {
117        const widthRatio = width / imageWidth
118        const heightRatio = height / imageHeight
119        const minRatio = Math.min(widthRatio, heightRatio)
120        width = imageWidth * minRatio
121        height = imageHeight * minRatio
122      }
123      this.croppie = new Croppie(this.previewImageTarget, {
124        viewport: { width, height, type: this.cropShape },
125        showZoomer: false,
126      })
127      window.setTimeout(() => this.croppie?.setZoom(0), 20)
128    }
129    reader.readAsDataURL(image)
130  }
131
132  remove(event) {
133    // Stop default click action
134    event.preventDefault()
135    event.stopPropagation()
136    // Remove the image
137    this.removeCroppie()
138    this.previewImageTarget.src = ""
139    this.previewImageTarget.classList.toggle("hidden", true)
140    this.placeholderTarget.classList.toggle("hidden", false)
141    this.imageInputTarget.value = ""
142    if (this.purgedInputTarget) this.purgedInputTarget.value = true
143  }
144
145  removeCroppie() {
146    if (!this.croppie) return
147    // Destroy croppie instance
148    this.croppie.destroy()
149    this.croppie = undefined
150    // Move the image preview outside of the croppie div
151    const previewImage = this.previewImageTarget
152    const target = previewImage.parentElement.parentElement
153    previewImage.parentElement.remove()
154    target.appendChild(previewImage)
155  }
156
157  async beforeSubmit(event) {
158    if (this.handledSubmit) return
159    this.handledSubmit = true
160    event.preventDefault()
161    const form = event.target.closest("form")
162    const submitButton = form.querySelector("button[type='submit']")
163    if (submitButton) submitButton.disabled = true
164    // No image
165    if (!this.croppie) {
166      window.setTimeout(() => form.requestSubmit(), 100)
167      return
168    }
169    // Get blob of cropped image, create File object
170    const blob = await this.croppie.result({
171      type: "blob",
172      size: "original",
173      format: "jpeg",
174      quality: 0.8,
175    })
176    const file = new File([blob], this.imageName, {
177      type: "image/jpeg",
178      lastModified: Date.now(),
179    })
180    // Add File to DataTransfer, and set DataTransfer to file input
181    const transfer = new DataTransfer()
182    transfer.items.add(file)
183    this.imageInputTarget.files = transfer.files
184    // Submit the form
185    window.setTimeout(() => form.requestSubmit(), 100)
186  }
187};

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.