Coolpc Search

原價屋快速搜尋套件-可以快速搜尋商品和儲存歷史搜尋紀錄

  1. // ==UserScript==
  2. // @name Coolpc Search
  3. // @namespace https://github.com/lovebuizel
  4. // @version v1.1.3
  5. // @description 原價屋快速搜尋套件-可以快速搜尋商品和儲存歷史搜尋紀錄
  6. // @author lovebuizel
  7. // @match *://www.coolpc.com.tw/evaluate.php
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13. class Coolpc {
  14. constructor() {
  15. this.keyword = ""
  16. this.keywords = []
  17. this.selecteds = []
  18. this.records = JSON.parse(localStorage.getItem('coolpc')) || [];
  19. this.input = null
  20. this.details = ''
  21. this.isOpenDetails = JSON.parse(localStorage.getItem('coolpc_isOpenDetails')) || false
  22. this.isAutoAddResults = JSON.parse(localStorage.getItem('coolpc_isAutoAddResults')) === null ? true : JSON.parse(localStorage.getItem('coolpc_isAutoAddResults'))
  23. this.searchResultOptions = []
  24. this.minPrice = null
  25. this.maxPrice = null
  26. }
  27.  
  28. init() {
  29. this.clearAd()
  30. this.addUI()
  31. this.removeOriginalEventListener()
  32. }
  33.  
  34. addUI() {
  35. const fDiv_table = document.querySelector('#fDiv table')
  36. const div = document.createElement("div")
  37. div.setAttribute('style', `
  38. width:100%;
  39. background:#FFCA62;
  40. `)
  41. div.innerHTML = `
  42. <div class="details" style="max-height:40vh;overflow:auto;height:${this.isOpenDetails ? '100%' : '0'};">
  43. <ul style="list-style:none;padding-left:0px;overflow:hidden;margin:0;"></ul>
  44. </div>
  45. <div style="display:flex;align-items:center;justify-content:flex-start;margin-top:5px;">
  46. <span class="openDetails" style="margin-left:20px;margin-right:20px;display:flex;align-items:center;cursor:pointer;color:blue;user-select:none;">
  47. <span style="font-size:15pt;" class="signOpenDetails">${this.isOpenDetails ? '&minus;' : '&plus;'}</span>
  48. <span style="margin-left:5px;">全部搜尋結果</span>
  49. </span>
  50. <div class="autoAddResults" style="display:flex;align-items:center;cursor:pointer;user-select:none;margin-right:20px;">
  51. <div class="autoAddResults_div" style="
  52. width:15px;
  53. height:15px;
  54. background:white;
  55. border:1px solid black;
  56. display:flex;
  57. justify-content:center;
  58. align-items:center;
  59. "></div>
  60. <span style="margin-left:5px;">搜尋後自動填入</span>
  61. </div>
  62. <input type="text" spellcheck="false" class="minPrice" style="width:70px;height:20px;padding-left:3px;flex-shrink:0;user-select:none;">
  63. <span style="margin-left:5px;margin-right:5px;">~</span>
  64. <input type="text" spellcheck="false" class="maxPrice" style="width:70px;height:20px;padding-left:3px;flex-shrink:0;user-select:none;">
  65. <span style="margin-left:5px;">價格區間內</span>
  66. </div>
  67. `
  68. fDiv_table.appendChild(div)
  69. const divDetails = document.querySelector(".details")
  70. divDetails.addEventListener('click', this.clickDetails.bind(this))
  71.  
  72. const openDetails = document.querySelector('.openDetails')
  73. const signOpenDetails = document.querySelector('.signOpenDetails')
  74. openDetails.addEventListener('click', () => {
  75. this.isOpenDetails = !this.isOpenDetails
  76. signOpenDetails.innerHTML = `${this.isOpenDetails ? '&minus;' : '&plus;'}`
  77. divDetails.style.height = `${this.isOpenDetails ? '100%' : 0}`
  78. localStorage.setItem('coolpc_isOpenDetails', this.isOpenDetails)
  79. })
  80. // 價格篩選
  81. this.minPrice = document.querySelector('.minPrice')
  82. this.maxPrice = document.querySelector('.maxPrice')
  83. this.minPrice.addEventListener('input', this.checkPrice.bind(this))
  84. this.maxPrice.addEventListener('input', this.checkPrice.bind(this))
  85.  
  86. const divSearch = document.createElement("div")
  87. divSearch.setAttribute('style', `
  88. width:100%;
  89. height:40px;
  90. background:#FFCA62;
  91. display:flex;
  92. align-items:center;
  93. justify-content:flex-start;
  94. `)
  95. divSearch.innerHTML = `
  96. <img src="https://i.imgur.com/aXfbfwd.png" style="width:15px;height:15px;margin-right:5px;margin-left:20px;user-select:none;"></img>
  97. <input class="input" type="text" placeholder="e.g., R5 3600" spellcheck="false" style="width:250px;height:25px;padding-left:5px;flex-shrink:0;user-select:none;">
  98. <button class="btn" style="
  99. height:25px;
  100. color:black;
  101. user-select:none;
  102. border-width:2px;
  103. border-style:outset;
  104. border-color:buttonface;
  105. border-image:initial;
  106. border-radius:0;
  107. background-color:buttonface;
  108. cursor:pointer;
  109. ">GO</button>
  110. <ul class="records" style="display:flex;list-style:none;padding-left:20px;overflow:hidden;align-items:center;"></ul>`
  111. fDiv_table.appendChild(divSearch)
  112. this.input = document.querySelector(".input")
  113. this.input.focus()
  114. const btn = document.querySelector(".btn")
  115. const records = document.querySelector(".records")
  116. btn.addEventListener('click', () => {
  117. this.keyword = this.input.value
  118. this.search()
  119. })
  120. this.input.addEventListener('keydown', (e) => {
  121. if (e.keyCode === 13 || e.KeyCode === 13) {
  122. this.keyword = this.input.value
  123. this.search()
  124. }
  125. })
  126. records.addEventListener('click', this.clickRecords.bind(this))
  127.  
  128. const autoAddResults_div = document.querySelector('.autoAddResults_div')
  129. autoAddResults_div.innerHTML = this.isAutoAddResults ? '&check;' : ''
  130.  
  131. const autoAddResults = document.querySelector('.autoAddResults')
  132. autoAddResults.addEventListener('click', this.clickAutoAddResults_div.bind(this))
  133.  
  134. this.updateRecords()
  135. }
  136.  
  137. search() {
  138. // 重置
  139. this.selecteds.forEach(option => {
  140. this.clearSelected(option)
  141. })
  142. this.selecteds = []
  143. this.details = ''
  144. this.searchResultOptions = []
  145.  
  146. if (this.keyword.trim() === '') return
  147. this.keywords = []
  148. this.keyword.toLowerCase().split(' ').forEach(keyword => {
  149. if (keyword.trim() !== '') {
  150. this.keywords.push(keyword.trim())
  151. }
  152. })
  153. const totalSelects = document.querySelectorAll('td:nth-child(3) > select')
  154. const ulDetails = document.querySelector('.details ul')
  155. // 重置optionNumber
  156. let optionNumber = 0
  157. totalSelects.forEach(select => {
  158. const totalOptions = select.querySelectorAll('option')
  159. for (let i = 0; i < totalOptions.length; i++) {
  160. if(this.isMatch(totalOptions[i])){
  161. if (this.isAutoAddResults) {
  162. this.addSelected(totalOptions[i])
  163. this.selecteds.push(totalOptions[i])
  164. }
  165. this.details += `<li style="padding-left:20px;background:#fffd92;margin-top:20px;border:none;">${select.parentNode.previousSibling.textContent}</li>`
  166. break
  167. }
  168. }
  169. for (let i = 0; i < totalOptions.length; i++) {
  170. if(this.isMatch(totalOptions[i])){
  171. this.details += `<li style="padding-left:20px;cursor:pointer;border:none;" data-optionnumber="${optionNumber}">${this.addKeywordsStyle(totalOptions[i].text)}</li>`
  172. this.searchResultOptions.push(totalOptions[i])
  173. optionNumber++
  174. }
  175. }
  176. })
  177.  
  178. ulDetails.innerHTML = this.details
  179. this.updateDetailStyle()
  180. this.checkPrice()
  181. this.addRecords()
  182. this.updateRecords()
  183. }
  184.  
  185. isMatch(option) {
  186. return this.keywords.every(keyword => option.text.toLowerCase().includes(keyword.toLowerCase()))
  187. }
  188. addKeywordsStyle(text) {
  189. let reg = ''
  190. this.keywords.forEach((keyword, i) => {
  191. i === 0 ? reg = reg + keyword : reg = reg + `|${keyword}`
  192. })
  193. text = text.replace(new RegExp(reg, 'gi'), str => {
  194. return `<span style="color:red;">${str}</span>`
  195. })
  196. return text
  197. }
  198.  
  199. addSelected(option) {
  200. option.selected = true
  201. option.parentNode.parentNode.dispatchEvent(new Event('change'))
  202. }
  203.  
  204. clearSelected(option) {
  205. option.selected = false
  206. option.parentNode.parentNode.dispatchEvent(new Event('change'))
  207. }
  208.  
  209. updateRecords() {
  210. let str = `<span style="flex-shrink:0;">歷史紀錄:</span>`
  211. for (let i = 0; i < this.records.length; i++) {
  212. str += `<li class="record" style="margin-right:5px;cursor:pointer;color:blue;text-decoration:underline;flex-shrink:0;border:none;background:transparent;">${this.records[i]}</li><span class="deleteRecord" style="margin-right:20px;flex-shrink:0;font-size:15pt;cursor:pointer;" data-recordnumber="${i}">&times;</span>`
  213. }
  214. const records = document.querySelector('.records')
  215. records.innerHTML = str
  216. this.input.value = this.keyword
  217. }
  218.  
  219. addRecords() {
  220. const isNotRepeat = this.records.every( record => record.toLowerCase() !== this.keyword.toLowerCase() )
  221. if (!isNotRepeat) return
  222. this.records.unshift(this.keyword)
  223. if (this.records.length > 10) {
  224. this.records.splice(10)
  225. }
  226. localStorage.setItem('coolpc', JSON.stringify(this.records))
  227. }
  228.  
  229. clickRecords(e) {
  230. if (e.target.nodeName == 'LI' && e.target.className == 'record') {
  231. this.keyword = e.target.textContent
  232. this.search()
  233. }
  234.  
  235. if (e.target.nodeName === 'SPAN' && e.target.className === 'deleteRecord') {
  236. this.records.splice(e.target.dataset.recordnumber,1)
  237. localStorage.setItem('coolpc', JSON.stringify(this.records))
  238. this.updateRecords()
  239. }
  240. }
  241.  
  242. clearAd() {
  243. const body = document.querySelector('body')
  244. if (body !== null) {
  245. body.style.overflow = 'auto'
  246. }
  247. const Psu = document.querySelector('#Psu')
  248. if (Psu !== null) {
  249. Psu.style.display = 'none'
  250. }
  251. const doc = document.querySelector('#doc tbody')
  252. if (doc !== null) {
  253. doc.style.display = 'none'
  254. }
  255. }
  256.  
  257. removeOriginalEventListener() {
  258. const script = document.createElement('script')
  259. script.innerHTML = `
  260. document.querySelector('body').onkeyup = null;
  261. document.querySelector('body').onselectstart = null;
  262. document.querySelector('body').oncontextmenu = null;
  263. document.querySelector('#fDiv').ondblclick = null;
  264. `
  265. document.querySelector('body').append(script)
  266. }
  267.  
  268. clickDetails(e) {
  269. if (e.target.nodeName === 'LI' && e.target.dataset.optionnumber) {
  270. if(!this.searchResultOptions[e.target.dataset.optionnumber].selected){
  271. this.addSelected(this.searchResultOptions[e.target.dataset.optionnumber])
  272. } else {
  273. this.clearSelected(this.searchResultOptions[e.target.dataset.optionnumber])
  274. }
  275. this.updateDetailStyle()
  276. }
  277. }
  278.  
  279. updateDetailStyle() {
  280. const lists = document.querySelectorAll('.details li')
  281. lists.forEach(li => {
  282. if(li.dataset.optionnumber){
  283. if(this.searchResultOptions[li.dataset.optionnumber].selected === true) {
  284. li.style.background = '#c99932'
  285. } else {
  286. li.style.background = 'transparent'
  287. }
  288. }
  289. })
  290. }
  291.  
  292. clickAutoAddResults_div() {
  293. this.isAutoAddResults = !this.isAutoAddResults
  294. localStorage.setItem('coolpc_isAutoAddResults', this.isAutoAddResults)
  295. const autoAddResults_div = document.querySelector('.autoAddResults_div')
  296. autoAddResults_div.innerHTML = this.isAutoAddResults ? '&check;' : ''
  297. }
  298. checkPrice() {
  299. const details = document.querySelectorAll(".details ul li")
  300. if(this.minPrice.value.trim() === '' || this.maxPrice.value.trim() === '' || isNaN(Number(this.minPrice.value)) || isNaN(Number(this.maxPrice.value))) {
  301. details.forEach(li => {
  302. li.style.display = 'list-item';
  303. })
  304. return
  305. }
  306. details.forEach(li => {
  307. const match = li.textContent.match(/\$[0-9]+/g)
  308. if(match){
  309. const price = match[match.length-1].slice(1)
  310. if(Number(this.minPrice.value) <= Number(price) && Number(price) <= Number(this.maxPrice.value)) {
  311. li.style.display = 'list-item';
  312. } else {
  313. li.style.display = 'none';
  314. }
  315. }
  316. })
  317. }
  318. }
  319.  
  320. const coolpc = new Coolpc()
  321.  
  322. window.onload = coolpc.init()
  323.  
  324. })();