hhFiller

Fill response post for vacation in hh.ru by template

  1. // ==UserScript==
  2. // @id hhFiller
  3. // @name hhFiller
  4. // @name:ru hhFiller
  5. // @version 12.2020.8.4
  6. // @namespace spmbt.github.io/spmbt
  7. // @author spmbt
  8. // @description Fill response post for vacation in hh.ru by template
  9. // @description:ru Заполнить отклик на вакансию на hh.ru|moikrug|itmozg|superjob с помощью шаблона
  10. // @include http://hh.ru/*
  11. // @include https://hh.ru/*
  12. // @include https://moikrug.ru/*
  13. // @include https://itmozg.ru/*
  14. // @include https://www.superjob.ru/*
  15. // @run-at document-end
  16. // @update 12 hh.ru
  17. // @update 8 https-hh.ru;
  18. // @update 7 itmozg.ru,superjob.ru added;
  19. // @grant none
  20. // ==/UserScript==
  21. (function(win, u, noConsole, letterTmpl, addTmpl){
  22. if(win != top) return; //не выполнять в фрейме
  23.  
  24. var site = ({'hh.ru':'hh', 'career.ru':'hh', 'moikrug.ru':'moikrug', 'itmozg.ru':'itmozg', 'www.superjob.ru':'superjob'})[location.host]; //сайт, определяющий способ и правила публикации
  25.  
  26. var $e = function(g){ //===создать или использовать имеющийся элемент DOM===
  27. //g={el,blck,elA,cl,ht,cs,at,on,apT,prT,bef,aft}
  28. if(typeof g.el =='function') g.el = g.el.apply(g, g.elA);
  29. if(!g.el && g.el !==undefined && g.el !='') return g.el; //null|0|false
  30. var o = g.el = g.el ||'DIV';
  31. o = g.el = typeof o =='string'? /\W/.test(o) ? $q(o, g.blck) : win.document.createElement(o) : o;
  32. if(o){ //выполнять, если существует el
  33. if(g.cl)
  34. o.className = g.cl;
  35. if(g.cs)
  36. $x(o.style, g.cs);
  37. if(g.ht || g.at){
  38. var at = g.at ||{}; if(g.ht) at.innerHTML = g.ht;}
  39. if(at)
  40. for(var i in at){
  41. if(i=='innerHTML') o[i] = at[i];
  42. else o.setAttribute(i, at[i]);}
  43. if(g.on)
  44. for(var i in g.on) if(g.on[i])
  45. o.addEventListener(i, g.on[i],!1);
  46. g.apT && g.apT.appendChild(o);
  47. g.prT && (g.prT.firstChild
  48. ? g.prT.insertBefore(o, g.prT.firstChild)
  49. : g.prT.appendChild(o) );
  50. g.bef && g.bef.parentNode.insertBefore(o, g.bef);
  51. g.aft && (g.aft.nextSibling
  52. ? g.aft.parentNode.insertBefore(o, g.aft.nextSibling)
  53. : g.aft.parentNode.appendChild(o) );
  54. }
  55. return o;
  56. }
  57. ,$q = function(q, f){ return (f||win.document).querySelector(q);}
  58. ,$x = function(el, h){if(h) for(var i in h) el[i] = h[i]; return el;} //===extend===
  59. ,$qA = function(q, f){ return (f||win.document).querySelectorAll(q);}
  60. ,wcl = function(a){ a = a!==undefined ? a :''; //вывод в консоль (для тестов), с отключением по noConsole ==1
  61. if(win.console && !noConsole)
  62. win.console.log.apply(win.console, this instanceof String
  63. ? ["'=="+ this +"'"].concat([].slice.call(arguments))
  64. : arguments);
  65. }
  66. /** периодическая проверка условия с увеличением интервала
  67. * @constructor
  68. * @param{Number} t start period of check
  69. * @param{Number} i number of checks
  70. * @param{Number} m multiplier of period increment
  71. * @param{Function} check event condition
  72. * @param{Function} occur event handler
  73. */
  74. ,Tout = function(h){
  75. var th = this;
  76. (function(){
  77. if((h.dat = h.check() )) //wait of positive result, then occcurense
  78. h.occur && h.occur();
  79. else if(h.i-- >0) //next slower step
  80. th.ww = win.setTimeout(arguments.callee, (h.t *= h.m) );
  81. })();
  82. }
  83. ,resuH =180 //высота списка резюме (hh)
  84. ,hhLenLast =0 //число всех копий поля ввода (специфично для hh)
  85. ,selMod ='', selModPrev = selMod
  86. ,selCopy = function(){ //выделенный текст
  87. var sel ='';
  88. if(sel = win.getSelection() +''){
  89. sel = (sel +'\r\n').replace(/[;.,]\s+(\r?\n)/g,'\r\n').replace(/\r?\n\r?\n/g,'\r\n')
  90. .replace(/([^\r\n])(\r?\n)/g,'$1 ==да;$2')
  91. .replace(/(\r?[\n:])\s+==да;/g,'$1');
  92. };
  93. return sel;
  94. }, selC, selS;
  95. String.prototype.wcl = wcl; //(для вывода в консоль)
  96.  
  97. // по нажатию кнопки ответа на вакансию - скопировать выделенный текст (с обработкой):
  98. $e({el:'.HH-VacancyResponsePopup-MainButton', on:{mousedown: selC = function(ev){
  99. selMod = selCopy(); //'selMod'.wcl(selMod)
  100. }}});
  101. $e({el:'.HH-VacancyResponsePopup-Link', on:{mousedown: selC}});
  102. $e({el:'.HH-VacancyResponsePopup-TopicButton', on:{mousedown: selC}});
  103. $pismo = $q('.HH-Negotiations-Conversation-NewMessage');
  104. var fillTarea = function(){
  105. if(!selMod){
  106. selMod = localStorage.lastSel;
  107. selMod && (localStorage.lastSel ='');
  108. }
  109. return (localStorage.tmpl || letterTmpl).replace(/\n?$/,'\n') + (selMod && (addTmpl + selMod)||'');
  110. };
  111. if($pismo)
  112. $pismo.innerHTML = fillTarea();
  113. if(site =='moikrug') //предлагать подмену ответов при каждом выделении текста
  114. $e({el:'.job_show_description', on:{mouseup: function(ev){
  115. var ta = $q('#vacancy_response_body');
  116. if((selMod = selCopy()) && ta && win.confirm(
  117. 'Обновить область ответов на требования новым выделенным текстом?\n(Все изменения в старом тексте пропадут.)')){
  118. if(!RegExp(addTmpl).test(ta.value))
  119. ta.value = ta.value + addTmpl;
  120. ta.value = ta.value.replace(RegExp('('+addTmpl.replace(/\r/g,'\\\r?').replace(/\n/g,'\\\n') +')([\\s\\S]*)'),'$1') + selMod;
  121. win.setTimeout(function(){ta.style.height = ta.scrollHeight +'px';},0);
  122. ta.style.maxHeight ='none';
  123. }
  124. }}});
  125. if(site =='superjob') //предлагать подмену ответов при каждом выделении текста
  126. $e({el:'.VacancyView_details', on:{mouseup: function(ev){
  127. var ta = $q('.VacancyView_body .VacancySendResumeButton_popup .VacancySendResumeButton_message_textarea');
  128. if((selMod = selCopy()) && ta && win.confirm(
  129. 'Обновить область ответов на требования новым выделенным текстом?\n(Все изменения в старом тексте пропадут.)')){
  130. if(!RegExp(addTmpl).test(ta.value))
  131. ta.value = ta.value + addTmpl;
  132. ta.value = ta.value.replace(RegExp('('+addTmpl.replace(/\r/g,'\\\r?').replace(/\n/g,'\\\n') +')([\\s\\S]*)'),'$1') + selMod;
  133. win.setTimeout(function(){ta.style.height = ta.scrollHeight +'px';},0);
  134. ta.style.maxHeight ='none';
  135. }
  136. }}});
  137. $vacaBrand = $q('.vacancy-branded-user-content');
  138. if(/^hh$|itmozg/.test(site)){ //сохранять выделенное
  139. $e({el: /itmozg/.test(site)?'.margin-bottom-50':'.vacancy-section '+(!$vacaBrand?'.g-user-content':'.vacancy-branded-user-content'),
  140. on:{mouseup: selS = function(ev){
  141. if(selMod = selCopy()) //сохранить непустое выделение на случай перехода через "тестовую страницу"
  142. localStorage.lastSel = selMod; // ...или просто на новую страницу для itmzog
  143. }}});
  144. $e({el:'.respond.button.mt-30', on:{mouseup: selS}});
  145. }
  146. if(!localStorage.tmpl && /^Ув\. соискатель/.test(letterTmpl)){ //начальное заполнение шаблона
  147. //диалог сохранения в localStorage шаблона письма
  148. //wcl('taTmplBack')
  149. $e({el: $q('.taTmplBack')||0 //-чтобы создать не более 1 раза
  150. ,cl:'taTmplBack'
  151. ,cs:{position:'fixed', zIndex: 99991, width:'100%', height:'100%', top: 0, background:'rgba(48,48,48,0.4)'}
  152. ,apT: win.document.body
  153. });
  154. $e({el: $q('.taTmplOver')||0
  155. ,cl:'taTmplOver'
  156. ,cs:{position:'fixed', zIndex: 99992, width:'98%', height:'80%', margin:'0 1%', top:'12px'}
  157. ,ht:'<div style="width:100%; height:100%; text-align: left; background:rgba(255,255,255,0.5)">'
  158. +'<div style="display: inline-block; padding: 0 20px; border-bottom: 2px dotted #000; font-size:16px; background:rgba(255,255,255,0.5); color:#333">'
  159. +'Сообщение от скрипта <a href="https://greasyfork.org/en/scripts/10338-hhfiller" target=_blank><b>hhfiller.user.js</b></a></div>'
  160. +'<div style="padding: 4px 0; text-align: center; font-size:24px; background:rgba(255,255,255,0.5); color:#777">'+ letterTmpl +'</div>'
  161. +'<textarea class="taTmpl" style="width:80%; height:80%; margin: 10px 10%;"></textarea><br>'
  162. +'<div style="text-align: center">'
  163. +'<button onclick="var d = document, taS = d.querySelectorAll(\'.taTmpl\'), ta = taS[taS.length -1];'
  164. +'ta && (localStorage.tmpl = ta.value); d.querySelector(\'.taTmplBack\').style.display '
  165. +'= d.querySelector(\'.taTmplOver\').style.display =\'none\';" style="font-size: 24px">Сохранить</button></div>'
  166. +'<div style="width:79%; margin: 10px 10%; padding: 10px; font-size:16px; background:rgba(255,255,255,0.5); color:#333">Этот текст будет появляться в поле ответа'
  167. + (({hh:' по кнопке "Откликнуться на вакансию"',moikrug:''})[site]||'') +'.<br>'
  168. +'<a href="http://habrahabr.ru/post/259881/" target=_blank>Подробности</a> <i>(статья о скрипте)</i>.<br>'
  169. +'Чтобы записать другой шаблон, сотрите прежний командой "localStorage.tmpl=\'\'" в консоли.<br>'
  170. +'Чтобы <i>отказаться</i> от использования шаблона, отключите скрипт hhFiller в настройках браузера.</div>'
  171. +'</div>'
  172. ,apT: win.document.body
  173. });
  174. }
  175.  
  176. var taChanged =0;
  177. new Tout({t:620, i:2e6, m: 1 //периодическая проверка наличия поля ввода на странице
  178. ,check: ({
  179. hh: function(){
  180. var tAreas = $qA('.vacancy-response-popup .bloko-textarea')
  181. ,tLast;
  182. if(tAreas.length && (!(tLast = tAreas[tAreas.length -1]).value
  183. || hhLenLast < tAreas.length && selModPrev != selMod) ){ //если пустое поле ввода - заполнить шаблоном
  184. if(!tLast.value)
  185. tLast.value = fillTarea();
  186. if(selModPrev != selMod){
  187. if(!RegExp(addTmpl).test(tLast.value))
  188. tLast.value = tLast.value + addTmpl ;
  189. tLast.value = tLast.value.replace(RegExp('('+addTmpl.replace(/\r/g,'\\\r?').replace(/\n/g,'\\\n') +')([\\s\\S]*)'),selMod ?'$1':'') + selMod;
  190. }
  191. hhLenLast = tAreas.length;
  192. selModPrev = selMod;
  193. }
  194. $e({el: tLast, cs:{height: Math.max(400, win.innerHeight - 220 //растянуть поле ввода для удобства
  195. - (resuH = ($q('.vacancy-response-popup__resumes')||{}).offsetHeight||resuH)) +'px'}});
  196. var elTa = $qA('.bloko-toggle__expandable');
  197. $e({el: elTa.length && elTa[elTa.length -1], cs:{display:'block'}}); //показать свёрнутое поле ввода
  198. $e({el: '.popup.g-anim-fade.g-anim-fade_in', cs:{top:0}});
  199. return 0;
  200. }
  201. ,moikrug: function(){
  202. var ta = $q('#vacancy_response_body');
  203. if(ta && !ta.value){
  204. ta.value = fillTarea();
  205. win.setTimeout(function(){ta.style.height = ta.scrollHeight +'px';},0); //подправить высоту поля
  206. ta.style.maxHeight ='none';
  207. }
  208. return 0;
  209. }
  210. ,superjob: function(){
  211. var ta = $q('.VacancyView_body .VacancySendResumeButton_popup .VacancySendResumeButton_message_textarea')
  212. ,txt = ($q('.VacancySendResumeContacts_txt span')||{}).innerHTML||'';
  213. if(ta && !taChanged){
  214. ta.value = fillTarea().replace(/(Здравствуйте)(!)/,'$1, '+ txt +'$2');
  215. win.setTimeout(function(){ta.style.height = ta.scrollHeight +'px'; taChanged = 1;},900); //подправить высоту поля
  216. ta.style.maxHeight ='none';
  217. }
  218. return 0;
  219. }
  220. ,itmozg: function(){
  221. var ta = $q('#applyForm #text');
  222. if(ta && !ta.value){
  223. ta.value = fillTarea();
  224. win.setTimeout(function(){ta.style.height = ta.scrollHeight +'px';},0); //подправить высоту поля
  225. ta.style.maxHeight ='none';
  226. }
  227. ($qA('#resume option')||[{},{}])[1].selected =1; //выбрать первое резюме по списку
  228. return 0;
  229. }
  230. })[site]});
  231. (function(css){ //addRules
  232. if(typeof GM_addStyle !=u) GM_addStyle(css); //Fx,Chr (old)
  233. else if(typeof addStyle !=u) addStyle(css);
  234. else //Op and all
  235. $e({el:'style', apT: $q('head') }).appendChild(document.createTextNode(css)); //не проходит в Опере через $e
  236. })
  237. ('.search-result-item__label:not(.g-hidden) +.search-result-description{background-color:#eee}'
  238. +'.search-result-item__label:not(.g-hidden) +.search-result-description .search-result-description__item_primary{margin-bottom:-6px; padding-bottom: 6px;}'
  239. +'div[class*="banner-place"], div[id*="mt_ot"], .b-mainbanner, .related-vacancies-preview{display:none}'
  240. +'.VacancyView_body .VacancySendResumeButton_popup{display:block!important}'
  241. +'.VacancyView_body .VacancySendResumeButton_message, .VacancyView_body .VacancySendResumeButton_message_textarea_bg{height:auto;}'
  242. +'.vacancy-section .bloko-column:nth-child(2){height: 60vh;overflow-y: auto;border-bottom: 1px solid #ccc;box-shadow: 0 4px 6px 4px #ccc;}'
  243. +'@media(min-width:1020px){.main-content .bloko-columns-wrapper .row-content{padding-bottom: 0}}.vacancy-noprint .vacancy-section{margin: 10px 0 0}'
  244. +'.HH-Negotiations-Conversation-NewMessage{width: 900px; height: 540px}');
  245.  
  246. })(top,'undefined',''
  247. //Вместо этой строки можно вставить свой шаблон письма.
  248. //Его же можно сохранить в localStorage в диалоге по запросу скрипта.
  249. //(Используйте "\n" для перевода текста на новую строку.)
  250. ,'Ув. соискатель! Пожалуйста, заполните это поле шаблоном ответа на вакансию, с учётом ваших индивидуальных качеств.',
  251. '===================================\r\n\
  252. Ответы по вакансии:\r\n');