Wîkîferheng kuwiktionary https://ku.wiktionary.org/wiki/Destp%C3%AAk MediaWiki 1.48.0-wmf.4 case-sensitive Medya Taybet Gotûbêj Bikarhêner Gotûbêja bikarhêner Wîkîferheng Gotûbêja Wîkîferhengê Wêne Gotûbêja wêneyî MediaWiki Gotûbêja MediaWiki Şablon Gotûbêja şablonê Alîkarî Gotûbêja alîkariyê Kategorî Gotûbêja kategoriyê Pêvek Gotûbêja pêvekê Nimînok Gotûbêja nimînokê Portal Gotûbêja portalê Tewandin Gotûbêja tewandinê Jinûvesazî Gotûbêja jinûvesaziyê TimedText TimedText talk Modul Gotûbêja modulê Event Event talk tine 0 8691 6903093 6729036 2026-11-10T15:21:38Z Keyomerd 8814 /* Ji wêjeya klasîk */ 6903093 wikitext text/x-wiki {{h-bnr|Tine|tinê|tîne|ține}} {{ds|tine|tune|çine}} == {{ziman|ku}} == === Bilêvkirin === * {{ku-kîte}} === Formeke lêkerê === {{lêker|ku|form=erê}} # ''[[dema niha|Dema niha]] ya [[yekjimar]] bo kesê sêyem ji lêkera'' '''[[nebûn]]'''. #: ''Ew '''tine'''.'' # ne [[xwedî bûn]] #: '' Min ferheng '''tine'''.'' # ne [[xwestin]] #* {{J:Robert Louis Stevenson:Meseleya Seyr a Dr. Jekyll û Mr. Hyde|r=28|jêgirtin=Dilê min '''tine''' tiştekî bêtir bibihîsim.}} === Rengdêr === {{ku-tewîn-rd|tine}} {{rengdêr|ku}} # [[neyî]], [[nîn]], [[nî]], ne [[heyî]] # ne ti tişt ==== Ji wêjeya klasîk ==== * {{J:Melayê Cizîrî:Dîwana Melayê Cizîrî|jêgirtin=Lew xef xedengan nuhtine<br />Ji qewsê hilalê jih '''tine'''<br />Heyranê dêma mehweş în}} ==== Formên alternatîv ==== * [[çine]] * [[çune]] * [[tune]] * [[nîne]] ==== Dijmane ==== * [[heye]] ==== Jê ==== {{kol3|ku |tine bûn |tinebûn |tine kirin |tinekirin |tinetî |tineyî }} ==== Etîmolojî ==== [[ti]] + [[ne]], bi hin devokên kurmancî herwiha [[tune]], [[çine]], [[çune]], zazakî [[çinî]]. Herwiha bide ber ermenî ''çe'' ("ne"). ==== Rastnivîsîn ==== Binere rûpela [[ti]] ==== Werger ==== {{werger-ser}} * {{Z|de}}: {{W+|de|abwesend}}, {{W+|de|nicht}} * {{Z|fa}}: {{W+|fa|نیست}}, {{W+|fa|نیستی|guman=erê}} * {{Z|he}}: {{W+|he|אין}} * {{Z|en}}: {{W+|en|zero}}, {{W+|en|naturopathy}}, {{W-|en|there is not}}, {{W-|en|ther are not}}, {{W-|en|there aren't}}, {{W-|en|there isn't}}, {{W-|en|has not}}, {{W+|en|have not}} * {{Z|tr}}: {{W+|tr|yok}}, {{W+|tr|sıfır}}, {{W+|tr|mafiş}}, {{W+|tr|silik}} * {{Z|yi}}: {{W-|yi|נישטאָ}} {{werger-bin}} ==== Anagram ==== * {{anagram|ku|a=eint|etin|neti}} == {{ziman|en}} == === Navdêr === {{navdêr|en}} # [[sertîj]], [[stirî]], [[çetel]] <ref>Ferhenga Kurdî-Îngîlîzî ya Salah Sadallah</ref> ==== Anagram ==== * {{anagram|en|a=eint|Tien}} === Çavkanî === {{çavkanî}} mgotvhfyp06bfmfh8k81urtylhrj7vm xeyirîn 0 45630 6903089 6805631 2026-11-10T12:29:40Z Keyomerd 8814 /* Lêker */ 6903089 wikitext text/x-wiki == {{ziman|ku}} == === Bilêvkirin === * {{ku-kîte}} === Lêker === {{lêker|ku|c=ngh}} # [[tav]] ([[roj]]) [[tarî]] bûn ji ber ku [[heyv]] dikeve ber # [[guherîn]], [[hatin guhartin]] #: ''Çi gereke '''bixeyire''' ku hun cardin vegerin Kurdistanê? Eger '''xeyirî''', çêbû em dixwazin vegerin Kurdistanê.'' # [[çilmisîn]] #* {{jêder|ku|jêgirtin=Îro qewî têtin girî</br>Sosin di mêrgan '''xeyirî'''</br>Sed heyf li lêvê şekirî</br>Êdî venaxwut şerbetê|{{Jêgirtin/Feqiyê Teyran/Dîwan}}}} ==== Ji wêjeya klasîk ==== * {{J:Feqiyê Teyran|jêgirtin=Muksî yê jar û bedtir im</br>Ferhemnî '''xeyre''' rabimîn}} ==== Tewandin ==== ''Lêker'': :-xeyir- ==== Bi alfabeyên din ==== * {{ku-ar|خه‌یرین}} ==== Formên alternatîv ==== * [[xeyrîn]] ==== Jê ==== * [[xeyirandî]] * [[xeyirîner]] * [[roj xeyirîn]] ==== Têkildar ==== * [[xeyirandin]] ==== Etîmolojî ==== {{etîmon|ku|:qertaf|xeyr|-în}} ==== Werger ==== {{werger-ser}} * {{Z|en}}: {{W+|en|eclipse}} * {{Z|tr}}: {{W+|tr|solmak|guman=erê}} {{werger-bin}} === Navdêr === {{navdêr|ku|z=m}} # {{mane?|ku}} ==== Tewandin ==== {{ku-tewîn-nav|mê}} ==== Werger ==== {{werger-ser}} {{werger-bin}} jxwt9f3gohgse598w16vvo4m0by48sk çilmisîn 0 50347 6903090 6472414 2026-11-10T12:29:58Z Keyomerd 8814 /* Lêker */ 6903090 wikitext text/x-wiki == {{ziman|ku}} == === Bilêvkirin === * {{ku-kîte}} === Lêker === {{ku-tewîn-reh|çilmisîn|form=ngh|niha=çilmis|borî=çilmisî}} [[Wêne:Withered Hibiscus Blossom.jpg|thumb|Guleke çilmisî.]] {{lêker|ku|c=x-ngh}} # [[xeyirîn]], [[qurmiçîn]], [[qirmiçîn]], [[qermiçîn]], <br> (gul û giya yan [[leş]]) ji ber [[piçek]] [[hişk]]bûnê [[xeml]] û [[geşî|geşiya]] xwe [[ji dest dan]] #: ''Gul '''çilmisîn''', <br>Rûyê mirovên pîr qurmiçî ye.'' #* {{jêder|ku|jêgirtin=Ey felek bo te dinalim <br>[[bo çi|Bo çi]] nêrgiz '''çilmisîn'''<br>Ew çima bextê me [[ho]] ye<br>Em bê dost û bêkes in.|Helbest: Bedirxan Sindî, stran: Mihemed Şêxo}} ==== Bi alfabeyên din ==== * {{ku-ar|چلمسین}} ==== Formên alternatîv ==== * [[çelmisîn]] * [[çirmisîn]] ==== Dijmane ==== * [[bişkivîn]] ==== Jê ==== * [[çilmisiyayî]] * [[çilmisî]] * [[çilmisok]] ==== Têkildar ==== * [[çilmisandin]] ==== Etîmolojî ==== Hevreha soranî [[çiro]] bûn, kirmaşanî [[çilüsyan]], ermenî [[թարշամել]] (tarşamêl) / [[թառամել]] / (taramêl) herdu bi maneya "çilmisîn", avestayî ''terşne-'' (tî, têhn, hewceyî avê), sanskrîtî ''trşyeti-'' (tî, hewceyî avê), yûnaniya kevn [[τέρσομαι]] (têrmosoy: [[hişk]] / [[ziwa]] bûn), latînî [[terra]] ([[ax]], erdê ziwa) û [[torrere]] ([[sotin]], [[şewitîn]]), almanî [[dürr]] ([[ziwa]], [[hişk]]), albanî [[thartë]] (tirş) û [[ter]] ([[hişk]] / [[ziwa]] bûn), îngilîzî [[thirst]] ([[têhn|tên]], hewcedariya bi avê) û [[dry]] ([[hişk]], [[ziwa]])... hemû ji {{ji|ine-pro|ku|-}} {{jinûvesazî|ine-pro|ters-|w=[[hişk]], [[ziwa]]}} ku herwiha serekaniya [[tî]] / [[tên]] (hewceyî avê) û [[tirş]]) yên kurdî ye jî. Bo guherîna "t > ç" bide ber "[[pate]]/[[paçe]]" û "[[qurt]]/[[qurç]]". Guherîna "l/r" di kurdî de [[diyarde]]yeke [[berbelav]] e. Ji formeke [[kevnar]] a [[hevpar]] a wekî *terşemsîn, rş/rd/rj di kurdî de [[gelek caran]] dibe l (binêre [[dil]], [[hiştin|hêl-]], [[bilîn]] û frs: {{g|fa|ورزیدن}}(werzîden)). m'ên ku pêş dengekî [[dengdêr]] tên jî [[li gor]] zimanî dibin dibe v/w. anku di [[kirmaşanî]] de bûye *çelewsîn > *çilûsîn û bi [[dengnasî|dengnasiya]] [[kirmaşanî]] bûye "çilüsiyan/çilüsîn" ji ber ku w'ên [[kurmancî|badînî]] bi [[kurdiya başûrî|kurmanciyên palewanî]] dibin ü. Bi [[soranî]] *çilewsîn bûye lê s herçima ketiye û paşê jî wek [[lêker]] êdî nemaye û wek *çilew maye ku bûye "[[çiro]]". Di [[kurmancî|badînî]] de m cih [[guhartin|guhartiye]] [[lewma]] jî nebûye v, wek "[[çilmisîn]]" maye. [[-mis]] di vir de paşgireke transformatîf e ku maneya guherînê nîşan dide, anku ''çilmisîn'' = ''tirş ketin''. Bo zêdetir agehî binêre [[-ij]]. ==== Bi zaravayên din ==== * {{Z|ckb}}: {{W-|ckb|گەچڵان}}, {{W-|ckb|چرچ بوون}}, {{W-|ckb|ژاکان}}, {{W-|ckb|سیس هەڵگەڕان}}, {{W-|ckb|سیس هەڵاتن}}, {{W-|ckb|سیس بوون}} ==== Werger ==== {{werger-ser}} * {{Z|de}}: {{W+|de|welken}}, {{W+|de|verwelken}}, {{W+|de|dörren}}, {{W+|de|bleichen}}, {{W+|de|verblassen}}, {{W+|de|verblühen}}, {{W-|de|ausbleichen}}, {{W+|de|Blässe|f|guman=erê}}, {{W+|de|verhallen|guman=erê}} * {{Z|cs}}: {{W+|cs|vadnout}} * {{Z|zh}}: [[枯萎]](kūwěi) * {{Z|da}}: {{W-|da|visne}} * {{Z|ar}}: [[صعق]] (se'eqe), {{W+|ar|زبل}}, {{W-|ar|اصفر|guman=erê}} * {{Z|hy}}: [[թարշամել]] (tʿaršamel) / [[թառամել]] / (tʿaṙamel) * {{Z|fa}}: {{W+|fa|پژمردن|tr=pejmoden}}, {{W-|fa|پژمرده شدن}} * {{Z|fi}}: {{W-|fi|kuihdutua}}, {{W+|fi|nuutua}}, {{W+|fi|näivettyä}}, {{W+|fi|lakastua}} * {{Z|fr}}: {{W+|fr|flétrir}}, se [[faner]] * {{Z|nl}}: {{W+|nl|verschrompelen}} * {{Z|en}}: to {{W+|en|wither}}, to {{W+|en|shrivel}}, to {{W+|en|droop}}, to {{W+|en|dry up}}, to {{W+|en|fade}}, to {{W+|en|wilt}}, {{W+|en|shriveling}}, {{W+|en|withering}}, {{W+|en|wizen}}, {{W+|en|shrinkage|guman=erê}}, {{W+|en|discoloration|guman=erê}}, {{W+|en|shrinking|guman=erê}} * {{Z|it}}: {{W+|it|appassire}} * {{Z|ja}}: {{W+|ja|枯れる|tr=かれる, kareru}} * {{Z|ko}}: [[시들다]] (sideurda) * {{Z|pl}}: {{W-|pl|wysuszać}}, {{W+|pl|suszyć}} * {{Z|pt}}: {{W+|pt|murchar}} * {{Z|ro}}: {{W+|ro|veșteji}}, {{W+|ro|ofili}} * {{Z|ru}}: [[вянуть]] (vyanuti), [[завянуть]] (zavyanuti), [[увянуть]] (uvyanuti), [[увядать]] (uvyadáti), [[сохнуть]] (sóxnuti), [[засохнуть]] (zasóxnuti) * {{Z|es}}: {{W+|es|marchitarse}} * {{Z|sv}}: {{W+|sv|vissna}}, {{W-|sv|förtorka}} * {{Z|tr}}: {{W+|tr|solmak}}, {{W+|tr|pörsümek}}, {{W-|tr|renk atmak}}, {{W+|tr|uçmak}}, {{W+|tr|uçuklaşmak}} * {{Z|el}}: {{W+|el|μαραίνομαι}} {{werger-bin}} === Navdêr === {{navdêr|ku|z=m}} # {{mane?|ku}} ==== Tewandin ==== {{ku-tewîn-nav|mê}} ==== Werger ==== {{werger-ser}} * {{Z|tr}}: {{W+|tr|solma}}, {{W+|tr|soluş}}, {{W+|tr|pörsüme}}, {{W+|tr|uçuklaşma}} {{werger-bin}} 9vncah784vw5pikxalj8e8g2t128pkm MediaWiki:Gadgets-definition 8 118586 6903103 6902727 2026-11-10T21:28:01Z Wikihez 7344 Wizard modules: add the files form.js and steps/sections.js 6903103 wikitext text/x-wiki Binêre [[Taybet:Gadgets]] ji bo belgekirina amûrên li jêr. ==Rû== * fix-kuarab-title[default|namespaces=0,108|actions=view]|fix-kuarab-title.js * definition-links[default|namespaces=0,108|actions=view]|definition-links.js * LiveClock[dependencies=mediawiki.util]|LiveClock.js * charinsert-styles[hidden|type=styles]|charinsert-styles.css * LanguagesAndScripts[default]|LanguagesAndScripts.css * jeder[dependencies=ext.gadget.VisibilityToggles|default]|jeder.css|jeder.js * CodeLinks[default|dependencies=site,mediawiki.util]|CodeLinks.js * Tabset[default]|Tabset.js|Tabset.css * kuTewandin[default]|kuTewandin.js * wikt.orange-links[dependencies=mediawiki.api,mediawiki.util,mediawiki.Title]|wikt.orange-links.js|wikt.orange-links.css * wikt.showids[package|namespaces=0,2,4,108|actions=view|dependencies=mediawiki.util]|wikt.showids.js|wikt.showids.css * wikt.targeted-translations[default|namespaces=0,2,4,108|actions=view|dependencies=mediawiki.storage]|wikt.targeted-translations.js|wikt.targeted-translations.css * EtytreeZoom[default|dependencies=mediawiki.util]|EtytreeZoom.js * PagePreviews[default|dependencies=site,mediawiki.api,mediawiki.util,mediawiki.Title|skins=vector,vector-2022,monobook,timeless,modern,cologneblue]|PagePreviews.js * AutoRedirect[default|dependencies=user,mediawiki.util|actions=view|namespaces=0]|AutoRedirect.js * TreeDescs[default|namespaces=0,108|peers=TreeDescs-pagestyles]|TreeDescs.js * clearer-edit-summary[default|actions=view]|clearer-edit-summary.js * DeluxeHistory[dependencies=user,mediawiki.util]|DeluxeHistory.js|DeluxeHistory.css * JumpToError[dependencies=mediawiki.notification]|JumpToError.js * purgetab[dependencies=mediawiki.api,mediawiki.util]|purgetab.js ==AmûrênGuhartinê== * check-transpage[default|namespaces=0,108|actions=view]|check-transpage.js * preload-text[default|actions=edit|dependencies=mediawiki.util]|preload-text.js * wikEd|wikEd.js * charinsert[default|peers=charinsert-styles]|charinsert.js * translation_editor[dependencies=mediawiki.notification|type=general]|silent_fail_storage.js|editor.js|translation_editor.js|translation_editor.css * QQ[dependencies=mediawiki.util,oojs-ui-core,oojs-ui-widgets|type=general]|QQ.js|QQ.css * WiktAccFormCreation[dependencies=mediawiki.util]|WiktAccFormCreation.js * AjaxEdit[dependencies=mediawiki.util,mediawiki.user,mediawiki.api]|AjaxEdit.js * EditHere[dependencies=mediawiki.api,oojs-ui-core,oojs-ui-widgets]|EditHere.js * aWa[rights=autoconfirmed|dependencies=mediawiki.util,mediawiki.api,mediawiki.user,user.options,mediawiki.Title|type=general]|aWa.js|aWa.css * start[default|package|namespaces=0,2,4,108|dependencies=mediawiki.util,ext.gadget.wikt.core]|start.js * wikt.dsb[default|package|namespaces=0,2,4,108|actions=view|dependencies=mediawiki.util,ext.gadget.wikt.core]|wikt.dsb.js|wikt.dsb.css * linkify-toolbar[default|actions=edit,submit|dependencies=ext.gadget.linkify,jquery.textSelection,ext.wikiEditor]|linkify-toolbar.js ==Kategorî== * HotCat|HotCat.js * Cat-a-lot[rights=autoconfirmed]|Cat-a-lot.css|Cat-a-lot.js ==AmûrênVeşartî== * check-transpage-popup[hidden|package|dependencies=oojs-ui,mediawiki.user]|check-transpage-popup.js * StorageUtils[hidden|dependencies=mediawiki.cookie,mediawiki.storage]|StorageUtils.js * charinsert-core[hidden|dependencies=jquery.textSelection,user,mediawiki.storage]|charinsert-core.js * VisibilityToggles[hidden|dependencies=user,mediawiki.cookie,mediawiki.storage]|VisibilityToggles.js * defaultVisibilityToggles[hidden|default|dependencies=ext.gadget.VisibilityToggles]|defaultVisibilityToggles.js * Site[hidden|default]|Site.css * catfix[hidden|default|dependencies=mediawiki.Title,mediawiki.util|categories=Pages using catfix]|catfix.js * Palette[hidden|default]|Palette.css * test[rights=autoconfirmed]|test.js * citoid-helper[hidden|dependencies=mediawiki.util]|citoid-helper.js * wikt.core[hidden|package|dependencies=mediawiki.util,mediawiki.storage]|wikt.core.js|wikt.core/sections.js|wikt.core/prefs.js|wikt.core/data-sections.json|wikt.core.css * wikt.suggest[hidden|package|dependencies=mediawiki.util]|wikt.suggest.js|wikt.suggest.css * wikt.lang-metadata[hidden|package]|wikt.lang-metadata.js|wikt.lang-metadata.json * wikt.edit-data[hidden|package|dependencies=ext.gadget.wikt.lang-metadata]|wikt.edit-data.js|wikt.edit/data-main.json * wikt.edit[hidden|package|dependencies=mediawiki.util,mediawiki.api,ext.gadget.wikt.core,ext.gadget.wikt.suggest,ext.gadget.wikt.edit-data]|wikt.edit.js|wikt.edit/util.js|wikt.edit/wikitext.js|wikt.edit/langs.js|wikt.edit/labels.js|wikt.edit/posparams.js|wikt.edit/ziman-core.json|wikt.edit.css * wikt.cite[hidden|package|dependencies=mediawiki.api,oojs-ui-core,oojs-ui-widgets,oojs-ui-windows,ext.gadget.citoid-helper,ext.gadget.wikt.edit]|wikt.cite.js|wikt.cite/etimon.js|wikt.cite.css * wikt.dsb-panels[hidden|package|dependencies=mediawiki.util,mediawiki.api,ext.gadget.wikt.core,ext.gadget.wikt.edit]|wikt.dsb-panels.js|wikt.dsb-panels.css * wikt.dsb-sense[hidden|package|dependencies=ext.gadget.wikt.dsb-panels,ext.gadget.wikt.cite,ext.gadget.wikt.add-examples,oojs-ui-windows]|wikt.dsb-sense.js * wikt.dsb-subsection[hidden|package|dependencies=ext.gadget.wikt.dsb-panels,ext.gadget.wikt.cite,oojs-ui-windows]|wikt.dsb-subsection.js * wikt.dsb-pos[hidden|package|dependencies=ext.gadget.wikt.dsb-panels]|wikt.dsb-pos.js * wikt.start-wizard-styles[hidden|type=general]|wikt.start-wizard.css * wikt.start-wizard-steps[hidden|package]|wikt.start-wizard-steps.js|wikt.start-wizard-steps/sections.js * wikt.start-wizard[hidden|package|dependencies=mediawiki.util,mediawiki.api,ext.gadget.wikt.core,ext.gadget.wikt.edit,ext.gadget.wikt.cite,ext.gadget.StorageUtils,ext.gadget.citoid-helper,oojs-ui-core,oojs-ui-widgets,oojs-ui-windows,ext.gadget.wikt.start-wizard-steps,ext.gadget.wikt.start-wizard-styles]|wikt.start-wizard.js|wikt.start-wizard/form.js * wikt.add-examples[hidden|package|dependencies=oojs,oojs-ui,ext.gadget.citoid-helper,ext.gadget.StorageUtils,ext.gadget.wikt.core,ext.gadget.wikt.edit,ext.gadget.wikt.cite]|wikt.add-examples.js|wikt.add-examples.css * wikt.translation-adder[hidden|package|dependencies=mediawiki.util,mediawiki.api,mediawiki.storage,ext.gadget.StorageUtils,ext.gadget.wikt.suggest,ext.gadget.wikt.lang-metadata]|wikt.translation-adder.js|wikt.translation-adder/editor.js|wikt.translation-adder/language-utils.js|wikt.translation-adder/data.js|wikt.translation-adder.css * linkify[hidden|dependencies=mediawiki.api]|linkify.js * TreeDescs-pagestyles[hidden|type=styles]|TreeDescs-pagestyles.css s148e4dhizqbd7z2ar7s7yrmdysk36s xûsî 0 429954 6903091 6538068 2026-11-10T12:41:53Z Keyomerd 8814 /* Formên alternatîv */ 6903091 wikitext text/x-wiki {{h-bnr|xuşî}} {{ds|xwîs|xwîsar}} == {{ziman|ku}} == === Bilêvkirin === * {{ku-kîte}} === Navdêr === [[Wêne:FrostyLeaf.jpg|thumb|250px|xwîsar li ser [[pelg]]eke [[dar]]an]] [[Wêne:Fleur_de_givre_L.jpg|thumb|250px|[[krîstal]]ên [[xwîsar]]ê ji nêzîk ve]] {{navdêr|ku|z=m}} # [[xunav|Xunavên]] [[qerisî]], [[çip]]ên [[av]]ê yên ku ji ber [[sarma]]yê bûne [[qeşa]] anku [[sehol]]. ==== Tewandin ==== {{ku-tewîn-nav|mê}} ==== Formên alternatîv ==== {{kol3|ku|cure=Herwiha |xûsar |xûs |xûsk |xwîs |xwîsar |xwîsî |xwîsk |xewsar }} ==== Etîmolojî ==== {{bêetîmolojî|ku}} ==== Werger ==== {{werger-ser}} * {{Z|sq}}: {{W+|sq|ngricë|m}}, {{W+|sq|brymë|m}}, {{W+|sq|çaf|n}} * {{Z|de}}: {{W+|de|Reif|n}} * {{Z|alt}}: {{W-|alt|кыру}} * {{Z|ami}}: {{W-|ami|'o'ol}} * {{Z|rup}}: {{W-|rup|brumã|m}} * {{Z|as}}: {{W-|as|নিয়ৰ}} * {{Z|ay}}: {{W-|ay|juyphi}} * {{Z|ain}}: {{W-|ain|シンナㇺ|tr=sinnam}} * {{Z|az}}: {{W+|az|qırov}} * {{Z|eu}}: {{W-|eu|antzigar}}, {{W-|eu|intzigar}} * {{Z|ba}}: {{W-|ba|ҡырау}} * {{Z|be}}: {{W-|be|і́ней|n}}, {{W-|be|шэ́рань|m}} * {{Z|bg}}: {{W+|bg|слана|m}}, {{W+|bg|скреж|n}} * {{Z|cim}}: {{W-|cim|bròzama|m}}, {{W-|cim|bròst|n}} * {{Z|cs}}: {{W+|cs|jinovatka|m}}, {{W+|cs|mráz|n}} * {{Z|zh}}: *: {{Z|yue}}: {{W-|yue|霜|tr=soeng1}} *: {{Z|cmn}}: {{W+|cmn|霜|tr=shuāng}} * {{Z|cv}}: {{W-|cv|тӑм}} * {{Z|da}}: {{W+|da|frost|g}} * {{Z|ar}}: {{W-|ar|صَقِيع|n}} *: {{Z|acw}}: {{W-|acw|صقيع|tr=ṣagīʕ|n|sc=Arab}} *: {{Z|ary}}: {{W-|ary|جْريحة|m|tr=jriḥa}} * {{Z|hy}}: {{W+|hy|եղյամ}} * {{Z|eo}}: {{W+|eo|prujno}} * {{Z|et}}: {{W+|et|härmatis}} * {{Z|fa}}: {{W+|fa|هسر|tr=hasar}} * {{Z|fo}}: {{W-|fo|frost|nt}} * {{Z|fi}}: {{W+|fi|kuura}}, {{W+|fi|huurre}}, {{W+|fi|härmä}} * {{Z|fr}}: {{W+|fr|givre|n}} * {{Z|gd}}: {{W-|gd|reòthadh|n}} * {{Z|gl}}: {{W+|gl|rosada|m}}, {{W+|gl|xeada|m}}, {{W+|gl|lazo|n}}, {{W+|gl|carazo|n}}, {{W-|gl|pruina|m}}, {{W-|gl|estromba|m}} * {{Z|gn}}: {{W+|gn|eláda}} * {{Z|nl}}: {{W+|nl|rijp}}, {{W+|nl|rijm}} * {{Z|he}}: {{W+|he|כְּפוֹר|n|tr=k'for}} * {{Z|ang}}: {{W-|ang|hrīm|n}} * {{Z|en}}: {{W+|en|frost}}, {{W+|en|hoar frost}} * {{Z|ga}}: {{W-|ga|sioc|n}}, {{W-|ga|siocán|n}}, {{W-|ga|reo|n}}, {{W-|ga|cuisne|n}} * {{Z|it}}: {{W+|it|brina|m}} * {{Z|is}}: {{W-|is|hrím|nt}} * {{Z|ja}}: {{W+|ja|霜|tr=[[しも]], shimo|sc=Jpan}} * {{Z|krc}}: {{W-|krc|къырау}} * {{Z|kdr}}: {{W-|kdr|kyrav}} * {{Z|kaa}}: {{W-|kaa|qıraw}} * {{Z|ca}}: {{W+|ca|gelada|m}}, {{W+|ca|glaçada|m}}, {{W+|ca|gebre|n}} * {{Z|qu}}: {{W-|qu|qasa}} * {{Z|ky}}: {{W+|ky|кыроо}} * {{Z|ko}}: {{W+|ko|서리}} * {{Z|kum}}: {{W-|kum|къырав}} * {{Z|la}}: {{W-|la|pruīna|m}}, {{W-|la|gelicidium|nt}} * {{Z|lo}}: {{W-|lo|ນ້ຳກະດ້າງ}}, {{W-|lo|ໝອກຂຸ້ນ}} * {{Z|lb}}: {{W-|lb|Räif|n}} * {{Z|ms}}: {{W+|ms|ibun}} * {{Z|mnc}}: {{W-|mnc|ᡤᡝᠴᡝᠨ}} * {{Z|mi}}: {{W-|mi|hukapapa}}, {{W-|mi|hukapuri}}, {{W-|mi|hukatū}}, {{W-|mi|hauhunga}}, {{W-|mi|hukapapa}} * {{Z|hu}}: {{W+|hu|dér}}, {{W+|hu|zúzmara}} * {{Z|nap}}: {{W-|nap|ggelo}} * {{Z|nv}}: {{W-|nv|shoh}}, {{W-|nv|dahtooʼ tin}}, {{W-|nv|dah tin}} * {{Z|nog}}: {{W-|nog|кырав}} * {{Z|nrf}}: {{W-|nrf|gélée|m}} * {{Z|no}}: *: {{Z|nb}}: {{W+|nb|rim|nt}}, {{W+|nb|frost|n}} *: {{Z|nn}}: {{W-|nn|frost|n}} * {{Z|oc}}: {{W+|oc|gibre|n}}, {{W-|oc|glacina|m}} * {{Z|ota}}: {{W-|ota|قراغو|tr=kırağu, kırağı}} * {{Z|ug}}: {{W-|ug|qiro}} * {{Z|uz}}: {{W+|uz|qirov}} * {{Z|pdt}}: {{W-|pdt|Frost|n}} * {{Z|pox}}: {{W-|pox|morz|n}} * {{Z|pl}}: {{W+|pl|mróz|n}} * {{Z|pt}}: {{W+|pt|geada|m}} * Punjabi: *: Gurmuxî: {{W-|pa|ਕੱਕਰ|n}} *: Şahmuxî: {{W-|pa|کَکَّر|n}} * {{Z|kk}}: {{W-|kk|қырау|sc=Cyrl}} * {{Z|ro}}: {{W+|ro|brumă|m}} * {{Z|ru}}: {{W+|ru|и́ней|n}}, {{W+|ru|и́зморозь|m}} * {{Z|sa}}: {{W+|sa|हिम|tr=himá|n}} * {{Z|sh}}: *: Kirîlî: {{W-|sh|мра̏з|n}}, {{W-|sh|и̑ње|nt}}, {{W-|sh|сла́на|m}} *: Latînî: {{W+|sh|mrȁz|n}}, {{W+|sh|ȋnje|nt}}, {{W+|sh|slána|m}} * {{Z|sk}}: {{W-|sk|mráz|n}} * {{Z|sl}}: {{W-|sl|ivje|nt}}, {{W-|sl|slana|m}} * Sorbî: *: {{Z|dsb}}: {{W-|dsb|mroz|n}} * {{Z|es}}: {{W+|es|escarcha|m}} * {{Z|sv}}: {{W+|sv|frost|g}}, {{W+|sv|rimfrost|g}} * {{Z|cjs}}: {{W-|cjs|қыраа|tr=qyraa}} * {{Z|tl}}: {{W-|tl|eskartsa}}, {{W-|tl|yamog}}, {{W-|tl|andap}} * {{Z|rif}}: {{W-|rif|ajris|n}} * {{Z|th}}: {{W-|th|น้ำค้างแข็ง|tr=nám káang kɛ̌ɛng}} * {{Z|te}}: {{W+|te|హిమము}} * {{Z|crh}}: {{W-|crh|qırav}} * {{Z|tt}}: {{W+|tt|кырау}} * {{Z|tr}}: {{W+|tr|kırağı}} * {{Z|kim}}: {{W-|kim|һыраа|tr=hyraa}} * {{Z|tyv}}: {{W-|tyv|хыраа}} * {{Z|uk}}: {{W-|uk|і́ній|n}} * {{Z|vec}}: {{W+|vec|bròxa|m}}, {{W+|vec|bróxema|m}} * {{Z|vi}}: {{W+|vi|sương giá}} * {{Z|vo}}: {{W+|vo|flod}} * {{Z|wa}}: {{W+|wa|djalêye|m}} * {{Z|cy}}: {{W+|cy|rhew|n}}, {{W+|cy|barrug|n}} * {{Z|kjh}}: {{W-|kjh|хро}} * {{Z|yi}}: {{W-|yi|יאָדלעבוים|n}} * {{Z|el}}: {{W+|el|πάγος|n}}, {{W+|el|πάχνη|m}} *: Kevn: {{W-|grc|κρυμός|n}}, {{W+|el|πάγος|n}}, {{W-|grc|πάχνη|m}} * {{Z|nmn}}: {{W-|nmn|ǁqa̰ã}} {{werger-bin}} :{{werger çavkanî|en|frost|beş=Ev qismê Wergerê|oldid=82402009}} g1kgebeboyrsyvz4l4rsokn3gqw87ed peratin 0 957263 6903094 6496824 2026-11-10T15:25:35Z Keyomerd 8814 /* Lêker */ 6903094 wikitext text/x-wiki == {{ziman|ku}} == === Bilêvkirin === * {{ku-kîte}} === Lêker === {{ku-tewîn-lk|peratin|form=gerguhêz|niha=perêj|borî=perat}} {{lêker|ku|c=x-gh}} # [[sotin]], [[şewitandin]] #* {{jêder|ku|jêgirtin=Bilbilên wê gulşenê, ji ezel her dibêjin</br>Heta ku der neymînî, ji me'den da dirêjin</br>Herfek ji wesfan tenê, alem '''naperêjin'''|{{Jêgirtin/Feqiyê Teyran/Dîwan}}}} ==== Formên alternatîv ==== * [[perêhtin]] ==== Têkildar ==== * [[perijîn]] ==== Etîmolojî ==== Ji {{hv|ira-pro|*peri-}} + {{g|ira-pro|*tač}}, hevreha {{hv|ku|bihatin}} ko ji {{hv|ira-pro|*wi-}} + {{g|ira-pro|*tač}} e. ==== Werger ==== {{werger-ser}} {{werger-bin}} === Navdêr === {{navdêr|ku|z=m}} # {{mane?|ku}} ==== Werger ==== {{werger-ser}} {{werger-bin}} ==== Anagram ==== * {{anagram|ku|a=aeinprt|peritan}} 6vsj7ns8oyp2b7zm23pgfpvsy9kchhf 6903097 6903094 2026-11-10T15:44:04Z Wikihez 7344 /* Lêker */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re) 6903097 wikitext text/x-wiki == {{ziman|ku}} == === Bilêvkirin === * {{ku-kîte}} === Lêker === {{ku-tewîn-lk|peratin|form=gerguhêz|niha=perêj|borî=perat}} {{lêker|ku|c=x-gh}} # [[sotin]], [[şewitandin]] #* {{Jêgirtin/Feqiyê Teyran/Dîwan|jêgirtin=Bilbilên wê gulşenê, ji ezel her dibêjin</br>Heta ku der neymînî, ji me'den da dirêjin</br>Herfek ji wesfan tenê, alem '''naperêjin'''}} ==== Formên alternatîv ==== * [[perêhtin]] ==== Têkildar ==== * [[perijîn]] ==== Etîmolojî ==== Ji {{hv|ira-pro|*peri-}} + {{g|ira-pro|*tač}}, hevreha {{hv|ku|bihatin}} ko ji {{hv|ira-pro|*wi-}} + {{g|ira-pro|*tač}} e. ==== Werger ==== {{werger-ser}} {{werger-bin}} === Navdêr === {{navdêr|ku|z=m}} # {{mane?|ku}} ==== Werger ==== {{werger-ser}} {{werger-bin}} ==== Anagram ==== * {{anagram|ku|a=aeinprt|peritan}} jrl1j4k8deq8mjlxbky7jd63qa738pg MediaWiki:Gadget-wikt.add-examples.js 8 1272598 6903106 6902292 2026-11-10T21:29:10Z Wikihez 7344 Script pages under 100 KB: part of the code moved to a second file; shared code in one place 6903106 javascript text/javascript // <nowiki> // dependencies (in MediaWiki:Gadgets-definition): oojs, oojs-ui, ext.gadget.citoid-helper, // ext.gadget.StorageUtils, ext.gadget.wikt.core, ext.gadget.wikt.edit, ext.gadget.wikt.cite // // A ResourceLoader package module: it require()s wikt.cite, so it cannot be loaded as a plain // script. Load it through its gadget definition or the test loader. // // Two copies at once would fight over wikt.addExamples, so the second to arrive does nothing. window.wikt = window.wikt || {}; wikt.addExamples = wikt.addExamples || {}; var alreadyLoaded = wikt.addExamples.loaded; wikt.addExamples.loaded = true; // Formats a string using {0}, {1}, ... placeholders. if (!String.prototype.format) { String.prototype.format = function(var_args) { var args = arguments; return this.replace(/{(\d+)}/g, function(match, number) { return typeof args[number] !== "undefined" ? args[number] : match; }); }; } $(function() { "use strict"; if (alreadyLoaded) return; // Entries, reconstructions and sandboxes -- the same set DefSideBoxes and the wizard run on, from // wikt.core, so a page one of them works on is never a page this one quietly skips. if (!require("ext.gadget.wikt.core").sections.isEntryPage()) return; if (mw.config.get("wgPageName") === "Destpêk" || mw.config.get("wgArticleId") === 0) return; var NAME = "[[Mediawiki:Gadget-wikt.add-examples|Nimûne zêde bike]]"; // This gadget puts nothing on the page of its own. DefSideBoxes opens the form from its own // "Nimûne/jêgirtin" item, through openFor()/toggleFor() below, and takes the save over. window.wikt = window.wikt || {}; wikt.addExamples = wikt.addExamples || {}; // Lets another gadget take the save over: "Qeyd bike" becomes msg.add and the edit joins // that gadget's queue. Pass null to hand publishing back. Handler gets {transform, summary, done, fail}. wikt.addExamples.setSubmitHandler = function (fn) { wikt.addExamples.submitHandler = typeof fn === "function" ? fn : null; }; // Re-clicking the caller's own trigger closes the form again. wikt.addExamples.toggleFor = function (el) { var toggle = $(el).data("wiktAddExampleToggle"); if (!toggle) { return false; } toggle(); return true; }; wikt.addExamples.openFor = function (el) { var open = $(el).data("wiktAddExampleOpen"); if (!open) { return false; } open(); return true; }; // Opens the form on one example already under the sense el: itemEl is its <dd>/<li> on the page. wikt.addExamples.editFor = function (el, itemEl) { var editItem = $(el).data("wiktAddExampleEdit"); return !!editItem && editItem(itemEl); }; var VERSION = "2.0"; // Wiki markup for bold, both the button's label and what it inserts. var BOLD_MARKS = "'''"; // Persists form drafts (localStorage, cookie fallback) so unsaved text survives a failed submit. var draftStorage = new ObjectStorage("add_examples_draft", 1); // Citations are built by wikt.cite, the same module the wizard uses; Citoid lives in the // citoid-helper gadget. "citoid" as a _currentJederKey is a sentinel, not a real jederDefs key. var cite = require("ext.gadget.wikt.cite"); var core = require("ext.gadget.wikt.core"); var sections = core.sections; var msg = core.msg; var edit = require("ext.gadget.wikt.edit"); var jederDefs = cite.jederDefs; var nlToSpace = cite.nlToSpace; var boldenPageTitle = cite.boldenPageTitle; var CATEGORY_TEMPLATE_KEY = cite.CATEGORY_KEY; var RAW_TEMPLATE_KEY = cite.RAW_KEY; // Params the form already has boxes for; skipped when building a Kategorî:Çavkanî template's fields. var CATEGORY_TEMPLATE_COVERED_PARAMS = cite.COVERED_PARAMS; // A param labeled exactly this is filled from the language dropdown instead of shown as a field, // and only when the template's own TemplateData declares one. var CATEGORY_TEMPLATE_LANG_LABEL = cite.LANG_LABEL; // "span" is the field's width in the 12-column grid the fields render in (see .add-example-jeder-fields // in the CSS and _setSourceType below) -- field ORDER here is also the grid's reading order. // Heading/definition-list helpers and the recognized POS heading names, shared with the wizard and // DefSideBoxes so all three read the same page structure. var headingOuter = sections.headingOuter; var languageCodeForOl = sections.languageCodeForOl; var sectionOrdinalWithinLanguage = sections.sectionOrdinalWithinLanguage; var eachLeafDefinition = sections.eachLeafDefinition; // A heading's section key, the wizard's unclassified "=== Mane ===" placeholder included: it holds // real definitions, so examples belong in it, even though it is not a part of speech. var sectionKeyOf = sections.sectionKeyOf; // Locating a section and a definition inside it is wikt.edit's job, shared with DefSideBoxes. var wt = edit.wt; // Template names that count as real examples, so an annotation template is not mistaken for one. var exampleTemplateNames = ["jêder"].concat(cite.USEX_NAMES, Object.keys(jederDefs)); // The "no example yet" placeholder, with or without a language parameter. var EMPTY_EXAMPLE_MARKER = cite.EMPTY_EXAMPLE_MARKER; // The template name starting a "#:"/"#*" line, or null when the line does not start with one. function getLineTemplateName(line) { var m = /^#[:*]\s*\{\{\s*([^|}]+)/.exec(line); return m ? m[1].trim() : null; } // Where a template block starting at startIndex closes its braces, so multi-line calls stay whole. // The "#" run a definition line carries, so a sense's own annotations can be told from a sub-sense's: // "#:" belongs to "#", "##:" to "##". function senseDepth(definitionLine) { var m = /^#+/.exec(definitionLine || ""); return m ? m[0].length : 1; } // Any annotation of that sense -- "#:" an example, "#*" a citation -- at its own depth and no other. function annotationRegex(definitionLine) { return new RegExp("^#{" + senseDepth(definitionLine) + "}[:*]"); } function citationRegex(definitionLine) { return new RegExp("^#{" + senseDepth(definitionLine) + "}\\*"); } function findBlockEnd(lines, startIndex) { var depth = 0; var i = startIndex; for (; i < lines.length; i++) { var opens = (lines[i].match(/\{\{/g) || []).length; var closes = (lines[i].match(/\}\}/g) || []).length; depth += opens - closes; if (depth <= 0) { break; } } return i; } // URL of a template's own wiki page -- lets the user check a Kategorî:Çavkanî search result // before picking it (see showResults below). // Escapes str for interpolation into a RegExp: page titles carry regex metacharacters. var escapeRegExp = edit.util.escapeRegExp; // The readable half of an mw.Api rejection. var apiErrorMessage = edit.util.apiErrorMessage; // Same marker as the wizard's, positioned inside OOUI's own wrapper rather than a new one. var highlightLinebreak = edit.util.makeLinebreakHighlighter({ prefix: "add-example", wrap: false, dataKey: "addExampleLinebreakBackdrop", title: "Xeta n\u00fb d\u00ea wek <br> b\u00ea niv\u00eesandin" }); // Linebreak marking on the quote text and its translation/transcription only, not the // bibliographic fields. function watchForLinebreaks(widget) { widget.on("change", function() { highlightLinebreak(widget.$input[0]); }); } // The shared grip from wikt.edit, kept as a sibling inside OOUI's own wrapper and without the saved // height: these fields are plain MultilineTextInputWidgets with no class to key one by. function makeManualResizable(textarea) { edit.util.makeManualResizable(textarea, { wrap: false, persist: false, gripClass: "add-example-resize-grip", doneKey: "addExampleResizableDone" }); } // Applies makeManualResizable to every textarea in `context` (itself or its descendants). function initManualResize(context) { var $context = $(context); var $textareas = $context.is("textarea") ? $context : $context.find("textarea"); $textareas.each(function() { makeManualResizable(this); }); } var api = new mw.Api(); // Kategorî:Çavkanî's templates and their redirects, each redirect its own entry pointing at the // Language display name, read from the matching section's .besazimani link text. function languageNameForCode(code) { var $mark = $(".besazimani[id='" + code + "']").first(); return $mark.length ? $mark.text().trim() : null; } // Whether the page title itself (accented Kurdish Latin letters included) is written in Latin script. var pageTitleIsLatin = !/[^\x00-\x7FÀ-ɏḀ-ỿ‘’]/.test(mw.config.get("wgTitle") || ""); // What an example looks like once rendered: Şablon:jêder's old marker, Modul:usex's containers and // Modul:quote's wrapper. var EXAMPLE_SELECTOR = ".example, .benimune, .h-usage-example, .h-quotation, .citation-whole"; // The words drawn for one example: the example or quotation itself, never its translation or source. function exampleTextOf($item) { var $own = $item.find(".e-example, .e-quotation").first(); if ($own.length) { return $own.text(); } var $italic = $item.children("i").first(); return ($italic.length ? $italic : $item.clone().children("dl, ul, .dsb-example-toolbar").remove().end()).text(); } // A definition's own citation group, and the last one when it has several: a new citation is // written after every "#:"/"#*" line already there. function exampleGroupOf($li) { return $li.children("ul").filter(function () { return $(this).children("li").children(EXAMPLE_SELECTOR).length > 0; }).last(); } // Every recognized part-of-speech section, down to each of its definitions. Section, language and // nesting all come from the page's own structure, never from an example that happens to sit there. // "===" on most entries, "====" where the senses hang off numbered etymologies. $("h3, h4").each(function() { var sectionType = sectionKeyOf($(this)); if (!sectionType) { return; // not a recognized word-sense section (Bilêvkirin, Etîmolojî, Çavkanî, ...) } var sectionOrdinal = sectionOrdinalWithinLanguage($(this)); // Forward-walk siblings to the definitions <ol>, skipping over a declension table/TemplateStyles/the // headword line; stop (nothing to attach to in this section) if another heading comes first instead. var $ol = null; for (var $cursor = headingOuter($(this)).next(); $cursor.length; $cursor = $cursor.next()) { if ($cursor.is("ol")) { $ol = $cursor; break; } if (sections.headingDepth($cursor)) { break; } } if (!$ol) { return; } var language = languageCodeForOl($ol); if (!language) { console.warn("Gadget-wikt.add-examples: nikaribe koda zimanê vê beşê bibîne.", $ol); return; } eachLeafDefinition($ol, function($li) { var definitionLevel = []; var $definitionItem = $li; while ($definitionItem.prop("tagName") === "LI") { // Skip MediaWiki's empty <li class="mw-empty-elt"> so indices match the "#" lines in wikitext. definitionLevel.splice(0, 0, $definitionItem.prevAll("li:not(.mw-empty-elt)").length); $definitionItem = $definitionItem.parent().parent(); } definitionLevel.splice(0, 0, sectionType + "|" + sectionOrdinal); // Where a submitted example lands: after the group's last example, or after a hidden placeholder // when the definition has no group. var $group = exampleGroupOf($li); var $placeholder = $group.length ? $group.children("li").last() : $("<li style='display:none'></li>"); var $formItem = $("<li>"); // Never in the DOM: Form toggles it against the title, so it stays as the "is a form built // for this sense" handle. Nothing on the page opens this gadget -- DefSideBoxes does. var $button = $("<span>").hide(); // Says which sense and language the open form is for; the row exists only while it is open. var $title = $("<span class='add-example-form-title'></span>").hide(); $formItem.append($title); // The group the button lives in: the one on the page, or a new one. A "ul" under a definition // "li" starts hidden, so a new one needs the explicit display override. var $ul = $group.length ? $group : $("<ul>").css("display", "block").append($placeholder); // A citation already carrying the site's own small edit link does not need a second one. $ul.find("span.example > span.stubedit").remove(); // The panel opens after $ul, not inside it: full width, clear of the list's hanging indent. // edit, when given, is {index, text}: the form opens on that example instead of on a new one. function setFormVisible(visible, edit) { if (visible) { // A sense with no example list of its own gets one on first open, not on page load: // an empty list on every sense of every entry is margin nobody asked for. // Editing shows its title inside the form, so the list row is only for a new example. if (!edit && !$ul.parent().length) { $li.append($ul); } if (!edit && !$formItem.parent().length) { $ul.append($formItem); } if (edit) { $formItem.detach(); } if (!$button.form) { $title.text("Nimûneyeke bi " + (languageNameForCode(language) || "zimanekî nenas").toLowerCase() + " lê zêde dikî."); var form = new Form($placeholder, $button, $title, language, definitionLevel); form.onHide = function () { $formItem.detach(); }; $ul.after(form.$element); } // Right under the example being edited; after the whole list when adding a new one. (edit ? $(edit.item) : $ul).after($button.form.$element); if (edit) { $button.form.beginEdit(edit.index, edit.text); } else { $button.form.endEdit(); $button.form.setVisible(true); } } else if ($button.form) { $button.form.setVisible(false); $formItem.detach(); } return false; } function openForm() { return setFormVisible(true); } function toggleForm() { return setFormVisible(!($button.form && $button.form.isVisible())); } $li.data("wiktAddExampleOpen", openForm); $li.data("wiktAddExampleToggle", toggleForm); // Its place among the sense's examples finds the line; the words drawn for it confirm it. $li.data("wiktAddExampleEdit", function (itemEl) { var $item = $(itemEl); var index = sections.exampleItemsOf($li).index($item); if (index === -1) { return false; } setFormVisible(true, { index: index, text: exampleTextOf($item), item: itemEl }); return true; }); }); }); // Pulls the form panel out to the page's own edges, undoing the nested list's indent. function alignFormToPage($form) { var $container = $form.closest(".mw-parser-output"); if (!$container.length) { return; } // Measure unshifted: this runs on every re-open, and a margin left from last time would otherwise // count as part of the indent and cancel the alignment out. $form.css("marginLeft", 0); var pad = parseFloat(window.getComputedStyle($container[0]).paddingLeft) || 0; var containerLeft = $container.offset().left + pad; var indent = $form.offset().left - containerLeft; $form.css({ marginLeft: indent > 0 ? -indent : 0, width: $container.width() + "px" }); } /** * language is the section the sense sits in: what the new example is tagged with, and what the * wikitext lookup searches for. Never the language of an example that happens to be there. */ function Form($lastExample, $button, $title, language, definitionLevel) { var self = this; this._language = language; this._definitionLevel = definitionLevel; this._$lastExample = $lastExample; this._$button = $button; this._$button.form = this; this._$title = $title; // Where the title lives when adding; while editing it moves into the form itself. this._$titleHome = $title.parent(); this._currentJederKey = ""; this._jederFieldWidgets = {}; // Set once a template is picked in the Kategorî:Çavkanî mode (see _selectCategoryTemplate); the // canonical template title (without the "Şablon:" prefix), used as `sablon` in _doSubmit. this._categoryTemplateName = null; // Set by _renderCategoryTemplateFields if the picked template's TemplateData has a "koda zimanî" param; // that param's own key, used to emit "<key>=<language>" in _doSubmit -- null if no such param exists. this._categoryTemplateLangParamKey = null; // Set while an example already on the page is being edited (see beginEdit): {index, original, // example, key}, where key is the source type the line was read into. this._editing = null; this._addTitle = null; // The same prompt the wizard's own example box carries, so the two ask for an example in one // wording and the language it is to be written in is on screen here too. this._textInput = new OO.ui.MultilineTextInputWidget({ rows: 4, placeholder: edit.util.examplePrompt(languageNameForCode(language), false) }); var textInputLayout = new OO.ui.FieldLayout(this._textInput, { label: "Metna nimûneyê", align: "top", }); // Under the text box: what will happen to the entry's own name on save (see boldenPageTitle), and // the one markup button worth having beside it. Purely informational, never blocks submitting. this._titleBoldHint = $("<span class='add-example-title-bold-hint'></span>"); this._updateTitleBoldHint = function() { var text = self._textInput.getValue().trim(); var title = mw.config.get("wgTitle"); var missing = !!text && !boldenPageTitle(text, title).bolded; self._titleBoldHint .toggleClass("add-example-title-bold-warn", missing) .html(missing ? "Sermade (" + mw.html.escape(title) + ") nehat dîtin; dê neyê <b>stûr</b>kirin." : "Sermade otomatîk tê <b>stûr</b>kirin."); // Only where it is needed: when the name is bolded on its own there is nothing to reach for. self._boldButton.toggle(missing); }; // Shown only while the name cannot be bolded on its own -- a form the matcher does not recognise, // or another word the writer wants bolded. this._boldButton = $("<button type='button' class='add-example-bold-btn'></button>").hide() .text(BOLD_MARKS).attr("title", "Nivîsa hilbijartî stûr bike"); this._boldButton.on("click", function(e) { e.preventDefault(); var input = self._textInput.$input[0]; var value = input.value; var start = input.selectionStart; var end = input.selectionEnd; var next, caret; if (start !== end) { next = value.slice(0, start) + BOLD_MARKS + value.slice(start, end) + BOLD_MARKS + value.slice(end); caret = end + BOLD_MARKS.length * 2; } else { // Nothing selected: just the marks, where the caret is. Typing carries on after them. next = value.slice(0, start) + BOLD_MARKS + value.slice(start); caret = start + BOLD_MARKS.length; } self._textInput.setValue(next); input.focus(); input.setSelectionRange(caret, caret); }); this._textTools = $("<div class='add-example-text-tools'></div>") .append(this._boldButton, this._titleBoldHint); this._updateTitleBoldHint(); // A pasted citation switches to raw mode rather than being wrapped in a second {{nimûne}}: a whole // line with its marker and a bare template call both count. Only while the type is still citoid. this._maybeSwitchToRaw = function(value) { if (self._currentJederKey !== "citoid" || !(cite.looksLikeCitationLine(value) || cite.looksLikeTemplate(value))) { return; } var menu = self._sourceTypeDropdown.getMenu(); var item = menu.findItemFromData(RAW_TEMPLATE_KEY); if (item) { menu.chooseItem(item); // calls _setSourceType() itself } }; this._textInput.on("change", function(value) { self._applyButton.setDisabled(!value); self._maybeSwitchToRaw(value); self._updateReadyHint(); self._updateTitleBoldHint(); }); watchForLinebreaks(this._textInput); // Source type: free text, a jêder-* template, or "citoid" -- a sentinel meaning "not yet detected", // switched automatically once a Citoid fetch succeeds. var sourceTypeMenuItems = [ new OO.ui.MenuOptionWidget({ data: "citoid", label: msg.citoidAuto }), new OO.ui.MenuOptionWidget({ data: "", label: msg.sourceByHand }), ]; for (var jederKey in jederDefs) { if (jederDefs.hasOwnProperty(jederKey)) { sourceTypeMenuItems.push(new OO.ui.MenuOptionWidget({ data: jederKey, label: jederDefs[jederKey].label, })); } } sourceTypeMenuItems.push(new OO.ui.MenuOptionWidget({ data: CATEGORY_TEMPLATE_KEY, label: msg.sourceFromCategory, })); // Paste a finished citation into the example box and have it saved exactly as typed -- the quickest // way to reuse one jêgirtin across several entries. sourceTypeMenuItems.push(new OO.ui.MenuOptionWidget({ data: RAW_TEMPLATE_KEY, label: msg.sourceRaw, })); // Shown only in raw mode, where there is nothing else to fill in. this._rawTemplateHint = new OO.ui.MessageWidget({ type: "notice", inline: true, label: "Şablona temam li qada \"Metna nimûneyê\" binivîse \u2014 wek xwe tê qeydkirin. " + "Tenê navê maddeyê yê di nav jêgirtin= de tê stûrkirin.", classes: ["add-example-raw-hint"], }); this._rawTemplateHint.toggle(false); this._sourceTypeDropdown = new OO.ui.DropdownWidget({ menu: { items: sourceTypeMenuItems }, label: msg.citoidAuto, }); var sourceTypeLayout = new OO.ui.FieldLayout(this._sourceTypeDropdown, { label: "Şablona çavkaniyê", align: "top", }); this._sourceTypeDropdown.getMenu().on("choose", function(menuOption) { self._setSourceType(menuOption.getData()); }); this._sourceInput = new OO.ui.MultilineTextInputWidget(); this._sourceInput.on("change", function() { self._updateReadyHint(); }); this._sourceInputLayout = new OO.ui.FieldLayout(this._sourceInput, { label: "Çavkaniya nimûneyê", align: "top", }); // Visible in every mode: submit() reads it as "url=" regardless. The Citoid button sits beside it. this._sourceURLInput = new OO.ui.TextInputWidget({ icon: "link", placeholder: "https://… an jî DOI/ISBN", }); this._citoidButton = new OO.ui.ButtonWidget({ label: msg.citoidFill, icon: "download", flags: ["progressive"], title: "Li gorî URL, DOI an ISBNya jorîn, malûmatan ji Citoid tîne û qadên şablona jêder-* otomatîk tijî dike.", classes: ["add-example-citoid-fetch"], }); this._citoidButton.on("click", this._fillFromCitoid.bind(this)); this._sourceURLInput.on("change", function() { self._updateReadyHint(); self._autofillVideoTimestamp(); }); // URL input and its auto-fill button as one flex row; a bare OO.ui.Widget used as the container. this._urlRowWidget = new OO.ui.Widget({ classes: ["add-example-url-row"] }); this._urlRowWidget.$element.append(this._sourceURLInput.$element, this._citoidButton.$element); this._sourceURLInputLayout = new OO.ui.FieldLayout(this._urlRowWidget, { label: "Lînk, DOI an ISBNya çavkaniyê", align: "top", help: "Li qutiyê URL, DOI an ISBN binivîse û pê lê bike butona '" + msg.citoidFill + "' da ku otomatîk çavkanî were dagirtin.", helpInline: true, }); // Same wording as above, so the initial _setSourceType("citoid") rebuilds nothing. this._urlFieldIsFreeTextWording = false; // Shown right after a successful Citoid fetch (see _fillFromCitoid); hidden again on clear. this._citoidSuccessMsg = $("<span class='add-example-citoid-success'></span>").text(msg.citoidFilled).hide(); // Shown when Citoid found nothing and a form was opened to be filled by hand. this._citoidFallbackMsg = $("<span class='add-example-citoid-fallback'></span>").hide(); // Clears the URL field and any filled jêder-* fields without touching the example text/translation. this._citoidClearButton = new OO.ui.ButtonWidget({ label: "Vala bike", icon: "trash", framed: false, title: "Lînkê û qadên jêder-* yên vê beşê vala bike", classes: ["add-example-citoid-clear"], }); this._citoidClearButton.on("click", function() { self._sourceURLInput.setValue(""); self._citoidSuccessMsg.hide(); self._citoidFallbackMsg.hide(); Object.keys(self._jederFieldWidgets).forEach(function(key) { self._jederFieldWidgets[key].setValue(""); }); }); // Plain div (not OO.ui.FieldsetLayout) so the grid CSS class applies directly to the field items. this._jederFieldsFieldset = $("<div class='add-example-jeder-fields'>"); // Header and clear button above the fields grid. Only meaningful once a real jêder-* template is // active, so the whole section toggles together in _setSourceType(). this._jederSection = $("<div class='add-example-jeder-section'>").append( $("<div class='add-example-jeder-header'>") .append($("<span class='add-example-jeder-header-label'>Malûmatên çavkaniyê</span>")) .append(this._citoidClearButton.$element), this._jederFieldsFieldset ); // wikt.cite's picker, the one the wizard and DefSideBoxes show. Sits under the source-type // dropdown, not in the fields section, and is built once so switching modes keeps its handlers. var categoryPicker = cite.buildCategoryTemplateSearch({ describe: true, onPick: function (entry) { self._selectCategoryTemplate(entry); } }); this._categoryTemplateInput = categoryPicker.$input; this._categoryTemplateStatus = categoryPicker.$status; this._categoryTemplateSearchRow = categoryPicker.$element .addClass("add-example-cat-tpl-search-row").hide(); this._translationInput = new OO.ui.MultilineTextInputWidget({ rows: 4 }); var translationInputLayout = new OO.ui.FieldLayout(this._translationInput, { // A Kurmancî example gets an explanation rather than a translation. label: language === "ku" ? "Îzaheke kurmancî ya nimûneyê" : "Tercimeya kurmancî ya nimûneyê", align: "top", }); watchForLinebreaks(this._translationInput); this._transcriptionInput = new OO.ui.MultilineTextInputWidget({ rows: 4 }); var transcriptionInputLayout = new OO.ui.FieldLayout(this._transcriptionInput, { label: "Translîterasyona nimûneyê", align: "top", help: "Tenê lê zêde bike eger metna di nimûneyê de bi alfabeya latînî nehatibe nivîsandin.", helpInline: true, }); watchForLinebreaks(this._transcriptionInput); this._applyButton = new OO.ui.ButtonWidget({ label: "Qeyd bike", icon: "check", title: "Nimûneyê ji bo vê tarîfê nimûneyê lê zêde bike", flags: ["progressive", "primary"], disabled: true, }); this._applyButton.on("click", this.submit.bind(this)); // Reactive "ready to submit" hint next to Qeyd bike: no text yet, text but no source (still submittable // as a bare {{nimûne}}), or fully ready -- kept in sync by _updateReadyHint(), wired to every field below. this._readyHint = $("<span class='add-example-ready-hint'></span>").text("Metna nimûneyê lazim e."); // True while a Citoid-eligible URL is typed but unfetched and no jêder field has been filled by // hand -- the "looks done but isn't" case. this._citoidPending = function() { return self._currentJederKey !== "" && self._currentJederKey !== CATEGORY_TEMPLATE_KEY && self._currentJederKey !== RAW_TEMPLATE_KEY && !!self._sourceURLInput.getValue().trim() && !self._citoidSuccessMsg.is(":visible") && !Object.keys(self._jederFieldWidgets).some(function(key) { return self._jederFieldWidgets[key].getValue().trim(); }); }; // A Kurmancî jêgirtin takes no translation or explanation, only a {{nimûne}} does. An edited line // that already has one keeps its box. this._syncTranslationBox = function() { if (self._language !== "ku") { return; } var key = self._currentJederKey; var isQuote = key === RAW_TEMPLATE_KEY || key === CATEGORY_TEMPLATE_KEY || !!jederDefs[key] || !!self._sourceInput.getValue().trim() || !!self._sourceURLInput.getValue().trim(); translationInputLayout.toggle(!isQuote || !!(self._editing && self._translationInput.getValue().trim())); }; this._updateReadyHint = function() { self._syncTranslationBox(); if (!self._textInput.getValue()) { self._readyHint.text("Metna nimûneyê lazim e."); return; } if (self._citoidPending()) { self._readyHint.text("Pêşî '" + msg.citoidFill + "' bitikîne."); return; } // Raw mode carries its own source inside the pasted template, so there is nothing else to ask for. if (self._currentJederKey === RAW_TEMPLATE_KEY) { self._readyHint.text("Hazir e."); return; } var hasSource = self._sourceInput.getValue().trim() || self._sourceURLInput.getValue().trim() || Object.keys(self._jederFieldWidgets).some(function(key) { return self._jederFieldWidgets[key].getValue().trim(); }); self._readyHint.text(hasSource ? "Hazir e." : "Çavkanî nehatiye dayîn."); }; // Prefills Dem from the URL's own "t=" once jêder-vîdeo is active; never overwrites typed text. this._autofillVideoTimestamp = function() { var demWidget = self._jederFieldWidgets.dem; if (self._currentJederKey !== "jêder-vîdeo" || !demWidget || demWidget.getValue().trim()) { return; } var dem = wikt.citoid.extractVideoTimestamp(self._sourceURLInput.getValue().trim()); if (dem && !demWidget.getValue().trim()) { demWidget.setValue(dem); } }; // framed:false (+ the "destructive" flag for the red text/hover) matches the mockup's borderless // "Betal bike" -- a plain colored link rather than a boxed button, distinguishing it from Paqij bike. this._cancelButton = new OO.ui.ButtonWidget({ label: "Betal bike", icon: "close", title: "Formê bigire", flags: ["destructive"], framed: false, }); this._cancelButton.on("click", function() { self.setVisible(false); }); this._clearButton = new OO.ui.ButtonWidget({ label: "Paqij bike", icon: "trash", title: "Hemû qadan vala bike û pêşnûyisên hilanîbûyî jê bibe", }); this._clearButton.on("click", function() { self.clear(); self._clearDraft(); }); // Only while an example on the page is being edited. this._deleteButton = new OO.ui.ButtonWidget({ label: "Jê bibe", icon: "trash", flags: ["destructive"], }); this._deleteButton.toggle(false); this._deleteButton.on("click", function() { OO.ui.confirm("Ev nimûne dê were rakirin. Dewam bike?").done(function(ok) { if (ok) { self._submitDelete(); } }); }); // A CSS spinner rather than a Commons GIF: no request, and it follows dark/light mode. this._loadingImage = new OO.ui.Widget({ classes: ["add-example-loading"], }); // The ring alone says nothing to a reader who has asked for less motion: MediaWiki's own // reduced-motion reset cuts every animation to 0.01ms, so it never turns for them. The words // beside it carry the state either way. this._loadingImage.$element.append( $('<span class="add-example-spinner" aria-hidden="true"></span>'), $('<span class="add-example-loading-label"></span>').text(msg.loading) ); this._loadingImage.toggle(false); // Two-column top: example text (with translation/transliteration for non-ku) on the left, source // type and link on the right. Plain divs, so the CSS can lay them out as a grid. // The translation box is there for Kurmancî too: {{nimûne}} takes one for every language. var $topLeft = $("<div class='add-example-top-col'>").append(textInputLayout.$element, this._textTools, translationInputLayout.$element); if (!pageTitleIsLatin) { $topLeft.append(transcriptionInputLayout.$element); } // Kept for _fill, which adds either box when an edited line already carries one. this._$topLeft = $topLeft; this._translationInputLayout = translationInputLayout; this._transcriptionInputLayout = transcriptionInputLayout; var $topRight = $("<div class='add-example-top-col'>").append( sourceTypeLayout.$element, this._categoryTemplateSearchRow, this._rawTemplateHint.$element, this._sourceInputLayout.$element, this._sourceURLInputLayout.$element, this._citoidSuccessMsg, this._citoidFallbackMsg ); var $topGrid = $("<div class='add-example-top-grid'>").append($topLeft, $topRight); // Saved on the instance (not just a local var) so _setSourceType() can swap the URL field's label/help // wording in and out of it when switching to/from free text (see the FieldLayout replaceWith() there). this._fieldsLayout = $("<div class='add-example-fieldset'>").append($topGrid, this._jederSection); // Bottom action row: Qeyd bike (+ spinner and hint), a spacer, then Paqij bike and Betal bike. A // flex div rather than an OOUI layout, so the hint and spacer can sit among the widgets. var $buttonsRow = $("<div class='add-example-main-buttons'>").append( this._applyButton.$element, this._loadingImage.$element, this._readyHint, $("<span class='add-example-buttons-spacer'></span>"), this._deleteButton.$element, this._clearButton.$element, this._cancelButton.$element ); this._frame = new OO.ui.PanelLayout({ id: "add-example-definition-{0}-form".format(this._definitionLevel.join("-")), classes: ["add-example-form"], expanded: false, }); this._frame.$element.append(this._fieldsLayout, $buttonsRow); // selectItemByData() does not fire "choose", so the initial mode is set up by hand. this._sourceTypeDropdown.getMenu().selectItemByData("citoid"); this._setSourceType("citoid"); initManualResize(this._frame.$element); } Form.prototype = { // Returns the jQuery object for this form. get $element() { return this._frame.$element; }, // Switches the source section between free text, Citoid auto-detect and a chosen jêder-* template, // rebuilding the field list. The URL field stays visible in every mode. _setSourceType: function(jederKey) { var self = this; this._currentJederKey = jederKey; this._citoidFallbackMsg.hide(); this._jederFieldsFieldset.empty(); this._jederFieldWidgets = {}; this._categoryTemplateName = null; this._categoryTemplateLangParamKey = null; var isRealJederKey = jederKey && jederDefs[jederKey]; var isCategoryTemplateKey = jederKey === CATEGORY_TEMPLATE_KEY; var isRawTemplate = jederKey === RAW_TEMPLATE_KEY; if (isRealJederKey) { var def = jederDefs[jederKey]; def.fields.forEach(function(field) { var widget = field.multiline ? new OO.ui.MultilineTextInputWidget({ rows: 2, autosize: false }) : new OO.ui.TextInputWidget(); self._jederFieldWidgets[field.key] = widget; widget.on("change", function(value) { // sal and tarîx say the same thing twice and Modul:quote refuses both; filling one // clears the other rather than letting the citation break on save. var partner = value && cite.exclusivePartner(field.key); if (partner && self._jederFieldWidgets[partner]) { self._jederFieldWidgets[partner].setValue(""); } self._updateReadyHint(); }); var layout = new OO.ui.FieldLayout(widget, { label: field.label, align: "top", classes: ["add-example-field-span-" + (field.span || 4)], }); self._jederFieldsFieldset.append(layout.$element); }); } // The box lives in $topRight always; here it is only shown for this mode, and reset on the way out // so a stale query does not linger. this._categoryTemplateSearchRow.toggle(isCategoryTemplateKey); // Raw mode has no source fields of its own: the citation is already written out in full. this._rawTemplateHint.toggle(isRawTemplate); if (!isCategoryTemplateKey) { this._categoryTemplateInput.val(""); } // The fields grid is for a real jêder-* template. The Kategorî:Çavkanî picker shows it only once a // template is picked (see _selectCategoryTemplate). this._jederSection.toggle(!!isRealJederKey); // Free text textarea only for "" (free text); hidden for "citoid" and any real jêder-* key. this._sourceInputLayout.toggle(jederKey === ""); // No Citoid button for free text (nothing to fill) or the Kategorî:Çavkanî picker (no mapping for // an arbitrary template); shown for auto-detect and any jêder-* key. this._citoidButton.toggle(jederKey !== "" && !isCategoryTemplateKey && !isRawTemplate); // Plain "attach a link" wording for free text, Citoid wording otherwise. Rebuilt only when crossing // that boundary. var isFreeText = jederKey === ""; if (this._urlFieldIsFreeTextWording !== isFreeText) { this._urlFieldIsFreeTextWording = isFreeText; // On the <input> itself: TextInputWidget has no setter for its placeholder. this._sourceURLInput.$input.attr("placeholder", isFreeText ? "URLyeke wekî https://…" : "URLyek wekî https://… an jî DOI/ISBN"); var $oldLayout = this._sourceURLInputLayout.$element; // Re-wraps the SAME _urlRowWidget (still holding _sourceURLInput + _citoidButton) in a new // FieldLayout with the new label/help; the widget's $element moves along with it. this._sourceURLInputLayout = new OO.ui.FieldLayout(this._urlRowWidget, isFreeText ? { label: "Adresa webê ya nimûneyê", align: "top", help: "Tenê binivîse eger lînk jixwe di referansa çavkaniyê de tinebe.", helpInline: true, } : { label: "Lînk, DOI an ISBNya çavkaniyê", align: "top", help: "Ev dibe lînka çavkaniyê (url=). Ji bo şablonên jêder-*, dikare bi Citoid an bi destan jî bên dagirtin.", helpInline: true, }); $oldLayout.replaceWith(this._sourceURLInputLayout.$element); } // The URL/Citoid row is normally always visible (see the constructor comment), but Citoid has no // mapping for an arbitrary Kategorî:Çavkanî template, so it'd just be dead UI in that one mode. this._sourceURLInputLayout.toggle(!isCategoryTemplateKey && !isRawTemplate); this._autofillVideoTimestamp(); this._updateReadyHint(); initManualResize(this._jederFieldsFieldset); // covers a freshly-rebuilt "sernav" field (jêder-*); the Kategorî:Çavkanî picker's own fields are resized separately once picked, see _renderCategoryTemplateFields }, // Wires the search box's handlers and starts the template fetch. Called once: the box lives in // $topRight and is only shown or hidden by _setSourceType. // Records the picked template's canonical name -- a redirect resolves to its target -- and shows // its parameter fields. _selectCategoryTemplate: function(entry) { // Left as the plain label (not "label → target") -- putting the arrow in the input's own value // would get re-used as the search query if the box is focused again to pick something else. this._categoryTemplateInput.val(entry.label); this._categoryTemplateName = entry.target; this._jederSection.toggle(true); return this._renderCategoryTemplateFields(entry.target); }, // One text input per TemplateData param of `name`, into the shared "Malûmatên çavkaniyê" grid, or a // lone "Rûpel" box when the template has no TemplateData. _renderCategoryTemplateFields: function(name) { var self = this; this._jederFieldsFieldset.empty(); this._jederFieldWidgets = {}; this._categoryTemplateLangParamKey = null; return cite.templateParams(name).then(function(params) { // A param labeled "koda zimanî" is filled from the language dropdown: found before filtering, so // it is left out of the fields and emitted by _doSubmit. (params || []).forEach(function(p) { if ((p.label || "").trim().toLowerCase() === CATEGORY_TEMPLATE_LANG_LABEL) { self._categoryTemplateLangParamKey = p.key; } }); var filtered = (params || []).filter(function(p) { return CATEGORY_TEMPLATE_COVERED_PARAMS.indexOf(p.key) === -1 && p.key !== self._categoryTemplateLangParamKey; }); var list = filtered.length ? filtered : [{ key: "rûpel", label: "Rûpel", required: false }]; list.forEach(function(p) { // Reuse the jêder-* field's own width and multiline when the key matches one (cite.fieldSize). var sizeHint = cite.fieldSize(p.key); var widget = (sizeHint && sizeHint.multiline) ? new OO.ui.MultilineTextInputWidget({ rows: 2, autosize: false }) : new OO.ui.TextInputWidget(); self._jederFieldWidgets[p.key] = widget; widget.on("change", function() { self._updateReadyHint(); }); var layout = new OO.ui.FieldLayout(widget, { label: p.label, align: "top", classes: ["add-example-field-span-" + (sizeHint ? sizeHint.span : 4)], }); self._jederFieldsFieldset.append(layout.$element); }); self._updateReadyHint(); initManualResize(self._jederFieldsFieldset); // matters now that a matched key like "sernav" can render multiline }); }, // Fetches data via wikt.citoid, picks the matching jêder-* template, and fills its empty fields. _fillFromCitoid: function() { var self = this; var query = this._sourceURLInput.getValue().trim(); if (!query) { mw.notify(msg.citoidNeedsUrl); return; } this._citoidButton.setDisabled(true); this._loadingImage.toggle(true); cite.fetchFromCitoid(query).then(function(result) { // Citoid may pick the type only while none is chosen; a hand-picked one is kept. var wasAutoDetect = self._currentJederKey === "citoid"; if (wasAutoDetect) { var menu = self._sourceTypeDropdown.getMenu(); var item = menu.findItemFromData(result.jederKey); if (item) { menu.chooseItem(item); // this calls _setSourceType(jederKey) automatically } } var targetJederKey = self._currentJederKey; // Citoid shaped its fields for the type it detected; re-derive them when the active template // differs. var fields = targetJederKey === result.jederKey ? result.fields : cite.citoidFieldsFor(targetJederKey, result.raw, query); Object.keys(fields).forEach(function(key) { var widget = self._jederFieldWidgets[key]; if (widget && !widget.getValue()) { widget.setValue(fields[key]); } }); self._citoidFallbackMsg.hide(); self._citoidSuccessMsg.show(); self._updateReadyHint(); mw.notify("Malûmat ji Citoid hatin barkirin."); }, function() { self._openFormForUnfound(query); }).always(function() { self._citoidButton.setDisabled(false); self._loadingImage.toggle(false); }); }, // Citoid found nothing: open the form the query points to (Kitêb for an ISBN, Kovar for a DOI, // Websîte for a link, free text otherwise) and say what is left to fill in by hand. _openFormForUnfound: function(query) { var menu = this._sourceTypeDropdown.getMenu(); var id = wikt.citoid.identifierOf(query); var isLink = /^https?:\/\//i.test(query); var opened = this._currentJederKey === "citoid"; if (opened) { menu.chooseItem(menu.findItemFromData(id ? id.jederKey : isLink ? "jêder-websîte" : "")); // calls _setSourceType() } var key = this._currentJederKey; var idWidget = id && this._jederFieldWidgets[id.field]; if (idWidget) { // The identifier now has its own field, so the link box would only repeat it as url=. if (!idWidget.getValue()) { idWidget.setValue(id.value); } this._sourceURLInput.setValue(""); } var siteWidget = isLink && this._jederFieldWidgets["websîte"]; if (siteWidget && !siteWidget.getValue()) { siteWidget.setValue(wikt.citoid.websiteNameForUrl(query)); } var text; if (key === "") { if (!this._sourceInput.getValue().trim()) { this._sourceInput.setValue(query); } this._sourceURLInput.setValue(""); text = "Ev ne lînk, DOI an ISBN e. Çavkaniyê li qutiya «Çavkaniya nimûneyê» bi destê xwe binivîse."; } else { text = "Citoid malûmat nedîtin. " + (opened ? "Forma «" + jederDefs[key].label + "» hate vekirin: qadên" : "Qadên") + " mayî bi destê xwe tijî bike."; } this._citoidSuccessMsg.hide(); this._citoidFallbackMsg.text(text).show(); this._updateReadyHint(); }, // Shows or hides the form and restores any saved draft. The title takes the button's place while // open; the button stays put for when it closes. isVisible: function() { return this._frame.isVisible(); }, setVisible: function(visible) { this._$button.toggle(!visible); this._$title.toggle(visible); this._frame.toggle(visible); // Closing from the form's own "Betal bike" has to take the row away too, or the empty <li> // it lives in is left behind as a bullet with nothing after it. if (!visible && typeof this.onHide === "function") { this.onHide(); } if (visible) { // Says what the button will actually do: publish now, or join whoever has taken the save // over. Set here rather than at build time, since the owner may install itself later. this._applyButton.setLabel(wikt.addExamples.submitHandler ? msg.add : "Qeyd bike"); alignFormToPage(this._frame.$element); // A draft belongs to a new example; an edited one opens with what its line says. var draft = this._editing ? {} : (draftStorage.Get() || {}); if (!this._textInput.getValue() && draft.text) { this._textInput.setValue(draft.text); } if (!this._sourceInput.getValue() && draft.source) { this._sourceInput.setValue(draft.source); } if (!this._sourceURLInput.getValue() && draft.sourceURL) { this._sourceURLInput.setValue(draft.sourceURL); } if (!this._translationInput.getValue() && draft.translation) { this._translationInput.setValue(draft.translation); } if (!this._transcriptionInput.getValue() && draft.transcription) { this._transcriptionInput.setValue(draft.transcription); } } }, // Opens the form on the index-th example of this sense. drawnText is what the page shows for it: the // line found at that place has to say the same before anything from it is put in the form. beginEdit: function(index, drawnText) { var self = this; this.clear(); this._editing = { index: index, original: null, example: null, key: null }; if (this._addTitle === null) { this._addTitle = this._$title.text(); } this._$title.text("Vê nimûneyê diguherînî."); this._frame.$element.prepend(this._$title); this._deleteButton.toggle(true); this.setVisible(true); this._applyButton.setDisabled(true); this._deleteButton.setDisabled(true); this._loadingImage.toggle(true); var sectionKeyParts = this._definitionLevel[0].split("|"); api.get({ action: "query", prop: "revisions", titles: mw.config.get("wgPageName"), rvprop: "content", rvslots: "main", formatversion: 2 }).then(function(data) { var editing = self._editing; if (!editing || editing.index !== index || editing.original !== null) { return; // closed, or opened on another example, while this was loading } var content = data.query.pages[0].revisions[0].slots.main.content; // The page as it will read once the queued changes are saved, so an example already edited // in the queue is found as it is drawn. if (typeof wikt.addExamples.pendingContent === "function") { content = wikt.addExamples.pendingContent(content); } var loc = wt.locateSection(content, self._language, sectionKeyParts[0], parseInt(sectionKeyParts[1], 10)); var def = wt.findDefinitionLine(loc.lines, loc.posLineIndex, self._definitionLevel); var block = cite.exampleBlocks(loc.lines, def).filter(function(b) { return b.example; })[index]; if (!block || !cite.exampleMatches(drawnText, block.example)) { throw new edit.util.SubmitAbort("Ev nimûne di wîkîtextê de nehat dîtin — rûpelê nû bike û dîsa biceribîne."); } editing.original = block.text; editing.example = block.example; return self._fill(block.example).then(function(aliasMap) { editing.aliases = aliasMap; editing.key = self._currentJederKey; self._syncTranslationBox(); self._deleteButton.setDisabled(false); self._loadingImage.toggle(false); }); }).catch(function(err) { self._loadingImage.toggle(false); mw.notify(err instanceof edit.util.SubmitAbort ? err.message : "Nikare muhtewaya heyî bixwîne.", { type: "error" }); self.setVisible(false); self.endEdit(); }); }, // Leaves edit mode: the next time the form opens, it adds a new example again. endEdit: function() { if (!this._editing) { return; } this._editing = null; this._deleteButton.toggle(false); if (this._addTitle !== null) { this._$title.text(this._addTitle); } this._$titleHome.append(this._$title); this.clear(); }, // Puts a line read by cite.parseCitationLine() into the form. A plain italic line and every // {{nimûne}} redirect read as {{nimûne}}, so saving writes them as that. Resolves once the fields // that depend on the template's own parameters -- a Kategorî:Çavkanî template's boxes, an alias // such as "r=" for "rûpel=" -- are drawn and filled. _fill: function(example) { var self = this; var key = example.mode === "jeder" ? example.template : example.mode === "category" ? CATEGORY_TEMPLATE_KEY : example.mode === "raw" ? RAW_TEMPLATE_KEY : ""; this._sourceTypeDropdown.getMenu().selectItemByData(key); this._setSourceType(key); this._sourceInput.setValue(example.source); this._sourceURLInput.setValue(example.sourceURL); this._transcriptionInput.setValue(example.transcription); var drawn = example.mode === "category" ? this._selectCategoryTemplate({ label: example.template, target: example.template }) : $.Deferred().resolve().promise(); var aliasTemplate = example.mode === "jeder" || example.mode === "category" ? example.template : example.mode === "nimune" ? "nimûne" : example.mode === "source" ? "jêder" : null; var aliases = aliasTemplate ? cite.templateAliases(aliasTemplate) : $.Deferred().resolve({}).promise(); return $.when(drawn, aliases).then(function(_, aliasMap) { cite.applyAliases(example, aliasMap || {}, Object.keys(self._jederFieldWidgets)); // buildCitation() writes the language parameter itself. var langKey = self._categoryTemplateLangParamKey; if (langKey) { example.extra = example.extra.filter(function(param) { return $.trim(param.split("=")[0]) !== langKey; }); } self._textInput.setValue(example.text); self._translationInput.setValue(example.translation); Object.keys(example.fields).forEach(function(field) { self._jederFieldWidgets[field].setValue(example.fields[field]); }); // A box the form leaves out for this language still opens when the line already has something in it. [[example.translation, self._translationInputLayout], [example.transcription, self._transcriptionInputLayout]] .forEach(function(pair) { if (pair[0] && !$.contains(self._$topLeft[0], pair[1].$element[0])) { self._$topLeft.append(pair[1].$element); } }); // Kept for the save, which puts the rebuilt line's parameters back in the page's order. return aliasMap || {}; }); }, // Removes the example being edited, through the same hand-over as a save. _submitDelete: function() { var self = this; function settle() { self._deleteButton.setDisabled(false); self._applyButton.setDisabled(!self._textInput.getValue()); } this._applyButton.setDisabled(true); this._deleteButton.setDisabled(true); this._sendReplacement(null, "−nimûneya bikaranînê bi amûra « {0} » (v{1}).".format(NAME, VERSION), settle, function(message) { mw.notify(message || msg.saveFailed, { type: "error" }); settle(); }); }, // Replaces the example being edited with code, or removes it when code is null: queued with // DefSideBoxes when it has taken the save over, published at once otherwise. _sendReplacement: function(code, summary, reenable, error) { var self = this; var editing = this._editing; var parts = this._definitionLevel[0].split("|"); var language = this._language; var definitionLevel = this._definitionLevel; function transform(revision) { return { text: cite.replaceExample(revision.content, language, parts[0], parseInt(parts[1], 10), definitionLevel, editing.index, editing.original, code), summary: summary }; } var handOver = wikt.addExamples.submitHandler; if (typeof handOver === "function") { try { handOver({ transform: transform, summary: summary, removes: code === null, // No key of its own: an example edited twice is two entries, applied in turn, the // second reading the line as the first left it. done: function() { self.setVisible(false); self.endEdit(); reenable(); }, fail: function(message) { error(message || "Nikare li rêza guhartinan zêde bike."); } }); } catch (e) { error("Nikare li rêza guhartinan zêde bike: " + (e && e.message ? e.message : e)); } return; } api.edit(mw.config.get("wgPageName"), transform).then(function() { location.reload(); }, function(codeOrAbort, result) { error(codeOrAbort instanceof edit.util.SubmitAbort ? codeOrAbort.message : msg.saveFailed + apiErrorMessage(codeOrAbort, result)); }); }, // Drops the saved draft so it cannot resurface on another page. _clearDraft: function() { draftStorage.Set({}); }, // Clears all fields in this form. clear: function() { this._textInput.setValue(""); this._sourceInput.setValue(""); this._sourceURLInput.setValue(""); this._translationInput.setValue(""); this._transcriptionInput.setValue(""); this._citoidSuccessMsg.hide(); this._citoidFallbackMsg.hide(); this._sourceTypeDropdown.getMenu().selectItemByData("citoid"); this._setSourceType("citoid"); }, // A link, DOI or ISBN typed but never fetched is fetched first, and the filled form shown, rather than // saved as a bare url=. A name that cannot be bolded is reported under the text box, never blocking. submit: function() { if (this._citoidPending()) { this._fillFromCitoid(); return; } this._doSubmit(); }, // Generates and submits the wikicode, then inserts the resulting HTML element if no errors occurred. _doSubmit: function() { var self = this; self._textInput.setDisabled(true); self._sourceInput.setDisabled(true); self._sourceURLInput.setDisabled(true); self._translationInput.setDisabled(true); self._transcriptionInput.setDisabled(true); self._sourceTypeDropdown.setDisabled(true); Object.keys(self._jederFieldWidgets).forEach(function(key) { self._jederFieldWidgets[key].setDisabled(true); }); // self._disableTranslationChk.setDisabled(true); this._applyButton.setDisabled(true); this._clearButton.setDisabled(true); this._loadingImage.toggle(true); var source = this._sourceInput.getValue().trim(); var sourceURL = this._sourceURLInput.getValue().trim(); var text = this._textInput.getValue().trim(); // The filled-in fields of the selected template. buildCitation() adds the language parameter and // the "Ç:" flag itself. var jederFields = {}; var usingCategoryTemplate = this._currentJederKey === CATEGORY_TEMPLATE_KEY && !!this._categoryTemplateName; if (usingCategoryTemplate) { Object.keys(self._jederFieldWidgets).forEach(function(key) { // A raw linebreak would break the single-line "#*" wikitext just like in the example text, // but these are plain param values (not display text), so collapse to a space, not <br>. var value = nlToSpace(self._jederFieldWidgets[key].getValue().trim()); if (value) { jederFields[key] = value; } }); } else if (this._currentJederKey && jederDefs[this._currentJederKey]) { jederDefs[this._currentJederKey].fields.forEach(function(field) { var widget = self._jederFieldWidgets[field.key]; var value = widget ? nlToSpace(widget.getValue().trim()) : ""; if (value) { jederFields[field.key] = value; } }); cite.withLinkAsUrl(this._currentJederKey, jederFields, sourceURL); } // A Kategorî:Çavkanî template is used as soon as it's picked, even with every field left blank // (still submittable as a bare jêgirtin=); a jêder-* type only counts once a field is actually filled. var usingJederTemplate = usingCategoryTemplate || (this._currentJederKey && Object.keys(jederFields).length > 0); var usingRawTemplate = this._currentJederKey === RAW_TEMPLATE_KEY; var minakAnNa, sablon; if (usingRawTemplate) { // A citation, so "#*" like the others -- the line itself is built below, not assembled here. minakAnNa = "*"; sablon = ""; } else if (usingJederTemplate) { // The jêder-*/Kategorî:Çavkanî template itself is used as the template, not as the "2=" content. minakAnNa = "*"; sablon = usingCategoryTemplate ? this._categoryTemplateName : this._currentJederKey; } else if (source || sourceURL) { minakAnNa = "*"; sablon = "jêder"; } else { minakAnNa = ":"; sablon = "nimûne"; } var listMarker = "".padStart(this._definitionLevel.length - 1, "#") + minakAnNa; var title = mw.config.get('wgTitle'); // One builder for every citation line here and in the wizard. Bolding the entry name, the language // parameter, the "Ç:" flag and <br> conversion all belong to it. var code = cite.buildCitation({ mode: usingRawTemplate ? "raw" : usingCategoryTemplate ? "category" : usingJederTemplate ? "jeder" : "plain", marker: listMarker, language: this._language, title: title, template: usingCategoryTemplate ? this._categoryTemplateName : this._currentJederKey, fields: jederFields, langParamKey: this._categoryTemplateLangParamKey, quote: text, source: source, sourceURL: sourceURL, translation: this._translationInput.getValue().trim(), transcription: this._transcriptionInput.getValue().trim(), // An edited line keeps its translation, and every parameter the form has no box for as long // as it is still the same kind of line. keepTranslation: !!this._editing, extra: this._editing && this._editing.key === this._currentJederKey ? this._editing.example.extra : [] }); // definitionLevel[0] is "type|ordinal": the section's position among same-type headings within its // language, resolved client-side, never MediaWiki's page-wide DOM id number. var sectionKeyParts = this._definitionLevel[0].split("|"); var sectionType = sectionKeyParts[0]; var sectionOrdinal = parseInt(sectionKeyParts[1], 10); if (this._editing) { // A citation written out by hand is saved exactly as typed, its list marker included. // Otherwise the rebuilt line keeps the page's own parameter order. var replacement = usingRawTemplate ? (/^#/.test(text) ? text : this._editing.original.match(/^#+[:*]/)[0] + " " + text) : cite.keepOrder(code, this._editing.original, this._editing.aliases); this._sendReplacement(replacement, "±nimûneya bikaranînê bi amûra « {0} » (v{1}).".format(NAME, VERSION), reenable, error); return; } // Read by the success handler below, which is a sibling of the transform, not nested in it. var emptyTemplate = false; // Thrown from inside an edit() transform to bail out with a specific message. var SubmitAbort = edit.util.SubmitAbort; // api.edit() fetches the revision it will conflict-check against and hands it here, so the // insertion runs on exactly that content. Named, so another gadget can run it instead. function buildEdit(revision) { var pageContent = revision.content; // Said here rather than left to the locator: which sections this tool does not work in is // this tool's own business, and the locator only knows that the key names no section. if (!sections.headingNamesFor(sectionType)) { throw new SubmitAbort('Navê beşê xelet e. Di beşên "Ji wêjêyê" û "Ji wêjêya klasîk" ev amûr nayê bikaranîn.'); } // loc.lines covers the rest of the page from this language's heading on: the saved page is rebuilt // from it below (see "text:"). var loc = wt.locateSection(pageContent, self._language, sectionType, sectionOrdinal); var langSectionIndex = loc.langSectionIndex; var lines = loc.lines; var targetLineIndex = wt.findDefinitionLine(lines, loc.posLineIndex, self._definitionLevel); // Where to insert inside this definition's "#:"/"#*" block. findBlockEnd() keeps multi-line // templates whole; exampleTemplateNames tells a real example from an annotation. var insertionFound = false; var lastRelevantBlockEnd = -1; var isJederFamily = (sablon === "jêder" || Object.prototype.hasOwnProperty.call(jederDefs, sablon)); var scanIndex = targetLineIndex + 1; // Read off the definition line, so a sub-sense's examples are matched at its own depth and // a deeper "#" line ends the block instead of being skipped past. var annotation = annotationRegex(lines[targetLineIndex]); var citation = citationRegex(lines[targetLineIndex]); for (; scanIndex < lines.length; scanIndex++) { var scanLine = lines[scanIndex]; if (!annotation.test(scanLine)) { // Left this definition's own block (a sub-sense, a new definition, a new section). break; } var blockEnd = findBlockEnd(lines, scanIndex); var templateName = getLineTemplateName(scanLine); var isExampleLine = templateName !== null && exampleTemplateNames.indexOf(templateName) !== -1; if (isJederFamily) { // A jêder-family example goes after every "#:"/"#*" line of this definition. lastRelevantBlockEnd = blockEnd; insertionFound = true; } else if (sablon === "nimûne") { // "nimûne" examples go before any "#*" line, but after "#:" lines. if (citation.test(scanLine)) { break; } lastRelevantBlockEnd = blockEnd; insertionFound = true; } scanIndex = blockEnd; // Skip past the whole (possibly multi-line) block } var insertAt = lastRelevantBlockEnd >= 0 ? lastRelevantBlockEnd + 1 : scanIndex; // After the relevant block, or straight after the definition when it had no "#:"/"#*" lines. lines.splice(insertAt, 0, code); // The "no example yet" marker goes once a real one lands. The whole "#:"/"#*" block is swept, not // just its last line: the marker is often followed by other annotations. var sweepFrom = targetLineIndex + 1; for (var sweep = sweepFrom; sweep < lines.length; sweep++) { if (!annotation.test(lines[sweep])) { break; } if (EMPTY_EXAMPLE_MARKER.test(lines[sweep])) { lines.splice(sweep, 1); emptyTemplate = true; // tells the DOM update below to replace the marker's <li> sweep--; } } return { text: pageContent.slice(0, langSectionIndex) + lines.join("\n"), summary: "+nimûneya bikaranînê bi amûra « {0} » (v{1}).".format(NAME, VERSION), }; } // DefSideBoxes takes the save over when its panel opened this form: the edit joins that gadget's // queue instead of publishing now. var handOver = wikt.addExamples.submitHandler; if (typeof handOver === "function") { try { handOver({ transform: buildEdit, // The line on its own, for a caller that has nowhere to splice it yet: a sense still // in someone's queue has no line number to be found by (DefSideBoxes' new-sense row). line: code, summary: "+nimûneya bikaranînê bi amûra « {0} » (v{1}).".format(NAME, VERSION), done: function() { self.setVisible(false); self.clear(); self._clearDraft(); reenable(); }, fail: function(message) { error(message || "Nikare li rêza guhartinan zêde bike."); } }); } catch (e) { error("Nikare li rêza guhartinan zêde bike: " + (e && e.message ? e.message : e)); } return; } api.edit(mw.config.get("wgPageName"), buildEdit).then(function() { api.parse(code).done(function(data) { var $renderedExample = $(data).find("ul > li").html(); var $item; // Insert rendered example into page if (emptyTemplate) { self._$lastExample.html($renderedExample); $item = self._$lastExample; } else { self._$lastExample.after($item = $("<li>").append($renderedExample)); } $item.css("background-color", "lightgreen"); setTimeout(function() { $item.css("background-color", "inherit"); }, 1000); self._$lastExample = $item; }); self.setVisible(false); self.clear(); self._clearDraft(); reenable(); }, function(codeOrAbort, result) { if (codeOrAbort instanceof SubmitAbort) { error(codeOrAbort.message); } else { error(msg.saveFailed + apiErrorMessage(codeOrAbort, result)); } }); function error(errorMessage) { mw.notify(errorMessage || "Nimûne nehat weşandin, îhtimalen rûpel hate guhartin. Ji kerema xwe re rûpelê dîsa bar bike û dîsa biceribîne."); // A draft is kept for a new example only: an edited one would come back on the next new one. if (!self._editing) { draftStorage.Set({ text: self._textInput.getValue(), source: self._sourceInput.getValue(), sourceURL: self._sourceURLInput.getValue(), translation: self._translationInput.getValue(), transcription: self._transcriptionInput.getValue(), }); } reenable(); } function reenable() { self._textInput.setDisabled(false); self._sourceInput.setDisabled(false); self._sourceURLInput.setDisabled(false); self._translationInput.setDisabled(false); self._transcriptionInput.setDisabled(false); self._sourceTypeDropdown.setDisabled(false); Object.keys(self._jederFieldWidgets).forEach(function(key) { self._jederFieldWidgets[key].setDisabled(false); }); // self._disableTranslationChk.setDisabled(false); self._applyButton.setDisabled(false); self._cancelButton.setDisabled(false); self._clearButton.setDisabled(false); self._loadingImage.toggle(false); } }, }; }); // </nowiki> 5lp6bg8x1kcbfpwypq0jybii6uqgunr Gotûbêj:şilêr 1 1356961 6903099 6232894 2026-11-10T20:34:00Z Ghybu 1393 /* Fritillaria kurdica an Fritillaria imperyalis ? */ format 6903099 wikitext text/x-wiki == Fritillaria kurdica an Fritillaria imperyalis ? == [[Wêne:Nature of Buin va Miandasht County.jpg|thumb|''Fritillaria imperyalis'']] [[Wêne:Fritillaria kurdica.jpg|thumb|''Fritillaria kurdica'']] {{silav|Dûrzan}}. Şilêr an gulderxwîn navê kîjan gul e: ''Fritillaria imperyalis'' an ''Fritillaria kurdica'' ? [[Bikarhêner:Ghybu|Ghybu]] ([[Gotûbêja bikarhêner:Ghybu|gotûbêj]]) 01:34, 26 nîsan 2026 (UTC) : Merheba, bi esasa xwe navê imperialis e. Lê bi a min be ji bo gişê fritillaria (stûxwar) mirov dikare weke navê cins bikarbîne. Mînak: Şilêr/gulderxwîna(stûxwarka) kurdan (fritillaria kurdica)--{{bêîmze|Dûrzan|11:00, 26 nîsan 2026}} g9ypp0l81ypkdil4bwdivtj85g3uhcj MediaWiki:Gadget-citoid-helper.js 8 1358211 6903102 6414340 2026-11-10T21:26:18Z Wikihez 7344 Split the wizard's script pages under 100 KB; identifierOf shared from citoid-helper 6903102 javascript text/javascript // <nowiki> /** * Gadget-citoid-helper.js * * Amûreke muşterek ji bo standina daneyên çavkaniyê ji Citoid û * veguherandina wan bo qadên şablonên jêder-* (jêder-kitêb, jêder-kovar, jêder-websîte, jêder-vîdeo). * * BIKARANÎN JI SCRIPTEKE DIN: * * Vê rûpelê wek dependency lê zêde bike di eyarên gadgeta xwe de li ser * [[Special:Gadgets]] (an jî MediaWiki:Gadgets-definition), paşê: * * mw.loader.using("ext.gadget.citoid-helper").then(function () { * wikt.citoid.fetchJederFields(url).then(function (result) { * console.log(result.jederKey, result.fields); * }); * }); * * API ya `window.wikt.citoid`: * - getRestUrl() → {string} URLya REST a Citoid a vê wîkiyê * - yearFromDate(dateStr) → {string} sala (4 reqem) ji rêzikeke tarîxê * - formatAuthors(authors) → {string} navên nivîskaran wek rêzikek yekane * - typeToJederKey(itemType) → {string} "jêder-kitêb" / "jêder-kovar" / "jêder-websîte" / "jêder-vîdeo" * - dataToJederFields(jederKey, data, originalUrl) → {Object<string,string>} merheleyên şablona jêder-* * - fetchData(query) → {jQuery.Promise<Object>} daneya xam a Citoid * - fetchJederFields(query) → {jQuery.Promise<{jederKey, fields, raw}>} hêsankirî * - extractVideoTimestamp(urlStr) → {string} a video URL's "t=" start time, as "M:SS"/"H:MM:SS" * - isYouTubeUrl(urlStr) → {boolean} whether the URL is a youtube.com/youtu.be link * - websiteNameForUrl(urlStr) → {string} jêder-websîte "websîte" value: known-site wikilink, else the bare domain * - TYPE_TO_JEDER → {Object} xerîteya itemType → jederKey */ window.wikt = window.wikt || {}; (function () { "use strict"; // Heke jixwe hatibe barkirin, dubare neke. if (window.wikt.citoid) { return; } // Xerîteya cureyên Citoid (itemType) li ser şablonên me yên jêder-* var CITOID_TYPE_TO_JEDER = { "book": "jêder-kitêb", "bookSection": "jêder-kitêb", "thesis": "jêder-kitêb", "report": "jêder-kitêb", "manuscript": "jêder-kitêb", "journalArticle": "jêder-kovar", "magazineArticle": "jêder-kovar", "newspaperArticle": "jêder-kovar", "webpage": "jêder-websîte", "blogPost": "jêder-websîte", "forumPost": "jêder-websîte", "videoRecording": "jêder-vîdeo", "podcast": "jêder-websîte", "encyclopediaArticle": "jêder-websîte" }; /** * Ji mantiqa [[:en:w:User:BrandonXLF/Citoid.js]] * Vedigere URLya REST a Citoid, li gorî eyarên vê wîkiyê. * @return {string} */ function getRestUrl() { var cfg = mw.config.get("wgCitoidConfig"); var vecfg = mw.config.get("wgVisualEditorConfig"); return (cfg && (cfg.fullRestbaseUrl || cfg.wbFullRestbaseUrl)) ? (cfg.fullRestbaseUrl || cfg.wbFullRestbaseUrl) + "v1/data/citation/mediawiki/" : (vecfg && vecfg.fullRestbaseUrl) ? vecfg.fullRestbaseUrl + "v1/data/citation/mediawiki/" : (cfg && cfg.citoidServiceUrl) ? cfg.citoidServiceUrl + "/mediawiki/" : "https://ku.wiktionary.org/api/rest_v1/data/citation/mediawiki/"; } /** * Ji tarîxa Citoid (rêzik an tarîxeke ISO) sala wê (4 reqem) derdixe. * @param dateStr {string} * @return {string} */ function yearFromDate(dateStr) { if (!dateStr) { return ""; } var match = /\d{4}/.exec(dateStr); return match ? match[0] : ""; } /** * Navên nivîskaran ên Citoid (rêzek [pêşnav, paşnav]) dike rêzikek yekane. * @param authors {Array<Array<string>>} * @return {string} */ function formatAuthors(authors) { if (!Array.isArray(authors)) { return ""; } return authors.map(function (pair) { return (pair || []).filter(function (part) { return !!part; }).join(" "); }).filter(function (name) { return !!name; }).join("; "); } function formatEditors(editors) { if (!Array.isArray(editors)) { return ""; } return editors.map(function (pair) { return (pair || []).filter(function (part) { return !!part; }).join(" "); }).filter(function (name) { return !!name; }).join("; "); } /** * Li gorî itemType a Citoid, navê şablona jêder-* a munasib vedigerîne. * @param itemType {string} * @return {string} Yek ji "jêder-kitêb", "jêder-kovar", "jêder-websîte" an "jêder-vîdeo". */ function typeToJederKey(itemType) { return CITOID_TYPE_TO_JEDER[itemType] || "jêder-websîte"; } /** * Whether a URL is a youtube.com/youtu.be link -- used to force jêder-vîdeo regardless of * Citoid's own itemType guess, and to pick the "w:Youtube" platform value. * @param urlStr {string} * @return {boolean} */ function isYouTubeUrl(urlStr) { return /^https?:\/\/(www\.|m\.)?(youtube\.com|youtu\.be)\//i.test(urlStr || ""); } // Known domains mapped to a nicer jêder-websîte "websîte" value (a wikilink where a Wikipedia article exists). var KNOWN_WEBSITE_NAMES = { "x.com": "w:Twitter", "twitter.com": "w:Twitter", "Rudaw": "w:Rûdaw", "rudaw.net": "w:Rûdaw" }; /** * Bare domain of a URL (protocol and "www." stripped), e.g. "https://www.example.com/x" -> "example.com". * @param urlStr {string} * @return {string} Lowercased domain, or an empty string if urlStr isn't an absolute http(s) URL. */ function bareDomainFromUrl(urlStr) { var match = /^https?:\/\/(?:www\.)?([^\/]+)/i.exec(urlStr || ""); return match ? match[1].toLowerCase() : ""; } /** * A jêder-websîte "websîte" value for a URL: a known-site wikilink (KNOWN_WEBSITE_NAMES) if the domain * matches one, else the bare domain itself as a plain fallback name. * @param urlStr {string} * @return {string} The site name, or an empty string if urlStr isn't an absolute http(s) URL. */ function websiteNameForUrl(urlStr) { var host = bareDomainFromUrl(urlStr); return host ? (KNOWN_WEBSITE_NAMES[host] || host) : ""; } /** * Extracts a video's start time from its URL's "t=" param (YouTube-style: plain seconds like "137", * or "2m17s"/"1h2m3s"). Reads the caller-supplied URL directly, since Citoid's own resolved data.url * usually strips this param. * @param urlStr {string} * @return {string} "M:SS" or "H:MM:SS", or an empty string if absent/unrecognized. */ function extractVideoTimestamp(urlStr) { if (!urlStr) { return ""; } var match = /[?&]t=([^&]+)/.exec(urlStr); if (!match) { return ""; } var raw = decodeURIComponent(match[1]); var seconds; if (/^\d+s?$/.test(raw)) { seconds = parseInt(raw, 10); } else { var parts = /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/.exec(raw); if (!parts || (!parts[1] && !parts[2] && !parts[3])) { return ""; } seconds = (parseInt(parts[1] || "0", 10) * 3600) + (parseInt(parts[2] || "0", 10) * 60) + parseInt(parts[3] || "0", 10); } var h = Math.floor(seconds / 3600); var m = Math.floor((seconds % 3600) / 60); var s = seconds % 60; var ss = s < 10 ? "0" + s : String(s); return h > 0 ? (h + ":" + (m < 10 ? "0" + m : m) + ":" + ss) : (m + ":" + ss); } /** * Daneyên Citoid vediguherîne tabloyeke parametreyên şablona jêder-* (li gorî cureyê wê). * @param jederKey {string} Yek ji "jêder-kitêb", "jêder-kovar", "jêder-websîte" an "jêder-vîdeo". * @param data {Object} Netîceya Citoid. * @param originalUrl {string} [optional] The URL/DOI/ISBN as originally typed/fetched -- preferred over * data.url for the "url" field when it's an absolute http(s) URL, since Citoid's own resolved data.url * strips query params (e.g. a video's "t=" timestamp) that the original URL still has. * @return {Object<string, string>} */ function dataToJederFields(jederKey, data, originalUrl) { var out = {}; var author = formatAuthors(data.author); var editor = formatEditors(data.editor); var url = (originalUrl && /^https?:\/\//i.test(originalUrl)) ? originalUrl : data.url; if (author) { out["nivîskar"] = author; } if (editor) { out["amadekar"] = editor; } if (data.title) { out["sernav"] = data.title; } if (url) { out.url = url; } if (jederKey === "jêder-kitêb") { if (data.publisher) { out["weşanxane"] = data.publisher; } if (data.place) { out.cih = data.place; } if (data.date) { out.sal = yearFromDate(data.date); } if (data.ISBN) { out.isbn = Array.isArray(data.ISBN) ? data.ISBN[0] : data.ISBN; } if (data.edition) { out["çap"] = data.edition; } if (data.pages) { out["rûpel"] = data.pages; } } else if (jederKey === "jêder-kovar") { if (data.publicationTitle) { out.kovar = data.publicationTitle; } if (data.publisher) { out["weşanxane"] = data.publisher; } if (data.volume) { out.cild = data.volume; } if (data.issue) { out.hejmar = data.issue; } if (data.pages) { out["rûpel"] = data.pages; } if (data.date) { out.sal = yearFromDate(data.date); } } else if (jederKey === "jêder-websîte") { var websiteName = data.websiteTitle || websiteNameForUrl(url); if (websiteName) { out["websîte"] = websiteName; } if (data.date) { out["tarîx"] = data.date; } } else if (jederKey === "jêder-vîdeo") { delete out["nivîskar"]; // "hunermend" is jêder-vîdeo's own performer/artist field if (author) { out.hunermend = author; } var platform = isYouTubeUrl(url) ? "w:Youtube" : (data.libraryCatalog || "").toLowerCase(); if (platform) { out.platform = platform; } // tarîx and sal are mutually exclusive -- prefer the full date, fall back to year-only otherwise. if (data.date) { out.tarîx = data.date; } else { var year = yearFromDate(data.date); if (year) { out.sal = year; } } var dem = extractVideoTimestamp(url); if (dem) { out.dem = dem; } } return out; } /** * Ji Citoid daneyên çavkaniyekê tîne (li gorî URL, DOI an ISBNyê). * @param query {string} * @return {jQuery.Promise} Bi daneya ewil a Citoid (Object) tê safîkirin. */ function fetchData(query) { var url; try { url = encodeURIComponent(decodeURIComponent(query)); } catch (e) { url = encodeURIComponent(query); } return $.get(getRestUrl() + url).then(function (res) { if (!res || !res[0]) { return $.Deferred().reject("Netîceya ji Citoidê vala ye.").promise(); } return res[0]; }); } /** * Fonksiyoneke hêsankirî: ji URL/DOI/ISBNyekê rasterast şablona jêder-* a munasib * û merheleyên wê yên tijîkirî vedigerîne (fetchData + typeToJederKey + dataToJederFields * bi hev re). * @param query {string} * @return {jQuery.Promise} Bi {jederKey: string, fields: Object<string,string>, raw: Object} tê safîkirin. */ function fetchJederFields(query) { return fetchData(query).then(function (data) { // YouTube links always use jêder-vîdeo, regardless of what itemType Citoid guessed. var jederKey = (isYouTubeUrl(query) || isYouTubeUrl(data.url)) ? "jêder-vîdeo" : typeToJederKey(data.itemType); return { jederKey: jederKey, fields: dataToJederFields(jederKey, data, query), raw: data }; }); } // The template and field an ISBN or DOI belongs in, or null for anything else. function identifierOf(query) { var isbn = query.replace(/^isbn:?\s*/i, ""); if (/^(\d{9}[\dX]|97[89]\d{10})$/i.test(isbn.replace(/[-\s]/g, ""))) { return { jederKey: "jêder-kitêb", field: "isbn", value: isbn }; } var doi = /^(?:https?:\/\/(?:dx\.)?doi\.org\/|doi:\s*)?(10\.\d{4,9}\/\S+)$/i.exec(query); return doi ? { jederKey: "jêder-kovar", field: "doi", value: doi[1] } : null; } window.wikt.citoid = { identifierOf: identifierOf, getRestUrl: getRestUrl, yearFromDate: yearFromDate, formatAuthors: formatAuthors, typeToJederKey: typeToJederKey, dataToJederFields: dataToJederFields, fetchData: fetchData, fetchJederFields: fetchJederFields, extractVideoTimestamp: extractVideoTimestamp, isYouTubeUrl: isYouTubeUrl, websiteNameForUrl: websiteNameForUrl, TYPE_TO_JEDER: CITOID_TYPE_TO_JEDER }; })(); // </nowiki> p9iya9qzf8cdvydgnxn3h3cn3i9o5gm MediaWiki:Gadget-wikt.dsb.js 8 1359171 6903107 6865759 2026-11-10T21:29:15Z Wikihez 7344 Script pages under 100 KB: part of the code moved to a second file; shared code in one place 6903107 javascript text/javascript // <nowiki> /* * Page: [[MediaWiki:Gadget-wikt.dsb.js]] * * The part of DefSideBoxes that runs on a page view: finds the definition lines and hangs a pencil on * each. Forms, dialogs and saves are in wikt.dsb-panels, fetched on the first click (never on hover). * * The "Nimûne/jêgirtin" item drives add-examples, which wikt.dsb-panels depends on -- so it is loaded by * the time any panel exists, and never on a plain page view. */ (function () { "use strict"; var core = require("ext.gadget.wikt.core"); var sections = core.sections; var MANE_SECTION = sections.MANE_SECTION; var PAGE_SUBSECTIONS = sections.PAGE_SUBSECTIONS; var msg = core.msg; var headingOuter = sections.headingOuter; var headingDepth = sections.headingDepth; var headingText = sections.headingText; var isManeHeading = sections.isManeHeading; var sectionTypeOf = sections.sectionTypeOf; var sectionOrdinalWithinLanguage = sections.sectionOrdinalWithinLanguage; var headingTextOrdinalWithinLanguage = sections.headingTextOrdinalWithinLanguage; var languageCodeForOl = sections.languageCodeForOl; var eachLeafDefinition = sections.eachLeafDefinition; if (mw.config.get("wgAction") !== "view") { return; } // An entry, a reconstruction, or a sandbox -- see sections.isEntryPage(). if (!sections.isEntryPage()) { return; } if (/&printable=yes|&diff=|&oldid=/.test(window.location.search)) { return; } window.wikt = window.wikt || {}; if (window.wikt.defSideBoxesLoaded) { return; } window.wikt.defSideBoxesLoaded = true; // A save from the queue box reloads with core's post-edit flag set (see dsb-panels.js). Core loads the // module that reads the flag only after a form save, so it is loaded here; it shows the toast and clears the flag. if (mw.storage.session.get("mw-PostEdit" + mw.config.get("wgPageName"))) { mw.loader.load("mediawiki.action.view.postEdit"); } // The panels never land on a page view: each module is fetched once, on the first click that needs it // -- never on hover -- and every later call reuses the same promise. var panelsApi = null; // A panel is built once and merely shown again after that, so the reveal cannot live in the builders. // Only click handlers call this -- hover must never pull the panels module. function revealQueue() { withPanels(function (panels) { panels.openQueueBox(); }); } // Each kind of panel is a module of its own, fetched the first time its kind of pencil is clicked; all of // them build on wikt.dsb-panels, which carries the queue and everything they share. var PANEL_MODULES = { sense: "ext.gadget.wikt.dsb-sense", subsection: "ext.gadget.wikt.dsb-subsection", pos: "ext.gadget.wikt.dsb-pos" }; var panelsPromises = {}; // fn gets wikt.dsb-panels' exports, plus those of the module for part (sense, subsection, pos) if given. function withPanels(part, fn) { if (typeof part === "function") { fn = part; part = null; } var name = PANEL_MODULES[part] || "ext.gadget.wikt.dsb-panels"; if (!panelsPromises[name]) { panelsPromises[name] = mw.loader.using(name).then(function (require) { panelsApi = require("ext.gadget.wikt.dsb-panels"); return name === "ext.gadget.wikt.dsb-panels" ? panelsApi : $.extend({}, panelsApi, require(name)); }); } panelsPromises[name].then(fn, function () { mw.notify("Nikare amûrên guhartinê bar bike.", { type: "error" }); }); } // The element holding an example's last line of text: a quotation's source line and its quote are // blocks, so a pencil after them would sit on a line of its own. function lastTextLine(el) { var last = el.lastElementChild; while (last && /^(DIV|DL|DD|UL|OL|LI)$/.test(last.tagName)) { el = last; last = el.lastElementChild; } return el; } // A pencil on every example and quotation under a sense, opening add-examples' form on that one line. function attachExampleEditors($li, language, sectionType, sectionOrdinal, definitionLevel, $h3) { // A queued change redraws the sense (dsb-panels.js raises this); its examples get their pencils again. if (!$li.data("dsbExampleRedraw")) { $li.data("dsbExampleRedraw", true); $li.on("dsbSenseRedrawn", function () { attachExampleEditors($li, language, sectionType, sectionOrdinal, definitionLevel, $h3); }); } sections.exampleItemsOf($li).each(function () { var item = this; if ($(item).hasClass("dsb-has-example-toolbar")) { return; } var $toggle = $("<a href='#' class='dsb-toggle'></a>").attr("title", "Nimûneyê biguhêre"); var $toolbar = $("<span class='dsb-example-toolbar'></span>").append($toggle); $toggle.on("click", function (e) { e.preventDefault(); e.stopPropagation(); withPanels("sense", function (panels) { panels.editExample($li, $h3, language, sectionType, sectionOrdinal, definitionLevel, item); }); }); // Touch has no hover: a tap on the example shows its pencil, as a tap on a sense does. $(lastTextLine(item)).append($toolbar); $(item).addClass("dsb-has-example-toolbar").on("click.dsbReveal", function () { $toolbar.addClass("dsb-revealed"); }); }); } // How many rows the pencil is currently holding open. A pinned row means somebody is part-way through // an edit, and opening another one under the pointer moves the page: the content below shifts, the // pointer ends up over something else, that fires its own hover, and the page jumps back and forth. // So while anything is pinned, hovering opens nothing. var pinnedRows = 0; var canHover = !window.matchMedia || window.matchMedia("(hover: hover)").matches; // The first pencil clicked shows every pencil on the page, so any other part is one tap away. Capture // phase, as each pencil's own handler stops the click there. document.addEventListener("click", function (e) { if (e.target.closest && e.target.closest(".dsb-toggle")) { document.body.classList.add("dsb-pencils-shown"); } }, true); function attachToolbar($li, language, sectionType, sectionOrdinal, definitionLevel, $h3, isLastSense) { if ($li.children(".dsb-toolbar").length) { return; } var $toolbar = $("<span class='dsb-toolbar'></span>"); // The real InterlanguageLinks pencil icon (same asset defEditButton uses); image swap is in the CSS. var $toggle = $("<a href='#' class='dsb-toggle' title='Guhartina pêşketî'></a>"); // Revealed by $toggle -- an indented panel under the sense, not always visible. Built lazily on // first open rather than eagerly for every sense on the page. var $quickRow = $("<div class='dsb-quick-row'></div>").hide(); var pinned = false; // the pencil pins the row open; hovering alone does not // Builds the row -- fetching the panels module the first time -- then calls back. Asynchronous now, // so the callback re-checks that the row is still wanted rather than showing it unconditionally. function ensureBuilt(then) { if ($quickRow.data("built")) { then(); return; } withPanels("sense", function (panels) { if (!$quickRow.data("built")) { panels.buildAdvancedPanel($quickRow, $li, $h3, language, sectionType, sectionOrdinal, definitionLevel, isLastSense); $quickRow.data("built", true); } then(); }); } // Anything the user has started here: an open field box, the open dropdown, or focus inside the row. function engaged() { return pinned || $quickRow.find(".dsb-open-boxes").children().length > 0 || $quickRow.find(".dsb-more-menu").is(":visible") || $.contains($quickRow.get(0), document.activeElement); } function setPinned(next) { if (pinned === next) { return; } pinned = next; pinnedRows += next ? 1 : -1; } $toggle.on("click", function (e) { e.preventDefault(); // Through their own cancel handler rather than a bare hide(), so a row this closes also drops // its pinned state -- hiding it alone would leave pinnedRows counting a row nobody can see. $(".dsb-quick-row").not($quickRow).trigger("dsbCancel"); setPinned(!pinned); if (pinned) { ensureBuilt(function () { if (pinned) { $quickRow.show(); } }); } else { $quickRow.hide(); } }); // Hover reveals the row and takes it away again, the way en.wikt's does -- but never while something // on it is in use, or a half-typed box would vanish on the way to the mouse leaving. $li.on("mouseenter.dsbHover", function () { // iOS plays a tap as mouseenter first, and a row shown here would swallow that tap's click. if (!canHover) { return; } // A row the pencil is holding open is already open, and no row opens on hover while another // one is: that is the case where the page is being edited and must stay still. if (pinned || pinnedRows) { return; } // Hover reveals a row that already exists; it never fetches the panels module. Otherwise // simply moving the pointer across an entry would pull ~100 KB on every page view, which is // the whole cost the split exists to avoid. The pencil is what asks for it. if ($quickRow.data("built")) { $quickRow.show(); } }); // Raised by the panel's "Betal bike" (see dsb-panels.js): clear the pinned state as well as // hiding, so one click on the pencil opens it again. $quickRow.on("dsbCancel", function () { setPinned(false); $quickRow.hide(); }); $li.on("mouseleave.dsbHover", function () { if (!engaged()) { $quickRow.hide(); } }); $toolbar.append($toggle); // The class positions the <li>, so the toolbar's gutter slot is measured from the line itself and // not from wherever it happens to sit in the child order -- ShowIDs prepends its id pill to this // same <li>. $li.addClass("dsb-def").prepend($toolbar); // Touch has no :hover -- a tap anywhere on the line reveals the pencil the same way hovering does on // desktop (CSS gates this to touch devices; harmless if it also fires on a mouse click). $li.on("click.dsbReveal", function () { $toolbar.addClass("dsb-revealed"); }); // Land $quickRow right after the sense's own gloss text -- before any existing "Hevmane: ..." <dl>, // citation <ul>, or the Nimûne/jêgirtin proxy link's own <ul>, not appended after all of that. var $firstBlock = $li.children("dl, ul, ol").first(); if ($firstBlock.length) { $firstBlock.before($quickRow); } else { $li.append($quickRow); } } // A heading's pencil: its toolbar, the pencil itself and the hidden panel it opens. function headingPencil(title, panelClass) { var $toggle = $("<a href='#' class='dsb-toggle'></a>").attr("title", title); return { $toolbar: $("<span class='dsb-toolbar dsb-toolbar-heading'></span>").append($toggle), $toggle: $toggle, $panel: $("<div></div>").addClass(panelClass || "dsb-subsection-panel").hide() }; } // Fills $panel the first time it is opened: build gets the exports of the module for part. function buildOnce($panel, part, build) { if ($panel.data("built")) { return; } $panel.data("built", true); $panel.text(msg.loading); withPanels(part, function (panels) { $panel.empty(); build(panels); }); } // A section the data marks hidden is left alone. function isHiddenSection(header) { return PAGE_SUBSECTIONS.some(function (s) { return s.hidden && s.header === header; }); } // The translation section, whose pencil opens the translation adder on its boxes. function isWergerSection(header) { return PAGE_SUBSECTIONS.some(function (s) { return s.kind === "werger" && s.header === header; }); } // The heading itself inside a skin's wrapper div, or the heading when there is no wrapper. function innerHeading($el) { return $el.is("h2, h3, h4, h5, h6") ? $el : $el.find("h2, h3, h4, h5, h6").first(); } // "===" sections that sit beside the parts of speech: a Werger after one belongs above it, not under it. var BESIDE_POS = ["Bilêvkirin", "Etîmolojî", "Çavkanî"]; // Why the translation adder cannot reach a Werger, or null when it can. {$heading, pos} when a heading // at part-of-speech level comes between it and its part of speech ("=== Jê ===" after "=== Mane ==="), // or when the Werger itself is not at h4; $heading is what belongs at h4. {beside, pos} when it sits // under a section like "=== Çavkanî ===" and has to move above it instead. function wergerBlocker($h) { var depth = headingDepth(headingOuter($h)); var $top = depth === 3 ? $h : null; for (var $c = headingOuter($h).prev(); $c.length; $c = $c.prev()) { var d = headingDepth($c); if (!d || d > 3) { continue; } if (d < 3) { return null; } var $hh = innerHeading($c); var isPos = !!sections.sectionKeyOf($hh); if (!$top) { if (isPos) { return depth === 4 ? null : { $heading: $h, pos: headingText($hh) }; } $top = $hh; } else if (isPos) { var name = headingText($top); return BESIDE_POS.indexOf(name) !== -1 ? { beside: name, pos: headingText($hh) } : { $heading: $top, pos: headingText($hh) }; } } return null; } // Says why a Werger's panel has no translation adder, with a button that queues the fix when a // heading's depth is all that is wrong. function wergerBlockerNotice(language, blocker) { var $notice = $("<div class='dsb-werger-blocked'></div>"); if (blocker.beside) { return $notice.text("Amûra tercimeyan li vir naxebite: ev beş di bin «" + blocker.beside + "» de ye. Wê bibe ser «" + blocker.beside + "», di bin «" + blocker.pos + "» de, û rûpelê nû bike."); } var name = headingText(blocker.$heading); var from = headingDepth(headingOuter(blocker.$heading)); $notice.text("Amûra tercimeyan li vir naxebite: «" + name + "» bi h" + from + " hatiye nivîsandin, " + "loma ev beş ne rasterast di bin «" + blocker.pos + "» de ye. «" + name + "» bike h4, qeyd bike û rûpelê nû bike."); var $fix = $("<button type='button' class='dsb-werger-blocked-fix'></button>").text("«" + name + "» bike h4"); $fix.on("click", function (e) { e.preventDefault(); withPanels(function (panels) { panels.setDepth(language, blocker.$heading, 4); }); revealQueue(); $fix.prop("disabled", true); }); return $notice.append(" ", $fix); } // Everything between a heading and the next heading at its level or above. function sectionBody($heading) { var depth = headingDepth($heading); var $body = $(); for (var $cursor = $heading.next(); $cursor.length; $cursor = $cursor.next()) { var d = headingDepth($cursor); if (d && d <= depth) { break; } $body = $body.add($cursor); } return $body; } // An editor for the translation adder that fetches the panels only when a change is first made, so a // page view with translation boxes does not pull them. function lazyTranslationEditor($h3, language, sectionType) { var real = null; function withEditor(fn) { withPanels(function (panels) { real = real || panels.translationEditor($h3, language, sectionType); panels.openQueueBox(); fn(real); }); } return { withCurrentText: function (callback) { withEditor(function (ed) { ed.withCurrentText(callback); }); }, addEdit: function (edit, node) { withEditor(function (ed) { ed.addEdit(edit, node); }); }, error: function (message) { mw.notify(message, { type: "error" }); }, undo: function () { withEditor(function (ed) { ed.undo(); }); } }; } // Under "==== Werger ====" every box gets the form for a new translation and a ± on its heading, from // [[MediaWiki:Gadget-wikt.translation-adder.js]]. The heading's pencil opens the boxes' own form (sense, // note, id of each box, and more boxes) and puts ✎ and × on the existing lines; "+ Qutiyeke din zêde // bike" under the boxes opens that form on a new box. All their changes join the save queue. False // when the section has no box the adder reads. function attachWergerEditor($hh, $heading, $h3, header, language, sectionType, sectionOrdinal) { var $body = sectionBody($heading); if (!$body.find("table.translations").length) { return false; } var editor = lazyTranslationEditor($h3, language, sectionType); var adder = mw.loader.using("ext.gadget.wikt.translation-adder").then(function (require) { var api = require("ext.gadget.wikt.translation-adder"); return api.attach($body, editor).then(function () { return api; }); }); var pencil = headingPencil(header + " biguhêre"); var $toolbar = pencil.$toolbar; var $toggle = pencil.$toggle; var $panel = pencil.$panel; placeHeadingToolbar($hh, $toolbar); revealHeadingToolbarOnTap($hh, $toolbar); $heading.after($panel); function lineEditing(open) { if (open) { // Collapsed boxes are opened, so their lines are in sight. $body.find(".NavFrame").each(function () { if ($(this).children(".NavContent").is(":hidden")) { $(this).find(".NavToggle").first().trigger("click"); } }); } adder.then(function (api) { api.setLineEditing($body, editor, open); }, function () { mw.notify("Nikare amûra tercimeyan bar bike.", { type: "error" }); }); } function showPanel() { $panel.show(); $toolbar.addClass("dsb-revealed dsb-werger-open"); buildOnce($panel, "subsection", function (panels) { panels.buildSubsectionPanel($panel, $h3, language, sectionType, sectionOrdinal, header); }); revealQueue(); } $toggle.on("click", function (e) { e.preventDefault(); var open = !$panel.is(":visible"); if (open) { showPanel(); } else { $panel.hide(); $toolbar.removeClass("dsb-revealed dsb-werger-open"); } lineEditing(open); }); // The panel's "Betal bike" closes it the way the pencil does. $panel.on("dsbPanelClosed", function () { $toolbar.removeClass("dsb-revealed dsb-werger-open"); lineEditing(false); }); var $add = $("<a href='#' class='dsb-quick-link'>+ Qutiyeke din zêde bike</a>").on("click", function (e) { e.preventDefault(); var addBox = $panel.data("dsbAddBox"); if ($panel.is(":visible") && addBox) { addBox(); return; } $panel.data("dsbAddBoxPending", true); showPanel(); // A form built before this click has its boxes already; the new one goes in now. if (addBox) { $panel.removeData("dsbAddBoxPending"); addBox(); } }); $("<div class='dsb-werger-add'></div>").append($add).insertAfter($body.last()); return true; } // Every subsection under a part of speech, whether or not the data knows the header: one it does not // know opens as wikitext, or as a word list when that is what it holds -- see buildSubsectionPanel. function attachSubsectionEditors($h3, language, sectionType, sectionOrdinal, posDepth) { posDepth = posDepth || 3; for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) { var depth = headingDepth($cursor); if (depth && depth <= posDepth) { break; } if (depth !== posDepth + 1) { continue; } var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first(); var header = headingText($hh); if (!header || $hh.data("dsbAttached")) { continue; } if (isWergerSection(header)) { $hh.data("dsbAttached", true); // A Werger heading with no box gets the section pencil, whose form adds one; old-style // boxes the translation adder cannot read are left alone. if (attachWergerEditor($hh, $cursor, $h3, header, language, sectionType, sectionOrdinal) || sectionBody($cursor).find(".NavFrame").length) { continue; } attachSubsectionEditor($hh, $cursor, $h3, header, language, sectionType, sectionOrdinal); continue; } if (isHiddenSection(header)) { continue; } $hh.data("dsbAttached", true); attachSubsectionEditor($hh, $cursor, $h3, header, language, sectionType, sectionOrdinal); } } // Where the pencil goes depends on what the skin does with its own edit links. Desktop skins put // "[biguhêre]" right after the heading text, so the pencil follows it and the two read as one group. // Minerva parks its edit icon at the far right of the heading row instead, a screen-width away from the // text, so there the pencil goes inside the heading rather than chasing it. function placeHeadingToolbar($heading, $toolbar) { if (mw.config.get("skin") !== "minerva") { var $editsection = headingOuter($heading).find(".mw-editsection").first(); if ($editsection.length) { $editsection.after($toolbar); return; } } $heading.append($toolbar); } // Touch has no hover, so a heading's pencil is shown by tapping the heading itself. Taps that mean // something else are left alone: the skin's own edit links, and the pencil once it is showing. function revealHeadingToolbarOnTap($heading, $toolbar) { headingOuter($heading).on("click", function (e) { if ($(e.target).closest("a, .dsb-toolbar").length) { return; } $toolbar.toggleClass("dsb-revealed"); }); } // One pencil's worth of behaviour, shared by the "====" headings and by an image's own caption: the // panel is built the first time it is opened, never before, and the pencil stays visible while it is. // free, when given, pins the panel to a heading located by its own text rather than through a part of // speech: {header, ordinal}. function bindPanelToggle($toggle, $toolbar, $panel, $h3, header, language, sectionType, sectionOrdinal, free) { $toggle.on("click", function (e) { e.preventDefault(); $panel.toggle(); // Stays put while its panel is open, so it does not vanish from under the pointer. $toolbar.toggleClass("dsb-revealed", $panel.is(":visible")); if (!$panel.is(":visible")) { return; } buildOnce($panel, "subsection", function (panels) { panels.buildSubsectionPanel($panel, $h3, language, sectionType, sectionOrdinal, header, free); if (free && free.blocker) { $panel.prepend(wergerBlockerNotice(language, free.blocker)); } }); revealQueue(); }); $panel.on("dsbPanelClosed", function () { $toolbar.removeClass("dsb-revealed"); }); } function attachSubsectionEditor($hh, $heading, $h3, header, language, sectionType, sectionOrdinal) { var pencil = headingPencil(header + " biguhêre"); var $toolbar = pencil.$toolbar; var $toggle = pencil.$toggle; var $panel = pencil.$panel; placeHeadingToolbar($hh, $toolbar); revealHeadingToolbarOnTap($hh, $toolbar); $heading.after($panel); bindPanelToggle($toggle, $toolbar, $panel, $h3, header, language, sectionType, sectionOrdinal); } // The "place" subsection that owns the thumbnails under a part-of-speech heading -- named in the data, // not spelled out here, so renaming it there renames it everywhere. var IMAGE_HEADER = (function () { var found = null; PAGE_SUBSECTIONS.forEach(function (s) { if (s.place === "pos-top") { found = s.header; } }); return found; }()); // A pencil on the image's own caption. A wrong caption is noticed while looking at the picture, and // this is where the writer reaches for it -- the same editor the section's own list offers, opened on // the image that is already there. // Every thumbnail between the heading and the first subsection, which is the run "pos-top" covers. // Anything else in that run is stepped over: a headword line, a declension table or the stylesheet a // template brings with it all sit before the pictures on plenty of entries. function attachImageEditors($h3, language, sectionType, sectionOrdinal) { if (!IMAGE_HEADER) { return; } for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) { if (headingDepth($cursor)) { break; } if ($cursor.is(".thumb, figure, .floatright, .floatleft")) { attachImageEditor($cursor, $h3, language, sectionType, sectionOrdinal); } } } // A "===" section that is not a part of speech but is one of ours: Bilêvkirin, which sits above the // part-of-speech blocks and belongs to the whole language. function placeSubsectionFor($h3) { var text = headingText($h3); var found = null; PAGE_SUBSECTIONS.forEach(function (s) { if (s.place === "pron" && s.header === text) { found = s; } }); return found; } // The first part-of-speech heading under the same language. The writer works from a section, so // without one there is nothing to anchor the box to. function firstPosHeadingAfter($h3) { for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) { if ($cursor.is(".mw-heading2, h2")) { return null; // the next language starts } var depth = headingDepth($cursor); if (depth !== 3 && depth !== 4) { continue; } var $hh = $cursor.is("h3, h4") ? $cursor : $cursor.find("h3, h4").first(); if (sectionTypeOf($hh) || isManeHeading($hh)) { return $hh; } } return null; } // Its own pencil, opening the same box the section list offers -- a heading nobody can edit from the // page is a heading people edit by hand. function attachPlaceHeadingEditor($h3, subsection) { if ($h3.data("dsbAttached")) { return; } var $pos = firstPosHeadingAfter($h3); if (!$pos) { return; } var $ol = null; for (var $cursor = headingOuter($pos).next(); $cursor.length; $cursor = $cursor.next()) { if ($cursor.is("ol")) { $ol = $cursor; break; } if ($cursor.is(".mw-heading2, .mw-heading3, h2, h3")) { break; } } var language = $ol && languageCodeForOl($ol); if (!language) { return; } $h3.data("dsbAttached", true); var pencil = headingPencil(subsection.header + " " + msg.edit); var $toolbar = pencil.$toolbar; var $toggle = pencil.$toggle; var $panel = pencil.$panel; placeHeadingToolbar($h3, $toolbar); revealHeadingToolbarOnTap($h3, $toolbar); headingOuter($h3).after($panel); bindPanelToggle($toggle, $toolbar, $panel, $pos, subsection.header, language, sectionTypeOf($pos) || MANE_SECTION, sectionOrdinalWithinLanguage($pos)); } // Every other heading inside a language: the ones that sit beside the parts of speech rather than // under one ("=== Etîmolojî ===", "=== Çavkanî ==="), and the ones nobody planned for. Each opens // the wikitext under its own heading, or the {{kol3}} form when that is what it holds. function attachFreeSectionEditors() { $("#mw-content-text").find("h3, h4, h5, h6").each(function () { var $h = $(this); // A heading that already carries one of this gadget's pencils is spoken for. if ($h.data("dsbAttached") || headingOuter($h).find(".dsb-toolbar").length) { return; } var header = headingText($h); if (!header || isHiddenSection(header)) { return; } var language = languageCodeForOl($h); if (!language) { return; } $h.data("dsbAttached", true); attachFreeSectionEditor($h, header, language); }); } function attachFreeSectionEditor($h, header, language) { var free = { header: header, ordinal: headingTextOrdinalWithinLanguage($h), blocker: isWergerSection(header) ? wergerBlocker($h) : null }; var pencil = headingPencil(header + " " + msg.edit); var $toolbar = pencil.$toolbar; var $toggle = pencil.$toggle; var $panel = pencil.$panel; placeHeadingToolbar($h, $toolbar); revealHeadingToolbarOnTap($h, $toolbar); headingOuter($h).after($panel); bindPanelToggle($toggle, $toolbar, $panel, $h, header, language, null, 0, free); } function attachImageEditor($fig, $h3, language, sectionType, sectionOrdinal) { if ($fig.data("dsbAttached")) { return; } var $caption = $fig.find("figcaption, .thumbcaption").first(); if (!$caption.length) { return; } $fig.data("dsbAttached", true); var $toolbar = $("<span class='dsb-toolbar dsb-toolbar-caption'></span>"); var $toggle = $("<a href='#' class='dsb-toggle'></a>").attr("title", IMAGE_HEADER + " biguhêre"); // After the figure, not inside it: the panel is full width, and a floated thumbnail is not. var $panel = $("<div class='dsb-subsection-panel dsb-panel-after-image'></div>").hide(); $toolbar.append($toggle); $caption.append($toolbar); revealHeadingToolbarOnTap($caption, $toolbar); $fig.after($panel); bindPanelToggle($toggle, $toolbar, $panel, $h3, IMAGE_HEADER, language, sectionType, sectionOrdinal); } function attachSubsectionTrigger($h3, $ol, language, sectionType, sectionOrdinal) { if ($h3.data("dsbAttached")) { return; } $h3.data("dsbAttached", true); var $anchor = $ol; for (var $cursor = $ol.next(); $cursor.length; $cursor = $cursor.next()) { if ($cursor.is(".mw-heading2, .mw-heading3, .mw-heading4, .mw-heading5, .mw-heading6, h2, h3, h4, h5, h6")) { break; } $anchor = $cursor; } var $trigger = $("<a href='#' class='dsb-add-subsection'>+ Binebeşekê lê zêde bike</a>"); var $panel = $("<div class='dsb-subsection-panel'></div>").hide(); $trigger.on("click", function (e) { e.preventDefault(); $panel.toggle(); if (!$panel.is(":visible")) { return; } buildOnce($panel, "subsection", function (panels) { panels.buildSubsectionPanel($panel, $h3, language, sectionType, sectionOrdinal); }); revealQueue(); }); $anchor.after($panel); $anchor.after($trigger); } // Attaches the "choose a part of speech" trigger under a heading whose part of speech is unsettled: an // unclassified "=== Mane ===" placeholder, or a "=== Biwêj ===" that is really a noun, verb, ... . function attachConvertPosTrigger($h3, sectionType) { if ($h3.data("dsbConvertAttached")) { return; } $h3.data("dsbConvertAttached", true); // languageCodeForOl() is really just "nearest preceding .besazimani[id] before this element in // document order" -- nothing ol-specific about it, so it works unchanged for a heading too. var language = languageCodeForOl($h3); if (!language) { return; } var sectionOrdinal = sectionOrdinalWithinLanguage($h3); // The same pencil the "====" subsections get, so reclassifying reads as an edit affordance rather // than a line of body text. var pencil = headingPencil("Cureyê rêzimanî hilbijêre", "dsb-convert-pos-panel"); var $toolbar = pencil.$toolbar; var $toggle = pencil.$toggle; var $panel = pencil.$panel; $toggle.on("click", function (e) { e.preventDefault(); $panel.toggle(); // Stays put while its panel is open, so it does not vanish from under the pointer. $toolbar.toggleClass("dsb-revealed", $panel.is(":visible")); if (!$panel.is(":visible")) { return; } buildOnce($panel, "pos", function (panels) { panels.buildConvertPosPanel($panel, $h3, language, sectionType, sectionOrdinal); }); revealQueue(); }); // Raised by the panel's "Betal bike": closes it and drops what was typed, so the pencil reopens it fresh. $panel.on("dsbCancel", function () { $panel.hide().empty().data("built", false); $toolbar.removeClass("dsb-revealed"); }); placeHeadingToolbar($h3, $toolbar); revealHeadingToolbarOnTap($h3, $toolbar); headingOuter($h3).after($panel); } // ---- driver ---- function init() { $("h3, h4").each(function () { var $h3 = $(this); var posDepth = headingDepth($h3) || 3; var sectionType = sectionTypeOf($h3); if (!sectionType) { var place = placeSubsectionFor($h3); if (place) { attachPlaceHeadingEditor($h3, place); return; } if (!isManeHeading($h3)) { return; // not a recognized word-sense section (Etîmolojî, Çavkanî, ...) } // An unclassified placeholder: offer the POS picker, then carry on and wire up the same // per-sense pencils, so the POS choice and the sense edits can be queued and saved together. sectionType = MANE_SECTION; attachConvertPosTrigger($h3, sectionType); } else { // Every part of speech, not just the ones most often mistaken: any section can turn out to // be another, and the picker is what changes it. attachConvertPosTrigger($h3, sectionType); } var sectionOrdinal = sectionOrdinalWithinLanguage($h3); var $ol = null; for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) { if ($cursor.is("ol")) { $ol = $cursor; break; } if (headingDepth($cursor)) { break; } } if (!$ol) { return; } var language = languageCodeForOl($ol); if (!language) { return; } var lastTopIndex = $ol.children("li").not(".mw-empty-elt").length - 1; eachLeafDefinition($ol, function ($li) { var definitionLevel = []; var $definitionItem = $li; while ($definitionItem.prop("tagName") === "LI") { definitionLevel.splice(0, 0, $definitionItem.prevAll("li:not(.mw-empty-elt)").length); $definitionItem = $definitionItem.parent().parent(); } definitionLevel.splice(0, 0, sectionType + "|" + sectionOrdinal); var isLastSense = definitionLevel.length === 2 && definitionLevel[1] === lastTopIndex; attachToolbar($li, language, sectionType, sectionOrdinal, definitionLevel, $h3, isLastSense); attachExampleEditors($li, language, sectionType, sectionOrdinal, definitionLevel, $h3); }); attachSubsectionTrigger($h3, $ol, language, sectionType, sectionOrdinal, posDepth); attachSubsectionEditors($h3, language, sectionType, sectionOrdinal, posDepth); attachImageEditors($h3, language, sectionType, sectionOrdinal); }); // Last, so a heading one of the walks above claimed keeps the pencil that knows more about it. attachFreeSectionEditors(); attachHeadingTools(); } // Beside each heading's pencil: ▲ ▼ to move an "===" section within its language, and its depth // (h3, h4, h5) for any heading from "===" to "=====". Both go into the save queue. function attachHeadingTools() { $("#mw-content-text").find("h3, h4, h5").each(function () { var $h = $(this); var depth = headingDepth($h); var language = languageCodeForOl($h); if (depth < 3 || depth > 5 || !language || !headingText($h) || $h.data("dsbHeadingTools")) { return; } $h.data("dsbHeadingTools", true); var $toolbar = headingOuter($h).find(".dsb-toolbar-heading").first(); if (!$toolbar.length) { $toolbar = $("<span class='dsb-toolbar dsb-toolbar-heading'></span>"); placeHeadingToolbar($h, $toolbar); revealHeadingToolbarOnTap($h, $toolbar); } var $tools = $("<span class='dsb-heading-tools'></span>"); if (depth === 3) { [[-1, "▲", "Vê beşê bibe jor"], [1, "▼", "Vê beşê bibe jêr"]].forEach(function (b) { $("<button type='button' class='dsb-heading-move'></button>").text(b[1]).attr("title", b[2]) .on("click", function (e) { e.preventDefault(); withPanels(function (panels) { panels.moveH3(language, $h, b[0]); }); revealQueue(); }) .appendTo($tools); }); } var $depth = $("<select class='dsb-heading-depth'></select>").attr("title", "Dereceya sernavê"); [3, 4, 5].forEach(function (d) { $depth.append($("<option></option>").val(d).text("h" + d)); }); $depth.val(depth).on("change", function () { var to = Number($depth.val()); withPanels(function (panels) { panels.setDepth(language, $h, to); }); revealQueue(); }); $tools.append($depth); $toolbar.append($tools); }); } // One listener for every "Hevmane û Bêhtir" dropdown on the page. $(document).on("click.dsbMoreMenu", function (e) { $(".dsb-more-menu:visible").each(function () { var $menu = $(this); if ($(e.target).closest($menu).length || $(e.target).closest($menu.prev(".dsb-more-toggle")).length) { return; } $menu.slideUp(); var $arrow = $menu.data("dsbArrow"); if ($arrow) { $arrow.text("▸"); } }); }); // Closes every open panel/preview this gadget can leave in the DOM, so bfcache never freezes one mid-open. function closeAllOpenUi() { if (panelsApi) { panelsApi.clearQueue(); panelsApi.closeAllPanels(); return; } // Before the panels arrive there is nothing built to tear down, only the view-path rows. $(".dsb-quick-row, .dsb-more-menu, .dsb-preview-inline, .dsb-preview-block").hide(); } $(window).on("pagehide", closeAllOpenUi); // Belt-and-suspenders: also close on restore-from-bfcache, in case pagehide's own hide didn't stick. $(window).on("pageshow", function (e) { if (e.originalEvent && e.originalEvent.persisted) { closeAllOpenUi(); } }); $(init); // </nowiki> }()); // </nowiki> s4lifbnocgn7283g4oljq1igyd6kima MediaWiki:Gadget-wikt.start-wizard.js 8 1359175 6903105 6865630 2026-11-10T21:29:06Z Wikihez 7344 Script pages under 100 KB: part of the code moved to a second file; shared code in one place 6903105 javascript text/javascript // <nowiki> /* * Page: [[MediaWiki:Gadget-wikt.start-wizard.js]] */ // Li ser [[vi:MediaWiki:Gadget-start.js]] hatiye refaktorkirin. // implicit dependencies: StorageUtils (ObjectStorage), Gadget-citoid-helper.js (wikt.citoid) /* global ObjectStorage, wikt */ $(function () { if (window.location.href.includes("editintro=User:Wikihez/creation/intro")) { return; // Do not run the script wextê WF:Bilez dişuxile } // The same gate DefSideBoxes, add-examples and this wizard's own launcher use, so a page one of them // works on is never a page this one quietly refuses to open on. var ns = mw.config.get("wgNamespaceNumber"); if (!require("ext.gadget.wikt.core").sections.isEntryPage()) { return; } // Reconstructed entries (ns 108) are unattested proto-forms: there is no recorded usage to give an // example of or quote, and no dialects to mark, so the sense block leaves out Nimûne, Jêgirtin and // Dever/Şîwe there. var isReconstruction = ns === 108; var addLangMode = mw.util.getParamValue("wiktkuaddlang") === "1"; // ── Tercîh (user settings) panel stored via mw.storage (localStorage), globally // Defaults and storage live in wikt.core/prefs.js, because the launcher reads them on every page view. var wiktkuPrefsApi = require("ext.gadget.wikt.core").prefs; function wiktkuLoadPrefs() { return wiktkuPrefsApi.load(); } function wiktkuSavePrefs(prefs) { return wiktkuPrefsApi.save(prefs); } var wiktkuPrefs = wiktkuLoadPrefs(); // Routine/informational notices go through here so "quiet mode" can // suppress them; errors and explicit-action confirmations (like the // Tercîh save toast) call mw.notify directly instead, and always show. function wiktkuNotify(message, options) { if (wiktkuPrefs.quiet) return; mw.notify(message, options); } // The launcher gadget adds the portlet link and decides whether this page wants the wizard before // loading it at all. Both are repeated here only when this file was loaded on its own. var wiktkuLaunched = !!(window.wikt && window.wikt.start && window.wikt.start.launched); if (!wiktkuLaunched && !document.getElementById("ca-wiktku-addlang")) { wiktkuPrefsApi.addLauncherLink(wiktkuPrefs); // If "onlyOnClick" is set, the wizard should ONLY ever open via the // "+Ziman/beşeke din" link — never automatically var wiktkuNewPageDetected = $(".mw-newarticletext, .mw-newarticletextanon").length > 0; if (wiktkuPrefs.onlyOnClick ? !addLangMode : (!addLangMode && !wiktkuNewPageDetected)) { return; } } // ── Shared helpers ─────────────────────────────────────────────────── // Generic text/UI utilities live in their own package file now — see the // header of util.js for why. Aliased to bare names here so the ~400 call // sites below read exactly as they did when these were local functions. var wiktkuEdit = require("ext.gadget.wikt.edit"); var wiktkuUtil = wiktkuEdit.util; var wiktkuWikitext = wiktkuEdit.wt; var wiktkuLangs = wiktkuEdit.langs; // Citations (jêder-*, Kategorî:Çavkanî, Citoid) all go through one module -- see wikt.cite.js. var wiktkuCite = require("ext.gadget.wikt.cite"); var wiktkuPosparams = wiktkuEdit.posparams; // Assigned once data-main.json is in: re-merges the language choices, and is called a second time if the // full language list has to be fetched. Declared here so the step renderer can reach it. var wiktkuMergeLanguages = null; var capitalize = wiktkuUtil.capitalize; var decapitalize = wiktkuUtil.decapitalize; var escapeRegExp = wiktkuUtil.escapeRegExp; var stripWikilinks = wiktkuUtil.stripWikilinks; var splitTercimeIzah = wiktkuUtil.splitTercimeIzah; var insertAtCursor = wiktkuUtil.insertAtCursor; var wrapNativeSelect = wiktkuUtil.wrapNativeSelect; var initSelects = wiktkuUtil.initSelects; var wiktkuInitManualResize = wiktkuUtil.initManualResize; var langHeaderSource = wiktkuWikitext.langHeaderSource; // Inserts a line-style template snippet ("* {{…}}") at the cursor, // prefixing it with a newline unless it's going at the very start of the // box or right after one already — otherwise picking a second template // from the pron dropdown right after the first lands them both on the // same line: "* {{ku-IPA}}* {{ku-kîte}}". function insertLineSnippet(el, snippet) { var before = el.value.slice(0, el.selectionStart); if (before && !/\n$/.test(before)) snippet = "\n" + snippet; insertAtCursor(el, snippet); } // One delete button per tab bar, acting on the tab in view. The trash glyph // (oojs-ui.styles.icons-moderation) is fetched here rather than at startup: it is only needed once a // second Mane or Beş exists. The button is built immediately and the icon appears when the CSS lands. function makeTrashButton(title) { mw.loader.using("oojs-ui.styles.icons-moderation"); return new OO.ui.ButtonWidget({ icon: "trash", flags: ["destructive"], framed: false, invisibleLabel: true, label: title, title: title, classes: ["wiktku-trash-btn"] }); } function findLangHeader(text, code) { var m = new RegExp("\n?" + langHeaderSource(escapeRegExp(code))).exec(text); return m ? { index: m.index, match: m[0] } : null; } // The category's members come from wikt.cite, which also lists each template's redirects so it can be // found by any of its names. Entries are {label, target}: label is what was typed/shown, target the // canonical template to transclude. var catSrcTemplates = null; function fetchCatSrcTemplates() { return wiktkuCite.fetchCategoryTemplates().then(function (list) { catSrcTemplates = list; return list; }); } var pageName = mw.config.get("wgPageName").replaceAll(/_/g, " "); if (ns) pageName = pageName.replace(/.*\//, ""); var userLang = mw.config.get("wgUserLanguage"); var formCache = new ObjectStorage("wiktku_start_draft_" + mw.config.get("wgPageName"), 1); var langsByCode = {}; // Looks up the display label for a language code, falling back to the // code itself if it isn't known yet (e.g. langsByCode not loaded). function getLangLabel(code) { var entry = langsByCode[code]; return entry ? entry.label : code; } // The mane box holds a Kurdish definition for a Kurdish entry, and a direct translation for every // other language -- the {{îzah}} box under it carries the explanation. function manePlaceholder(code) { return code && code !== "ku" ? wiktkuUtil.msg.tercimePrompt : "Maneya vê kelîmeyê binivîse…"; } // A Kurmancî example gets an explanation, any other a translation. function nimuneTranslationPrompt(code) { return code === "ku" ? "Îzaheke nimûneyê bi kurmancî…" : "Tercimeya nimûneyê bi kurmancî…"; } // Populated from data-main.json at runtime. var metadata = {}; var ZARAVA_LANG_CODES = ["ckb", "sdh", "lki", "hac", "zza"]; // Converts a {code: label} object (langChoices, posChoices) // into the {code, label[, sortKey]} array the rest of the script uses. // isLang: true adds a sortKey and shows "(code)" after the label — // language choices only. function choicesFromObject(obj, isLang) { var result = []; $.each(obj || {}, function (code, label) { var choice = { code: code, label: label }; if (isLang) choice.sortKey = label; result.push(choice); }); return result; } function getZaravaKeys(choices) { var byCode = {}; $.each(choices || [], function (_, c) { byCode[c.code] = c; }); var result = []; $.each(ZARAVA_LANG_CODES, function (_, code) { if (byCode[code]) result.push(byCode[code]); }); return result; } var needsTransliteration = wiktkuPosparams.needsTransliteration; // metadata[lang].sc is either a single script string or an array of // several — only the latter means the language is actually written in // more than one script. function langHasMultipleScripts(langCode) { var meta = metadata[langCode]; return !!(meta && Array.isArray(meta.sc) && meta.sc.length > 1); } // Kodên cinsiyetê yên di metadata.g de — ji data-main.json tê dagirtin var GENDER_BASE_LABELS = {}; var SENSE_GROUPS = []; var WORDLIST_TEMPLATE = "kol3"; // replaced by data-main.json's first wordlistTemplates entry // The pills the Dever/Şîwe picker opens with; everything else comes from Modul:labels by typing. var DEVER_QUICK_PICKS = []; // ── Template explanations ──────────────────────────────────────────── // Every place a template can be picked shows the same row: a status // dot, the name, a link to the template page, and an ⓘ. This is the ⓘ. // // Text comes from the template's own TemplateData where it has any, so // the explanation stays right without anyone maintaining a second copy // by hand — with the desc already in data-main.json as the fallback for // templates that have none. // {desc, params} for the ⓘ popup, from wikt.edit -- the fetch, the shaping and the cache are all // there, shared with wikt.cite and DefSideBoxes, so a template is asked for once however many pickers // show it. Null for a template with no TemplateData, and that answer is kept too, so clicking ⓘ on // an undocumented template twice is one request. var fetchTemplateData = wiktkuEdit.wt.templateDoc; var prefetchTemplateData = wiktkuEdit.wt.templateDocBatch; /** Rewrites each tagged <option> in `sel` as "name — description", using * TemplateData. Only fills rows that have no description yet, so the * hand-written ones in data-main.json (which carry usage examples the API * does not) are left exactly as they are. */ function applyTplDescriptions(sel) { if (!sel || !sel.length) return; var changed = false; sel.find("option[data-tpl]").each(function () { var opt = $(this); var name = opt.attr("data-tpl"); if (opt.text().indexOf(" — ") !== -1) return; // already described var td = wiktkuEdit.wt.templateDocNow(name); var desc = (td && td.desc) || localTplDesc(name); if (!desc) return; opt.text(name + " — " + wiktkuEdit.wt.shortDesc(desc)); changed = true; }); // The picker caches option text for searching, so it has to re-read. if (changed) sel.trigger("wiktku:refresh"); } /** Description from data-main.json's "desc", for templates with no TemplateData on the wiki. Works for any * pool: pronTpls, etymTpls, tplDefs, jederDefs. */ function localTplDesc(name) { var pools = [steps.pron.pronTpls, steps.etym.etymTpls, steps.dfn.tplDefs, steps.dfn.jederDefs]; var found = ""; $.each(pools, function (_, pool) { if (pool && pool[name] && pool[name].desc) { found = pool[name].desc; return false; } }); return found; } // Under the ⓘ it was opened from, kept inside the viewport, and flipped above it when there is no // room below. Narrow screens get the width they have rather than a fixed card off the edge. // // box is where that ⓘ was when it was clicked. The row it sits in is closed by then, so it can no // longer be measured; the last known box is kept for the re-placements after the text arrives. function positionTplInfo(panel, anchor, box) { var el = $(anchor).get(0); var rect = box || (el && el.getBoundingClientRect ? el.getBoundingClientRect() : null); if (rect && !rect.width && !rect.height) { rect = null; } if (rect) { panel.data("wiktkuAnchorBox", rect); } else { rect = panel.data("wiktkuAnchorBox") || null; } var margin = 8; var arrow = 8; var width = Math.min(360, $(window).width() - margin * 2); panel.css({ width: width + "px", top: "0px", left: "0px" }); var height = panel.outerHeight(); // Above the dropdown, pointing down at it, the way MediaWiki's own filter tips sit; below it only // when there is no room above. var above = !rect || rect.top - height - arrow >= margin; var top = !rect ? margin : (above ? rect.top - height - arrow : rect.bottom + arrow); var left = rect ? Math.min(Math.max(margin, rect.left), $(window).width() - width - margin) : margin; // The arrow points at the start of the box it explains, kept clear of the tip's corners. var arrowX = rect ? Math.min(Math.max(16, rect.left + 24 - left), width - 16) : 16; panel.toggleClass("wiktku-tpl-info-above", above).toggleClass("wiktku-tpl-info-below", !above); panel.css({ top: top + "px", left: left + "px" }); panel.get(0).style.setProperty("--wiktku-arrow-x", arrowX + "px"); } /** Opens the explanation for `name` as a tip above the dropdown it was asked from. */ function showTemplateInfo(name, anchorWrap, anchorEl, anchorBox) { $(".wiktku-tpl-info").remove(); var panel = $("<div class='wiktku-tpl-info'></div>"); panel.append($("<div class='wiktku-tpl-info-name'></div>").text("{{" + name + "}}")); var body = $("<div class='wiktku-tpl-info-desc'></div>").text(wiktkuUtil.msg.loading); panel.append(body); var params = $("<div class='wiktku-tpl-info-params'></div>"); panel.append(params); panel.append($("<a class='wiktku-tpl-info-link' target='_blank' rel='noopener'></a>") .attr("href", wiktkuUtil.tplPageUrl(name)) .text(wiktkuUtil.msg.openTemplatePage + " →")); function closePanel() { panel.remove(); $(document).off(".wiktkuTplInfo"); $(window).off(".wiktkuTplInfo"); } var close = $("<button type='button' class='wiktku-tpl-info-close' title='Bigire'>✕</button>"); close.on("click", closePanel); panel.prepend(close); $(document).on("keydown.wiktkuTplInfo", function (e) { if (e.which === 27) { closePanel(); } }); $(document).on("mousedown.wiktkuTplInfo", function (e) { if (!$(e.target).closest(".wiktku-tpl-info").length) { closePanel(); } }); // A panel that stayed put while the page moved under it would point at nothing. $(window).on("resize.wiktkuTplInfo scroll.wiktkuTplInfo", function () { positionTplInfo(panel, anchorWrap); }); // Over the form, not inside it: as a block in the flow it pushed every field below it down the // page for as long as it was open, and the picker it explains moved with them. $(document.body).append(panel); positionTplInfo(panel, anchorWrap); fetchTemplateData(name).then(function (td) { var desc = (td && td.desc) || localTplDesc(name); body.text(desc || "Ji bo vê şablonê îzahek nehatiye nivîsandin."); if (td && td.params && td.params.length) { params.append($("<div class='wiktku-tpl-info-params-title'></div>").text("Parametre:")); var listEl = $("<ul></ul>"); $.each(td.params, function (_, p) { var line = p.key + (p.label ? " — " + p.label : "") + (p.required ? " (mecbûrî)" : ""); listEl.append($("<li></li>").text(line)); }); params.append(listEl); } // The panel is as tall as its text, which only arrives now. positionTplInfo(panel, anchorWrap); }); } /** Marks a picker's <option>s as template rows so the searchable widget * renders them with the dot / 🔗 / ⓘ affordances, and wires the ⓘ * handler for that picker. `nameOf` maps an <option> to a template * name — the pickers store different things in value= (a snippet for * the insert dropdowns, the bare name for the sense selector). */ function markTemplateOptions(sel, nameOf) { if (!sel || !sel.length) return; var names = []; sel.find("option").each(function () { var opt = $(this); var name = nameOf(opt); if (!name) return; opt.attr("data-tpl", name); // Deprecated/typo templates are the "special cases only" ones. opt.attr("data-tpl-status", /kevnbûyî|şaşnivîs|kêmpeyda/i.test(opt.text()) ? "yellow" : "green"); names.push(name); }); sel.data("wiktkuTplInfo", showTemplateInfo); // Fill in the descriptions. Matters most for the Mane picker, whose // entries in data-main.json are names with no desc at all — "rastnivîs", // "ku-Latn", "guharto" on their own. One request covers every // template in the picker, cached for the per-row ⓘ as well. prefetchTemplateData(names).always(function () { applyTplDescriptions(sel); }); } /** Pulls the template name out of a snippet like "* {{ku-IPA}}" or * "{{ji|SOURCELANG|CODE|…}}". */ function tplNameFromSnippet(snippet) { var m = /\{\{\s*([^|}\n]+)/.exec(snippet || ""); return m ? $.trim(m[1]) : ""; } // True if a sense has content beyond mane worth keeping (etîket, word // lists, nimûne, jêder) — so it's still emitted with {{mane?}} if empty. function senseHasExtraContent(s) { if (s.maneid) return true; if (s.etiket && $.grep(s.etiket, function (e) { return $.trim(e) !== ""; }).length) return true; if (s.kategori && $.grep(s.kategori, function (k) { return $.trim(k) !== ""; }).length) return true; var anyWords = false; $.each(wordGroups(), function (_, g) { if ((s[g.field] || []).length) anyWords = true; }); if (anyWords || (s.dever && s.dever.length)) return true; if (s.nimune && $.grep(s.nimune, function (n) { return (typeof n === "object" ? n.text : n) !== ""; }).length > 0) return true; if (senseHasJeder(s)) return true; return false; } // A technically-"complete" entry (Ziman + Cureyê rêzimanî filled) with // nothing in Mane, Bi zaravayên din or Werger is still meaningfully // empty, so this is checked separately at submit-click time with its // own message. // Line breaks in a field save as "<br>"; this marks them so that is visible while typing. var highlightLinebreak = wiktkuUtil.makeLinebreakHighlighter({ prefix: "wiktku" }); // Each nimûne item can be { text, tr, t }: text, transliteration, translation. function formatNimune(nimunes, lang) { var hasNimune = nimunes && $.grep(nimunes, function (n) { return (typeof n === "object" ? n.text : n) !== ""; }).length > 0; if (!hasNimune) { // A sense with no example is left as it is, with no "{{nimûne?}}" placeholder. return ""; } return $.map(nimunes, function (n) { var text = typeof n === "object" ? n.text : n; if (!text) return undefined; // The builder DefSideBoxes and add-examples use: the translation in slot 3, rêz=1 for a short pair. return "\n" + wiktkuCite.buildCitation({ mode: "plain", marker: "#:", language: lang, title: pageName, quote: text, translation: typeof n === "object" ? n.t : "", transcription: typeof n === "object" ? n.tr : "" }); }).join(""); } // Formats jêder (citation) entries into "#* {{jêder…|…}}" lines; skips // entries with no quote text. Falls back to the generic {{jêder}} // template when no specific template was chosen. function formatJeder(jederList, lang) { if (!jederList || !jederList.length) return ""; return $.map(jederList, function (jeder) { if (!jeder || !jeder.quote) return undefined; var mode = jeder.raw ? "raw" : jeder.jederTpl ? "jeder" : jeder.jederCatTpl ? "category" : "plain"; return "\n" + wiktkuCite.buildCitation({ mode: mode, marker: "#*", language: lang, template: jeder.jederTpl || jeder.jederCatTpl, fields: jeder.jederTpl ? (jeder.jêderFields || {}) : (jeder.jederCatFields || {}), langParamKey: jeder.jederCatLangParam, quote: jeder.quote, transcription: jeder.quoteTr, translation: jeder.quoteT, source: jeder.jederManual || (mode === "plain" && !jeder.url ? "çavkanî?" : ""), sourceURL: jeder.url }); }).join(""); } // True if a sense has at least one jêder entry with quote text. function senseHasJeder(sense) { return !!(sense.jeder && $.grep(sense.jeder, function (j) { return j && j.quote; }).length > 0); } // Builds "{{tplName|lang|part1|part2|…}}" from a sense field (kategori, // etiket), wrapped inline (" {{…}} ") or on its own line ("\n#: {{…}}"). function buildSenseTpl(sense, field, lang, tplName) { if (!sense[field] || !sense[field].length) return ""; var parts = sense[field].map(function (p) { return $.trim(p); }) .filter(function (p) { return p !== ""; }); if (!parts.length) return ""; var call = "{{" + tplName + "|" + lang + "|" + parts.join("|") + "}}"; return call } // Dever/Şîwe values only ever get their own {{dever|…}} call for ku — // no |lang| param, since the template is Kurdish-dialect-specific. For // every other language there's no dever template at all; see // buildEtiketTpl() below, which folds them into {{e|}} instead. function buildDeverTpl(sense, lang) { if (lang !== "ku" || !sense.dever || !sense.dever.length) return ""; var parts = sense.dever.map(function (p) { return $.trim(p); }) .filter(function (p) { return p !== ""; }); if (!parts.length) return ""; return "{{dever|" + parts.join("|") + "}}"; } // Builds the {{e|lang|…}} etîket call. For non-ku senses, Dever/Şîwe // values are appended after the regular etîket values instead of going // into their own {{dever|}} call (see buildDeverTpl() above). function buildEtiketTpl(sense, lang) { var etiket = (sense.etiket || []).slice(); if (lang !== "ku" && sense.dever && sense.dever.length) { etiket = etiket.concat(sense.dever); } return buildSenseTpl({ etiket: etiket }, "etiket", lang, "e"); } /** * Assembles the nimûne + jêder wikitext block for a sense. Shared by * both the template-sense and normal-mane-sense branches below. */ function buildCite(sense, lang) { return formatNimune(sense.nimune, lang) + formatJeder(sense.jeder, lang); } // ── Inline modifier system ─────────────────────────────────────────────── // "{{maneid|lang|x}}", the anchor id a sense can be linked by. First of the leading calls, which is // data-main.json's leadingTplOrder. function buildManeidTpl(sense, lang) { var id = $.trim(sense.maneid || ""); return id ? "{{maneid|" + lang + "|" + id + "}}" : ""; } // Quick-pick suggestions for the Dever/Şîwe, Etîket and Kategorî chip pickers on each sense, taken // from [[Modul:labels]] rather than kept by hand here. Free text is still always accepted; this only // saves typing, and keeps the lists honest as the module changes. // // Fetched on the first time one of the three panels is opened, never on load: the wizard opens on // pages that never touch these fields. Once it lands, the open panel re-renders itself. var wiktkuLabelSets = null; var wiktkuLabelsPending = null; var wiktkuLabelListeners = []; function wiktkuEnsureLabels() { if (wiktkuLabelSets || wiktkuLabelsPending) return; wiktkuLabelsPending = wiktkuEdit.labels.load().then(function (sets) { wiktkuLabelSets = sets; wiktkuLabelsPending = null; $.each(wiktkuLabelListeners, function (_, fn) { fn(); }); }, function () { wiktkuLabelsPending = null; }); } // Which list each picker shows. Dever/Şîwe is the regions of the language being edited; Etîket adds // the qualifiers and subject labels on top; Kategorî is the subject labels alone. function wiktkuLabelSuggestions(which) { if (!wiktkuLabelSets) return []; var lang = (getValues().lang || "ku"); if (which === "dever") return wiktkuEdit.labels.regionalFor(wiktkuLabelSets, lang); if (which === "etiket") return wiktkuEdit.labels.etiketFor(wiktkuLabelSets, lang); return wiktkuEdit.labels.kategoriNames(wiktkuLabelSets); } // Lower-cased alias -> canonical label, so typing a name the module lists as an alias still finds it. // Built once per label load rather than per keystroke. var wiktkuAliasCache = null; function wiktkuLabelAliases() { if (!wiktkuLabelSets) return {}; if (!wiktkuAliasCache) { wiktkuAliasCache = wiktkuEdit.labels.aliasIndex(wiktkuLabelSets); } return wiktkuAliasCache; } // Appends a ⊕ button to a word input <li>. Clicking it shows a compact // modifier picker; selecting one prompts for the value and appends // <modifier:value> to the textarea's current content. // Sub-groups (inside a sense/definition block) that get the same ⊕ // modifier button as makeWordListStep fields. /** * The word lists (Hevmane, Dijmane, Formên alternatîv, Termên koordîne, Jornav, Jêrnav), taken from * data-main.json's senseGroups so the wizard, DefSideBoxes and the wikitext writer all name them once. * That array's own order is the order the "#:" lines are written to the page in; "order" is the * order they are shown in, which is not the same. cls is how each one's box is found in the DOM. */ function wordGroups() { return SENSE_GROUPS.slice().sort(function (a, b) { return (a.order || 0) - (b.order || 0); }).map(function (g) { return { cls: "wiktku-dfn-" + g.field, label: g.label, field: g.field, izah: g.hint || "" }; }); } /** True for the six word lists, which get the ⊕-modifier row; Nimûne and Jêgirtin do not. */ function isWordGroupCls(cls) { var found = false; $.each(SENSE_GROUPS, function (_, g) { if ("wiktku-dfn-" + g.field === cls) found = true; }); return found; } /** * Wraps a textarea in a <li> with a ⊕ modifier button and returns the <li>. * * `ta` stays hidden and holds the raw "word<key:value>…" text every reader and writer expects. What * is typed into is a plain word box, with one pill per modifier above it; clicking a pill edits its * value, ⊕ adds another. */ function makeModifierLi(ta) { var narrow = (ta.attr("class") || "").split(/\s+/).indexOf("wiktku-start-narrow") !== -1; // Not ta's own classes: code elsewhere counts elements by those, so a second matching textarea // would skew the counts. wiktku-word-input carries the same look. var wordInput = $("<textarea></textarea>") .attr("rows", ta.attr("rows") || 1) .addClass("wiktku-word-input" + (narrow ? " wiktku-start-narrow" : "")) .attr("placeholder", ta.attr("placeholder") || ""); ta.hide(); // Placeholders on the (hidden) canonical field are set by shared code that has no reason to know // wordInput exists -- mirror them instead of touching every call site. if (window.MutationObserver) { new MutationObserver(function () { wordInput.attr("placeholder", ta.attr("placeholder") || ""); }).observe(ta[0], { attributes: true, attributeFilter: ["placeholder"] }); } // ta stays the source of truth: the row writes back to it, and the caller's growth listener is // bound to ta's "input" event. var row = wiktkuUtil.makeModifierRow({ prefix: "wiktku-", wordInput: wordInput, modTitle: "Modifiyerê lê zêde bike", removeTitle: wiktkuUtil.msg.removeWord, removeModTitle: wiktkuUtil.msg.removeModifier, onChange: function (raw) { if (raw === null) return; // the row removed itself; ta goes with it ta.val(raw); ta.trigger("input"); }, // "bixe qutiyan" puts each comma-separated word in a row of its own. The list grows off the // hidden field's own "input" event, so filling the trailing row is what makes the next one // appear; the caret ends up in the empty row at the bottom. One word per row is also one // word per template parameter, which both {{hevmane}} and {{kol3}} read the same way -- a // comma left inside a parameter would not be. onSplit: function (rest) { // A tick later, not now: the row writes its own value to the hidden field after this // call returns, and it is that field's "input" that grows the list -- so the empty row // to put the next word in does not exist yet. setTimeout(function () { var $list = row.$li.closest("ol, ul"); rest.forEach(function (raw) { if (!raw) return; var setRaw = $list.children("li").last().data("setRawValue"); if (setRaw) setRaw(raw); }); $list.children("li").last().find(".wiktku-word-input").focus(); }, 0); } }); row.setRaw(ta.val()); var li = row.$li; // restoreSubList's "fill an existing row" path sets a raw "word<key:value>" value here. li.data("setRawValue", function (raw) { row.setRaw(raw); ta.val(row.getRaw()); ta.trigger("input"); }); li.prepend(ta); return li; } /** * A "chip + toggle" field: values render as removable chips, and a "+ <label>" toggle opens a panel * with quick-pick pills plus free text. panel is returned apart from wrap so callers can group all * toggles in one row and all panels below, keeping toggles from shifting when one opens. * * Pills carry a × as well: hiding a built-in one, or typing a new value, is remembered per wrapCls * in mw.storage. * * opts: { wrapCls, toggleLabel, hint, suggestions (array or function), placeholder, onOpen } * Returns { wrap, panel, addChip, values, setOpenDefault, close }. */ var WIKTKU_SUGGESTIONS_KEY_PREFIX = "wiktku_suggestions_"; function loadSuggestionOverrides(wrapCls) { if (!(window.mw && mw.storage)) return { removed: [], added: [] }; var raw; try { raw = mw.storage.get(WIKTKU_SUGGESTIONS_KEY_PREFIX + wrapCls); } catch (e) { raw = null; } var parsed = {}; if (raw) { try { parsed = JSON.parse(raw); } catch (e) { parsed = {}; } } return { removed: parsed.removed || [], added: parsed.added || [] }; } function saveSuggestionOverrides(wrapCls, overrides) { if (!(window.mw && mw.storage)) return; try { mw.storage.set(WIKTKU_SUGGESTIONS_KEY_PREFIX + wrapCls, JSON.stringify(overrides)); } catch (e) { /* storage unavailable — override just won't persist */ } } // One document listener for every sense's "Hevmane û Bêhtir" menu, not one per sense: a long entry can hold a // dozen senses and each would otherwise bind its own. var moreMenuCloserBound = false; function bindMoreMenuCloser() { if (moreMenuCloserBound) return; moreMenuCloserBound = true; $(document).on("click.wiktkuMore", function (e) { if ($(e.target).closest(".wiktku-dfn-more-wrap").length) return; $(".wiktku-dfn-more-menu").hide(); $(".wiktku-dfn-more-arrow").html("&#9656;"); }); } function buildChipField(opts) { var wrap = $("<div></div>").addClass("wiktku-dfn-chipfield-wrap " + opts.wrapCls + "-wrap"); var chipsRow = $("<span class='wiktku-dfn-chipfield-chips'></span>"); var toggle = $("<button type='button' class='wiktku-dfn-chipfield-toggle'></button>").text(opts.toggleLabel); var panel = $("<div class='wiktku-dfn-chipfield-panel'></div>").hide(); var suggestRow = $("<div class='wiktku-dfn-chipfield-suggest'></div>"); var input = $("<input type='text' class='wiktku-dfn-chipfield-input'>") .attr("placeholder", opts.placeholder || ((opts.suggestions && opts.suggestions.length) ? "An ya nû binivîse…" : "Binivîse…")); var addBtn = $("<button type='button' class='wiktku-dfn-chipfield-add'>Qeyd bike</button>"); // Once the person opens/closes the panel themselves, lang-driven // syncing (see setOpenDefault) backs off rather than fighting them. var userToggled = false; // The pick list shown is opts.suggestions with the person's own // edits layered on top — see loadSuggestionOverrides above. var overrides = loadSuggestionOverrides(opts.wrapCls); var removedSuggestions = overrides.removed; var addedSuggestions = overrides.added; function persistOverrides() { saveSuggestionOverrides(opts.wrapCls, { removed: removedSuggestions, added: addedSuggestions }); } // opts.suggestions is either a fixed array or a function returning one -- the label-driven // pickers depend on the language being edited, and on data that arrives from Modul:labels after // the field is built, so they cannot be a snapshot taken here. function baseSuggestions() { var list = typeof opts.suggestions === "function" ? opts.suggestions() : opts.suggestions; return list || []; } function allSuggestions() { return $.grep(baseSuggestions(), function (s) { return removedSuggestions.indexOf(s) === -1; }) .concat(addedSuggestions); } function learnSuggestion(val) { if (!val || allSuggestions().indexOf(val) !== -1) return; addedSuggestions.push(val); persistOverrides(); } function forgetSuggestion(name) { var idx = addedSuggestions.indexOf(name); if (idx !== -1) addedSuggestions.splice(idx, 1); else if (removedSuggestions.indexOf(name) === -1) removedSuggestions.push(name); persistOverrides(); renderSuggest(); // renderSuggest() just destroyed the pill's × button the click // came from — which was focused — dropping focus outside the // panel entirely. Left alone, that reads as "focus left the // widget" to the focusout handler below and closes the whole // panel. Move focus back inside before that check runs. input.focus(); } function values() { var vals = []; chipsRow.find(".wiktku-dfn-chipfield-chip").each(function () { vals.push($(this).attr("data-value")); }); return vals; } // Label-driven pickers are built before Modul:labels has been read, so re-render once it lands. if (typeof opts.suggestions === "function") { wiktkuLabelListeners.push(function () { if (panel.is(":visible")) renderSuggest(); }); } // At most this many pills at once. Modul:labels offers 87 regions, 362 etîket and 209 kategorî; // drawing them all was a wall of pills nobody reads and the field it belonged to scrolled away. var MAX_PILLS = 24; // What shows with an empty box: the short hand-picked list if the caller gave one, otherwise the // full set (capped) as before, for pickers whose lists are small enough to browse. function restingSuggestions() { var quick = typeof opts.quickPicks === "function" ? opts.quickPicks() : opts.quickPicks; if (!quick || !quick.length) { return allSuggestions(); } // A value the person added themselves outranks the hand-picked list -- it is theirs. return quick.concat($.grep(addedSuggestions, function (s) { return quick.indexOf(s) === -1; })); } // Typed text searches the WHOLE set, not just the pills on screen, and matches the module's own // aliases too, so "Behdînanî" finds Badînî and "USA" finds DYA. function matching(query) { var q = $.trim(query).toLowerCase(); if (!q) { return restingSuggestions(); } var aliases = wiktkuLabelAliases(); var hits = $.grep(allSuggestions(), function (name) { return name.toLowerCase().indexOf(q) !== -1; }); $.each(aliases, function (alias, canonical) { if (alias.indexOf(q) !== -1 && hits.indexOf(canonical) === -1 && allSuggestions().indexOf(canonical) !== -1) { hits.push(canonical); } }); return hits; } function renderSuggest(query) { var taken = values(); suggestRow.empty(); var shown = 0; $.each(matching(query || ""), function (_, name) { if (taken.indexOf(name) !== -1 || shown >= MAX_PILLS) return; shown++; var pill = $("<span role='button' class='wiktku-dfn-chipfield-pill'></span>").text(name); pill.on("click", function () { addChip(name); input.val(""); renderSuggest(""); input.focus(); }); var pillRemove = $("<button type='button' class='wiktku-dfn-chipfield-pill-remove' title='Vê tewsiyeyê veşêre'>×</button>"); pillRemove.on("click", function (e) { e.stopPropagation(); // don't also trigger the pill's own add-chip click forgetSuggestion(name); }); pill.append(pillRemove); suggestRow.append(pill); }); suggestRow.toggle(!!suggestRow.children().length); } function addChip(val) { val = $.trim(val || ""); if (!val || values().indexOf(val) !== -1) return; var chip = $("<span class='wiktku-dfn-chipfield-chip'></span>").attr("data-value", val).text(val); var remove = $("<button type='button' class='wiktku-dfn-chipfield-remove' title='Jê bibe'>×</button>"); // Same focus-loss hazard as forgetSuggestion() below — refocus // the input so removing a chip while the panel is open doesn't // also close the panel. remove.on("click", function () { chip.remove(); renderSuggest(); input.focus(); }); chip.append(remove); chip.insertBefore(toggle); // chips in insertion order, toggle stays last renderSuggest(); } toggle.on("click", function () { userToggled = true; if (panel.is(":visible")) { panel.hide(); return; } if (opts.onOpen) opts.onOpen(); renderSuggest(); panel.show(); input.focus(); }); // Anything typed and committed by hand joins the pick list too, not // just the current sense's chips. function commitInput() { var val = $.trim(input.val() || ""); if (val) { addChip(val); learnSuggestion(val); } input.val("").focus(); } addBtn.on("click", commitInput); input.on("keydown", function (e) { if (e.which === 13) { e.preventDefault(); commitInput(); } }); // Typing searches the whole list, so a value that is not among the pills is still one keystroke // from being found rather than something you have to know by heart and type out in full. input.on("input", function () { renderSuggest(input.val()); }); // Close the panel once focus leaves toggle+panel together — they're // siblings in the DOM (not nested), so both must be checked. function focusStillInside() { return $.contains(wrap[0], document.activeElement) || $.contains(panel[0], document.activeElement); } wrap.add(panel).on("focusout", function () { setTimeout(function () { if (!focusStillInside()) panel.hide(); }, 150); }); if (opts.hint) panel.append($("<div class='wiktku-dfn-chipfield-hint'></div>").text(opts.hint)); // Box first, pills under it. The other way round, every keystroke re-filtered the pills above the // box and the box itself jumped up and down the page as the list grew and shrank. panel.append( $("<div class='wiktku-dfn-chipfield-input-row'></div>").append(input).append(addBtn) ).append(suggestRow); chipsRow.append(toggle); wrap.append(chipsRow); return { wrap: wrap, panel: panel, addChip: addChip, values: values, /** Sets the panel's default open/closed state — a no-op once * the person has manually toggled it themselves this session. */ setOpenDefault: function (open) { if (userToggled) return; // Same as the toggle click handler: (re)build the suggestion // pills whenever the panel opens, not just on a manual click — // otherwise a panel opened by this default-state sync (e.g. // Dever/Şîwe starting open for ku) shows an empty pill row // until the person closes and reopens it themselves. if (open) { if (opts.onOpen) opts.onOpen(); renderSuggest(); } panel.toggle(!!open); }, /** Force-closes the panel as though the person had done it * themselves, so a later setOpenDefault() doesn't reopen it out * from under them — used by onOpen to keep only one of these * fields' panels open at a time (see the call site). */ close: function () { userToggled = true; panel.hide(); }, /** Opens the panel from somewhere other than the toggle — the * "Hevmane û Bêhtir" menu, which is where Etîket and Kategorî are asked * for now. Counts as a manual toggle for the same reason close() * does. */ open: function () { userToggled = true; if (opts.onOpen) opts.onOpen(); renderSuggest(); panel.show(); input.focus(); } }; } /** * One page-level word list (Formên alternatîv, Hevmane, … Herwiha binêre): a "====" section holding * the words for the whole entry rather than for one sense. * * Only the question is passed in. The heading and the step's own name come from data-sections.json's * pageSubsections, filled in below once the data lands — the same table DefSideBoxes reads to find * these sections, so the wizard cannot write a heading the panels then fail to recognise. */ function makeWordListStep(id, question_fmt) { return { id: id, name: id, question_fmt: question_fmt, multiple: true, narrow: true, modifiers: true, // Function instead of a plain string so it can name the current // language — computed fresh at build time and re-applied by // refresh() whenever the language changes. placeholder: function () { var lang = getValues().lang || ""; var langLabel = lang ? getLangLabel(lang) : ""; return "Kelîme" + (langLabel ? " bi " + langLabel : "") + "…"; }, visible: function (values) { return values.lang && values.pos; }, output: function (values) { var words = $.grep(values[id] || [], function (w) { return !!w; }); if (!words.length) return ""; var header = steps[id].header || capitalize(steps[id].name); var lang = values.lang; var section = "\n==== " + header + " ====\n"; var params = "cure=" + header + "\n" + $.map(words, function (w) { return "|" + w; }).join("\n"); return section + "{{" + WORDLIST_TEMPLATE + "|" + lang + "|" + params + "\n}}\n"; } }; } // The helpers and settings the step definitions use, for wikt.start-wizard-steps. DEVER_QUICK_PICKS, // SENSE_GROUPS, catSrcTemplates, pageName and wiktkuLabelSets change after loading, so they are passed as // getters and setters on this same closure rather than as copies. function wizardContext() { var ctx = { GENDER_BASE_LABELS: GENDER_BASE_LABELS, ZARAVA_LANG_CODES: ZARAVA_LANG_CODES, addLangMode: addLangMode, applyTplDescriptions: applyTplDescriptions, bindMoreMenuCloser: bindMoreMenuCloser, buildChipField: buildChipField, buildCite: buildCite, buildDeverTpl: buildDeverTpl, buildEtiketTpl: buildEtiketTpl, buildManeidTpl: buildManeidTpl, buildSenseTpl: buildSenseTpl, capitalize: capitalize, decapitalize: decapitalize, fetchCatSrcTemplates: fetchCatSrcTemplates, findLangHeader: findLangHeader, getLangLabel: getLangLabel, getValues: getValues, highlightLinebreak: highlightLinebreak, initSelects: initSelects, insertAtCursor: insertAtCursor, insertLineSnippet: insertLineSnippet, isReconstruction: isReconstruction, isWordGroupCls: isWordGroupCls, langHasMultipleScripts: langHasMultipleScripts, langsByCode: langsByCode, makeModifierLi: makeModifierLi, makeTrashButton: makeTrashButton, makeWordListStep: makeWordListStep, manePlaceholder: manePlaceholder, markTemplateOptions: markTemplateOptions, needsTransliteration: needsTransliteration, nimuneTranslationPrompt: nimuneTranslationPrompt, populateMenus: populateMenus, refresh: refresh, senseHasExtraContent: senseHasExtraContent, splitTercimeIzah: splitTercimeIzah, stripWikilinks: stripWikilinks, tplNameFromSnippet: tplNameFromSnippet, wiktkuCite: wiktkuCite, wiktkuEdit: wiktkuEdit, wiktkuEnsureLabels: wiktkuEnsureLabels, wiktkuLabelListeners: wiktkuLabelListeners, wiktkuLabelSuggestions: wiktkuLabelSuggestions, wiktkuPosparams: wiktkuPosparams, wiktkuUtil: wiktkuUtil, wiktkuWikitext: wiktkuWikitext, wordGroups: wordGroups }; Object.defineProperties(ctx, { DEVER_QUICK_PICKS: { get: function () { return DEVER_QUICK_PICKS; }, set: function (value) { DEVER_QUICK_PICKS = value; } }, SENSE_GROUPS: { get: function () { return SENSE_GROUPS; }, set: function (value) { SENSE_GROUPS = value; } }, catSrcTemplates: { get: function () { return catSrcTemplates; }, set: function (value) { catSrcTemplates = value; } }, pageName: { get: function () { return pageName; }, set: function (value) { pageName = value; } }, wiktkuLabelSets: { get: function () { return wiktkuLabelSets; }, set: function (value) { wiktkuLabelSets = value; } } }); return ctx; } // What each step asks, offers and writes: [[MediaWiki:Gadget-wikt.start-wizard-steps.js]]. var steps = require("ext.gadget.wikt.start-wizard-steps")(wizardContext()); steps.trans.keys = steps.lang.choices; steps.dunde.keys = steps.lang.choices; steps.zarava.keys = getZaravaKeys(steps.lang.choices); // This array only controls DOM build order — nothing else keys off it // (see the comment above stepIdToGroup). Mane tab order: Mane (senses, // with malûmatên pêşketî nested inside each, see steps.dfn.build) → // Wêne → Bi zaravayên din → Werger, so dfn/resim/zarava/trans sit // adjacent to each other in that order. var ordered_steps = [ steps.lang, steps.pos, steps.posparams, steps.cinsiyet, steps.jimar, steps.lektur, steps.tr, // Reading order: the senses, then what belongs to the entry as a whole. Etîmolojî follows // "Bi alfabeyên din" and keeps a permanent slot; dunde and the three "eleqedar" lists are // reached through the Beşên din picker like every other optional section. steps.dfn, steps.tewandin, steps.alfabeyendin, steps.etym, steps.zarava, steps.trans, steps.dunde, steps.je, steps.tekildar, steps.binereherwiha, // Bilêvkirin isn't part of any STEP_GROUPS tab, but still needs a // slot here because this array is what builds the DOM. steps.pron, // Beşên din fields — behind the picker. steps.resim, steps.bnrh, steps.bikaranin, steps.formalt, steps.hevmane, steps.dijmane, steps.jornav, steps.jernav, steps.cavkani, steps.xwendin]; // ── Sections ───────────────────────────────────────────────────────────── // One group, no tab bar. What is always on screen: the senses (whose own blocks carry Nimûne, // Jêgirtin, Hevmane and Dijmane — see steps.dfn.allGroups), Tewandin, alfabeyendin, zarava, trans // and Etîmolojî. Everything else is added from the "Beşên din" picker, which sits at the top of the // form; a newly-picked section is inserted directly under it (see renderBesenDinPicker()), so the // most recently added one is always the one nearest the picker. // // Tewandin keeps its own free-text {{ku-tewîn-nav}} field and does NOT absorb the Cins/zayend form // parameters (m=/n=/p=), which stay on the headword template. // Behind the "Beşên din" picker: shown only after an explicit open, the way DefSideBoxes' own // "+ Beşek lê zêde bike" works -- nothing is on screen until it is asked for. var BESENDIN_STEP_IDS = ["resim", "formalt", "hevmane", "dijmane", "jornav", "jernav", "bikaranin", "cavkani", "xwendin", "bnrh", "dunde", "je", "tekildar", "binereherwiha"]; // One group, so no tab bar: the senses, the fields that go with them, and Etîmolojî, which is common // enough to keep a permanent slot. Everything else is reached through the picker above. var STEP_GROUPS = [ { id: "mane", label: "Mane", steps: ["dfn", "tewandin", "alfabeyendin", "zarava", "trans", "etym"] } ]; // Step id -> index into STEP_GROUPS; undefined for the always-visible ungrouped steps. var stepIdToGroup = {}; $.each(STEP_GROUPS, function (gi, g) { $.each(g.steps, function (_, sid) { stepIdToGroup[sid] = gi; }); }); // Step ids that belong to each section (not shared across sections). var PER_SECTION_STEP_IDS = [ "pos", "posparams", "cinsiyet", "jimar", "lektur", "tr", "resim", "dfn", "tewandin", "formalt", "bikaranin", "alfabeyendin", "hevmane", "dijmane", "jornav", "jernav", "je", "tekildar", "etym", "dunde", "binereherwiha", "zarava", "trans", "cavkani", "xwendin" ]; // ── Multi-section state ────────────────────────────────────────────────── // Each entry is a plain snapshot: { pos, cinsiyet, lektur, tr, dfn (senses[]), // je, tekildar, binereherwiha, trans (rows[]) }. var sections = [{}]; // section 0 starts empty (DOM is the truth) var activeSection = 0; // ── Bilêvkirin as a standalone section ─────────────────────────────── // Pronunciation belongs to the word, not to one part of speech, so it // sits beside Navdêr/Lêker in the tab bar rather than inside any of them. // Modelled as a flag rather than an entry in `sections` since // activeSection is used as an array index in many places — viewing // Bilêvkirin leaves activeSection pointing at the POS section you were on. var viewingPron = false; // is it the tab currently on screen // Which STEP_GROUPS tab is currently shown. Lives out here (like // activeSection above) so refresh() — which is not nested inside build() // — can read it and so renderGroupTabs()/updateGroupTabIndicators() below // can be called from refresh() too. Both operate on the tab bar via its // #wiktku-group-tabs id rather than a closured element reference, for the // same reason. var activeGroup = 0; // Unsaved-changes / autosave state. Lives out here (same reason as // activeGroup above) so destroy() — which runs on final submit and on // switching to the source editor, and is not nested inside build() — // can clear the timers and the beforeunload warning. build() assigns // into these; it doesn't re-declare them. var formDirty = false; var autosaveDebounceTimer = null; var autosaveInterval = null; /** Re-renders the step-group tab bar to reflect the current activeGroup. */ // Scrolls the given tab bar to the top of the viewport, below the sticky offset, so a switched-to tab // does not open off-screen. Deferred one frame: switching can shorten the page, and the browser may // clamp the scroll position before the measurement runs. function scrollTabBarIntoView(barId) { window.requestAnimationFrame(function () { var bar = document.getElementById(barId); if (!bar) return; var form = document.getElementById("wiktku-start-form"); var stabsH = form ? (parseFloat(form.style.getPropertyValue("--wiktku-stabs-h")) || 0) : 0; var siteHeaderH = form ? (parseFloat(form.style.getPropertyValue("--wiktku-siteheader-h")) || 0) : 0; var top = bar.getBoundingClientRect().top + window.pageYOffset - stabsH - siteHeaderH; $("html, body").animate({ scrollTop: Math.max(0, top) }, 200); }); } function renderGroupTabs() { var groupTabsDiv = $("#wiktku-group-tabs"); if (!groupTabsDiv.length) return; // One group is nothing to switch between. Done here rather than only where the bar is built, // because the form is rebuilt on language/part-of-speech changes and a bar left over from an // earlier build would otherwise sit there until refresh() next got round to hiding it. if (STEP_GROUPS.length < 2) { groupTabsDiv.remove(); return; } groupTabsDiv.empty(); $.each(STEP_GROUPS, function (i, g) { var tab = $("<span></span>") .addClass("wiktku-group-tab") .attr("data-group-idx", i) .text(g.label) .toggleClass("wiktku-tab-active", i === activeGroup); (function (idx) { tab.on("click", function () { if (idx === activeGroup) return; activeGroup = idx; renderGroupTabs(); renderBesenDinPicker(); refresh(); scrollTabBarIntoView("wiktku-group-tabs"); }); }(i)); groupTabsDiv.append(tab); }); } /** * Toggles a "needs attention" dot on any group tab that still has a * required, unfilled field hidden inside it — so switching tabs doesn't * leave the person wondering why "Bişîne" stays disabled. */ function updateGroupTabIndicators(values) { var tabs = $("#wiktku-group-tabs .wiktku-group-tab"); if (!tabs.length) return; $.each(STEP_GROUPS, function (i, g) { var missing = false; $.each(g.steps, function (_, sid) { var step = steps[sid]; if (!step || !step.required) return; var val = values[sid]; if (val === "" || (step.multiple && $.isArray(val) && val.join("") === "")) { missing = true; } }); tabs.eq(i).toggleClass("wiktku-tab-missing", missing && i !== activeGroup); }); } // "Beşên din" reveals one field at a time via a picker rather than // stacking them all. Per-section state, snapshotted in // saveCurrentSection() and restored in restoreSection() below, and // auto-seeded from any field that already has content (so a returning // draft doesn't hide something already filled in). var openedOtherSections = {}; var BESENDIN_SET = {}; $.each(BESENDIN_STEP_IDS, function (_, sid) { BESENDIN_SET[sid] = true; }); // The picker sits at the top of the form, above the senses, and is on screen whenever the form is. /** Re-renders the "Beşên din" picker to list only the not-yet-opened sections. */ function renderBesenDinPicker() { var wrap = $("#wiktku-besendin-picker"); if (!wrap.length) return; wrap.empty(); var pickerValues = getValues(); // Every section it offers belongs to a part-of-speech section, so there is nothing to add until // one is chosen. Checked here rather than only in refresh(): this function is also called on form // build and on section changes, and each of those would otherwise show the picker on its own. if (!pickerValues.pos) { wrap.hide(); return; } var unopened = $.grep(BESENDIN_STEP_IDS, function (sid) { if (openedOtherSections[sid]) return false; // Don't offer a section that would open onto nothing: several of // these are gated (Bi zaravayên din / Werger need lang=ku, most // need a pos). refresh() would hide it again straight away, so // picking it would read as a broken dropdown. var step = steps[sid]; if (step && typeof step.visible === "function") { try { if (!step.visible(pickerValues)) return false; } catch (e) { /* a step whose visible() needs DOM we lack yet — offer it */ } } return true; }); if (!unopened.length) { wrap.hide(); // everything's open — nothing left to pick return; } // Same voice and styling as a step's own question line, not a section heading -- this picker sits // between cards rather than being one. wrap.append($("<label class='wiktku-start-step-question'></label>").text("Binebeşek lê zêde bike")); var sel = $("<select class='wiktku-besendin-select'></select>"); sel.attr("data-placeholder", "— binebeşên din hilbijêre —"); sel.append($("<option value=''>— binebeşên din hilbijêre —</option>")); $.each(unopened, function (_, sid) { var step = steps[sid]; if (!step) return; sel.append($("<option></option>").val(sid).text(capitalize(step.name))); }); sel.on("change", function () { var sid = $(this).val(); if (!sid) return; openedOtherSections[sid] = true; // Newly opened section moves right under the picker, ahead of // whatever's already open, instead of wherever it happened to // sit in the fixed step order. $("#wiktku-start-" + sid).insertAfter(wrap); renderBesenDinPicker(); refresh(); }); wrap.append(sel); initSelects(wrap); wrap.show(); } /** Marks any BESENDIN_STEP_IDS field that already has real content as * "opened", so restoring a draft doesn't bury filled-in sections behind * the picker. Reuses each step's own output() as the emptiness check — * it already knows what "empty" means per field, and it reads the DOM * the restore just populated. Call this AFTER the DOM has the restored * values in it. */ function seedOpenedFromContent() { var values = getValues(); $.each(BESENDIN_STEP_IDS, function (_, sid) { if (openedOtherSections[sid]) return; var step = steps[sid]; if (!step || typeof step.output !== "function") return; try { if (step.output(values)) openedOtherSections[sid] = true; } catch (e) { /* a step whose output() needs more state than we have yet — skip it */ } }); } function snapshotDfn() { return steps.dfn.getSenses($("#wiktku-start-dfn")); } function getWikitextForCurrentSection(callback) { var values = getValues(); var entry = steps.pos.output(values); // Bilêvkirin is NOT emitted here — it is one shared field for the // whole word, so emitting it per section duplicated it. getWikitext() // prints it once, above the POS sections. steps.dfn.getOutput(values, function (output) { entry += output; entry += steps.tewandin.output(values); entry += steps.bikaranin.output(values); entry += steps.alfabeyendin.output(values); entry += steps.formalt.output(values); entry += steps.hevmane.output(values); entry += steps.dijmane.output(values); entry += steps.jornav.output(values); entry += steps.jernav.output(values); entry += steps.je.output(values); entry += steps.tekildar.output(values); entry += steps.etym.output(values); entry += steps.dunde.output(values); entry += steps.binereherwiha.output(values); entry += steps.zarava.output(values); entry += steps.trans.output(values); entry += steps.cavkani.output(values); entry += steps.xwendin.output(values); callback(entry, values); }); } // Two sections of the same part of speech would otherwise both be "=== Rengdêr ===", which makes them // indistinguishable on the page, in the table of contents and in every link that targets one. Run over // the finished sections rather than guessed from the pos alone, so "Rengdêr" and "Formeke rengdêrê" // -- different headings from the same pos -- are correctly left alone. function numberRepeatedHeadings(results) { var headingRE = /^(===\s*)([^=\n]+?)(\s*===)$/m; var counts = {}; $.each(results, function (_, text) { var m = headingRE.exec(text || ""); if (m) counts[m[2]] = (counts[m[2]] || 0) + 1; }); var seen = {}; $.each(results, function (i, text) { var m = headingRE.exec(text || ""); if (!m || counts[m[2]] < 2) { return; } seen[m[2]] = (seen[m[2]] || 0) + 1; results[i] = text.replace(headingRE, m[1] + m[2] + " " + seen[m[2]] + m[3]); }); } function getWikitextAllSections(callback) { saveCurrentSection(); var results = new Array(sections.length); var sharedValues = getValues(); // lang/pron are shared function processSection(i) { // Each hop through here can be reached asynchronously: the dfn // step waits on mw.loader.using("ext.gadget.linkify") before // calling back, so from that point on the submit handler's own // try/catch is no longer on the stack. Guard every hop instead, // or a throw after linkify loads vanishes into an unhandled // rejection and the form just sits there looking busy. try { if (i >= sections.length) { // All done — restore original active section. restoreSection(sections[activeSection]); refresh(); numberRepeatedHeadings(results); callback(results, sharedValues); return; } restoreSection(sections[i]); refresh(); getWikitextForCurrentSection(function (sectionText) { results[i] = sectionText; processSection(i + 1); }); } catch (e) { if (typeof onSubmitError === "function") onSubmitError(e); else throw e; } } processSection(0); } // Assembles wikitext for all sections plus the shared lang header. function getWikitext(callback) { getWikitextAllSections(function (sectionTexts, sharedValues) { var bnrh = steps.bnrh.output(sharedValues); var entry = steps.lang.output(sharedValues); // One pronunciation per word, printed once between the language // header and the POS sections — see steps.pron.output. entry += steps.pron.output(sharedValues); entry += sectionTexts.join("\n"); // Reconstruction-namespace pages (ns 108) mark the language // section with {{jinûvesazkirî|langcode}} directly above it — // only prepended when generating fresh wikitext, same as h-bnr // below (addLangMode handles an existing page separately). if (!addLangMode && ns === 108) entry = "{{jinûvesazkirî|" + sharedValues.lang + "}}\n" + entry; // h-bnr goes before the language section — only prepended when // generating fresh wikitext (not addLangMode, which handles it separately). if (!addLangMode && bnrh) entry = bnrh + entry; // Edit summary uses section 0's values. Read from the saved snapshot // (not the live DOM) so it doesn't mix section 0's pos with whichever // section happens to be active/restored at this point. var dfnSenses = (sections[0] && sections[0].dfn) || []; var dfnSummary = dfnSenses.map(function (s) { if (s.mane) return s.mane; // Template sense (e.g. {{guharto|ku|somepage}}) — same // construction as the tplCall built in getOutput, so the // summary shows what actually gets written to the entry // instead of going blank. if (s.tpl && s.tplTitle) { return s.tplNoLang ? "{{" + s.tpl + "|" + s.tplTitle + "}}" : "{{" + s.tpl + "|" + sharedValues.lang + "|" + s.tplTitle + "}}"; } return ""; }).filter(Boolean).join("; "); var summary = "([[Mediawiki:Gadget-start.js|WF:NÛ]] v.2.0) +" + sharedValues.lang + ":" + sections[0].pos + ":" + dfnSummary; var summaryField = document.getElementById("wpSummary"); if (addLangMode && summaryField.value) { summary = summaryField.value + " " + summary; } summaryField.value = summary; callback(entry); }); } var continueBtn; // Set by build() to its submitFailed(). Lives out here because the // wikitext-assembly functions below are outside build()'s closure but // still need a way to report a failure to the person rather than dying // silently in the console. var onSubmitError = null; // Whether Mane/Bi zaravayên din/Werger has anything in it — kept // outside refresh() so the click handler can read the latest value. function getValuesForStep(stepId) { var fields = $("#wiktku-start-" + stepId + " .wiktku-start-step-field"); var step = steps[stepId]; if (step.keys) { var values = {}; fields.each(function (i, field) { var key = $(field).prevAll(".wiktku-start-step-keyfield").val(); if (key) values[key] = $(field).val(); }); return values; } else if (step.multiple) { return fields.map(function () { return $(this).val(); }).get(); } return fields.val(); } function getValues() { var values = {}; $.each(steps, function (stepId) { values[stepId] = getValuesForStep(stepId); }); return values; } function populateMenus(inputs, choices, disabledValues, placeholderText) { if (!disabledValues) disabledValues = {}; var vals = inputs.map(function (i, input) { return $(input).val(); }); inputs.empty(); inputs.append($("<option></option>").text(placeholderText || "")); $.each(choices, function (j, choice) { // Append the language code in parentheses so that typing a code // (e.g. "ku") in the search box still finds the language name. var displayText = capitalize(choice.label); if (choice.sortKey !== undefined) { // This is a language choice {code, label, sortKey} — show code. displayText = capitalize(choice.label) + " (" + choice.code + ")"; } var option = $(mw.html.element("option", { value: choice.code }, displayText)); if (disabledValues[choice.code]) option.prop("disabled", true); inputs.append(option); }); inputs.val(function (i) { return vals[i]; }); } // Keys to disable for a key menu: already-taken or explicitly disabled. function getDisabledKeys(step) { var occupiedKeys = {}; $("#wiktku-start-" + step.id + " .wiktku-start-step-keyfield").each(function () { occupiedKeys[$(this).val()] = true; }); if (step.disabledKeys) $.extend(occupiedKeys, step.disabledKeys); return occupiedKeys; } function buildKeyInput(step) { if (!step.keys) return undefined; var input = $(mw.html.element("select", { "class": "wiktku-start-step-keyfield wiktku-start-narrow" })); input.attr("data-placeholder", "Hilbijêre " + step.keyName); populateMenus(input, step.keys, getDisabledKeys(step)); // Keep the key menus mutually exclusive: whatever one row selects is // disabled in every other row. Recomputed from the DOM on each change. input.on("change", function () { var all = $("#wiktku-start-" + step.id + " .wiktku-start-step-keyfield"); var taken = {}; all.each(function () { var v = $(this).val(); if (v) taken[v] = true; }); all.not(this).each(function () { var self = $(this), own = self.val(); self.find("option").each(function () { var code = $(this).val(); if (code) $(this).prop("disabled", !!taken[code] && code !== own); }); self.trigger("wiktku:refresh"); }); }); // Return the wrapper; the select lives inside it and still fires change. return wrapNativeSelect(input); } // Adds another text field to the list if the last one now has text. function refreshForTextField(currentInput, step) { currentInput = $(currentInput); var value = currentInput.val(); var item = currentInput.parent(); if (value && !item.next("li").length) { var nextInput = $("<textarea></textarea>") .attr("name", "wiktku-field-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("class", currentInput.attr("class")) .attr("rows", currentInput.attr("rows") || 1); if (step.placeholder) { nextInput.attr("placeholder", typeof step.placeholder === "function" ? step.placeholder() : step.placeholder); } nextInput.on("input", function (evt) { refreshForTextField(evt.target, step); }); var nextKeyInput = buildKeyInput(step); var nextItem = step.modifiers ? makeModifierLi(nextInput).prepend(nextKeyInput || "") : $("<li></li>").append(nextKeyInput).append(nextInput); item.parent().append(nextItem); if (nextKeyInput) initSelects(nextItem); } var canContinue = !continueBtn.isDisabled(); if ((value && !canContinue) || (!value && canContinue)) { refresh(step); } } function prepareInput(input, step) { if (!input) return; if (step.required) input.prop('required', true); input.change(step, refresh); } // Shows/hides steps based on whether their prerequisites are met, and // updates placeholders where applicable. function refresh(evt) { var values = getValues(); var changedStep = evt && evt.data; var canContinue = true; // Re-filter template options in every sense block whenever lang changes. $("#wiktku-start-dfn .wiktku-dfn-tpl-select").each(function () { var fn = $(this).data("updateOptions"); if (typeof fn === "function") fn(); }); // Re-filter the pronunciation template dropdown when language changes -- etym/cavkani's lists don't depend on lang, so they're only built once at data load instead of on every refresh(). steps.pron.refreshTplSelect(); // Re-render any step's question label that depends on current values // (e.g. dfn's Werger-section reminder, which only applies to ku). $.each(steps, function (stepId, step) { if (typeof step.question_fmt === "function") { $("#wiktku-start-" + stepId + " .wiktku-start-step-question") .text(step.question_fmt(values)); } }); // Pos's definition/example text, right under its dropdown — see // steps.pos.example and the .wiktku-start-pos-example build above. $("#wiktku-start-pos .wiktku-start-pos-example").text(steps.pos.example(values)); // Sync every sense block's mane-box mode (plain mane vs tercime+îzah) // to the current language, in case lang changes after blocks exist. // Same pass also re-syncs Dever/Şîwe's default open/closed state // (open for ku, closed otherwise — see updateDeverOpenState) and // reveals any sub-field group that has ended up with content in it // while still behind the chip row (a draft, a restore). $("#wiktku-start-dfn li.wiktku-dfn-sense").each(function () { var updateMane = $(this).data("updateManeMode"); if (updateMane) updateMane(); var updateDeverOpen = $(this).data("updateDeverOpenState"); if (updateDeverOpen) updateDeverOpen(); var syncPicks = $(this).data("syncSensePicks"); if (syncPicks) syncPicks(); }); // Sync tr/t sub-fields in every nimûne item to the current language. // tr shown when language needs transliteration; t once a language is chosen. (function () { var lang = values.lang || ""; var showTr = needsTransliteration(lang); var showT = !!lang; $("#wiktku-start-dfn .wiktku-dfn-nimune-list li.wiktku-dfn-nimune-li").each(function () { var li = $(this); // Ensure the sub-wrapper exists when we need it. var subWrap = li.find(".wiktku-dfn-nimune-sub"); // tr field var trTa = li.find(".wiktku-dfn-nimune-tr"); if (showTr && !trTa.length) { if (!subWrap.length) { subWrap = $("<div></div>").addClass("wiktku-dfn-nimune-sub"); li.append(subWrap); } trTa = $("<textarea></textarea>") .attr("name", "wiktku-dfn-nimune-tr-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("rows", 5) .attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-tr") .attr("placeholder", "Transliterasyon…"); subWrap.prepend(trTa); } else if (!showTr && trTa.length) { trTa.remove(); } // t field var tTa = li.find(".wiktku-dfn-nimune-t"); if (showT && !tTa.length) { if (!subWrap.length) { subWrap = $("<div></div>").addClass("wiktku-dfn-nimune-sub"); li.append(subWrap); } tTa = $("<textarea></textarea>") .attr("name", "wiktku-dfn-nimune-t-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("rows", 5) .attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-t"); subWrap.append(tTa); } else if (!showT && tTa.length) { tTa.remove(); } tTa.attr("placeholder", nimuneTranslationPrompt(lang)); // Remove the wrapper if it ended up empty. subWrap = li.find(".wiktku-dfn-nimune-sub"); if (subWrap.length && !subWrap.children().length) subWrap.remove(); }); }()); // Re-sync the "<label> bi <ziman>…" placeholder on every always-visible // sub-field list (etîket, kategorî, hevmane, …), since the language may // not have been chosen yet when these lists were first built, or may // have changed since. $("#wiktku-start-dfn .wiktku-dfn-sublist").each(function () { var upd = $(this).data("updatePlaceholders"); if (typeof upd === "function") upd(); }); // Same idea for word-list steps (jê, têkildar, binêre herwiha, …) // whose placeholder is a function of the current language. $.each(steps, function (stepId, step) { if (typeof step.placeholder === "function") { $("#wiktku-start-" + stepId + " textarea.wiktku-start-step-field") .attr("placeholder", step.placeholder()); } }); // Sync tr/t sub-fields in every jêder citation to the current language. // tr shown when language needs transliteration; t shown when lang !== "ku". (function () { var lang = values.lang || ""; var showTr = needsTransliteration(lang); var showT = lang && lang !== "ku"; $("#wiktku-start-dfn .wiktku-dfn-jeder-list li.wiktku-dfn-jeder-li").each(function () { var li = $(this); // Ensure the sub-wrapper exists when we need it. It must sit right // after the quote textarea and before the source selector. var subWrap = li.find(".wiktku-dfn-quote-sub"); // tr field var trTa = li.find(".wiktku-dfn-nimune-quote-tr"); if (showTr && !trTa.length) { if (!subWrap.length) { subWrap = $("<div></div>").addClass("wiktku-dfn-quote-sub"); // Insert after the quote textarea's resize-wrap (if the manual // resize handle has already been attached to it), not after the // bare textarea -- otherwise this lands *inside* that wrap, // between the textarea and its own grip, breaking jêgirtin's // handle and nesting the tr/t boxes inside it. var quoteTa = li.find(".wiktku-dfn-nimune-quote"); var quoteResizeWrap = quoteTa.closest(".wiktku-resize-wrap"); (quoteResizeWrap.length ? quoteResizeWrap : quoteTa).after(subWrap); } trTa = $("<textarea></textarea>") .attr("name", "wiktku-dfn-nimune-quote-tr-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("rows", 2) .attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote-tr") .attr("placeholder", "Transliterasyona jêgirtinê…"); subWrap.prepend(trTa); } else if (!showTr && trTa.length) { trTa.remove(); } // t field var tTa = li.find(".wiktku-dfn-nimune-quote-t"); if (showT && !tTa.length) { if (!subWrap.length) { subWrap = $("<div></div>").addClass("wiktku-dfn-quote-sub"); // See the tr-field branch above for why this targets the // resize-wrap instead of the bare textarea. var quoteTa2 = li.find(".wiktku-dfn-nimune-quote"); var quoteResizeWrap2 = quoteTa2.closest(".wiktku-resize-wrap"); (quoteResizeWrap2.length ? quoteResizeWrap2 : quoteTa2).after(subWrap); } tTa = $("<textarea></textarea>") .attr("name", "wiktku-dfn-nimune-quote-t-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("rows", 2) .attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote-t") .attr("placeholder", "Tercimeya jêgirtinê bi kurmancî…"); subWrap.append(tTa); } else if (!showT && tTa.length) { tTa.remove(); } // Remove the wrapper if it ended up empty. subWrap = li.find(".wiktku-dfn-quote-sub"); if (subWrap.length && !subWrap.children().length) subWrap.remove(); }); }()); $.each(steps, function (stepId, step) { var groupIdx = stepIdToGroup[stepId]; var hiddenByTab = (groupIdx !== undefined && groupIdx !== activeGroup) || // Beşên din picker: stays hidden even while its tab is // active until explicitly opened from the dropdown. (BESENDIN_SET[stepId] && !openedOtherSections[stepId]); // Bilêvkirin is its own section now, so the two views are // mutually exclusive: on it, only pron shows (plus Ziman, which // is shared and stays above every section); off it, pron is // hidden no matter which tab is active. pron is deliberately // ungrouped — stepIdToGroup has no entry for it — so without // this branch it would show on all four tabs at once. if (stepId === "pron") { hiddenByTab = hiddenByTab || !viewingPron; } else if (viewingPron && stepId !== "lang") { hiddenByTab = true; } if (hiddenByTab || (typeof(step.visible) === "function" && !step.visible(values))) { $("#wiktku-start-" + stepId).hide(); } else { var stepDiv = $("#wiktku-start-" + stepId); if (changedStep !== step && typeof(step.build) === "function") { prepareInput(step.build(stepDiv, values), step); } // After build, sync the posparams helper widgets whenever pos or lang changes. if (step.id === "posparams" && typeof step.syncHelpers === "function") { step.syncHelpers(stepDiv); } var input = stepDiv.find(".wiktku-start-step-field"); if (typeof(step.example) === "function") { // .find() above already reaches nested fields (e.g. the dfn // step's mane textareas inside each sense block), so one // assignment covers every step; dfn keeps its own fallback. var exampleText = step.example(values) || (step.id === "dfn" ? "Mane…" : ""); input.attr("placeholder", exampleText); } stepDiv.slideDown(); } if (step.required && (values[stepId] === "" || (step.multiple && values[stepId].join("") === ""))) { canContinue = false; } }); // One group means no bar to show; it only appears again if STEP_GROUPS ever grows back. $("#wiktku-group-tabs").toggle(STEP_GROUPS.length > 1 && !!values.pos && !viewingPron); // Bilêvkirin hides the picker outright; otherwise renderBesenDinPicker() decides, showing itself // only once a part of speech is chosen and there are sections left to offer. The decision lives // there rather than here because that function has other callers, which would each need the same // test. var besenDinActive = !viewingPron; if (!besenDinActive) { $("#wiktku-besendin-picker").hide(); } // The option list is filtered by each step's visible(), which depends // on lang/pos — so it has to be rebuilt when those change, not just // when a section is opened. Safe to call from here: rendering the // picker doesn't itself call refresh(), only picking an option does. if (besenDinActive) renderBesenDinPicker(); updateGroupTabIndicators(values); // Ziman and Cureyê rêzimanî are all it takes: an entry with a heading and a template but no // definition yet is a skeleton somebody means to come back to, not a mistake to be blocked. continueBtn.setDisabled(!canContinue); } function destroy() { if (autosaveDebounceTimer) clearTimeout(autosaveDebounceTimer); if (autosaveInterval) clearInterval(autosaveInterval); formDirty = false; $(window).off("beforeunload.wiktkuUnsaved"); $("#wiktku-start-form, .noafterwizard").slideUp(function () { $(this).remove(); }); $(".noarticletext .nowizard").slideDown(); // #editform was kept laid-out but off-screen (see build()) instead of // display:none, precisely so the editor never had to size itself // against a zero-width/height box. Just bring it back on-screen — // no slideDown needed since it was never actually collapsed. $("#editform").removeClass("wiktku-offscreen"); // Belt-and-braces: if WikiEditor/CodeMirror/mobile's auto-grow still // cached a stale size from some earlier point, nudge them to // recalculate now that the box is genuinely visible. $(window).trigger("resize"); if (window.CodeMirror) { var cm = $("#wpTextbox1").data("CodeMirror") || $("#wpTextbox1").data("cmInstance"); if (cm && typeof cm.refresh === "function") cm.refresh(); } $("#ca-edit").removeClass("selected"); $("#ca-wiktku-nowizard").addClass("selected"); } // Priority order for Kurdish dialects and navz. // Codes listed here come before all other languages; unlisted codes // are sorted alphabetically by their display name via localeCompare. var LANG_PRIORITY = ["navz", "ku", "hac", "sdh", "lki", "ckb", "zza"]; // Inserts a new "== {{ziman|code}} ==" section at the correct sorted // position: navz, then ku/hac/sdh/lki/ckb/zza, then alphabetical by name. function insertNewLangSection(existing, wikitext) { var newCodeMatch = wikitext.match(new RegExp("^\n?" + langHeaderSource("[^}]+"))); var newCode = newCodeMatch ? newCodeMatch[1] : ""; var sections = []; var preamble = ""; var anyHeaderSrc = "\n?" + langHeaderSource("[^}]+"); var firstMatch = new RegExp("^" + anyHeaderSrc, "m").exec(existing); if (!firstMatch) { // No existing lang sections — just write the new section. // Add \n\n only when there is real non-whitespace content above. var trimmed = existing.replace(/\s+$/, ""); return trimmed ? trimmed + "\n\n" + wikitext : wikitext; } // Everything before the first == {{ziman|...}} == is the preamble. var firstSectionStart = firstMatch.index; // Walk back to the start of that line. var lineStart = existing.lastIndexOf("\n", firstSectionStart - 1); var preambleEnd = lineStart !== -1 ? lineStart + 1 : 0; preamble = existing.slice(0, preambleEnd); // Parse each language section: header line + everything until the next header. var body = existing.slice(preambleEnd); // (?![\s\S]) matches only true end-of-string, since a bare "$" with // /m would stop at the section's first line break instead. var langSectionRe = new RegExp( "^(" + anyHeaderSrc + "\\n?)([\\s\\S]*?)(?=\\n" + anyHeaderSrc + "|(?![\\s\\S]))", "gm" ); var m; while ((m = langSectionRe.exec(body)) !== null) { sections.push({ code: m[2], content: m[0] // full text of this section including header }); } // Determine sort rank: priority list first (by index), then alphabetical. function getLangName(code) { var entry = langsByCode[code]; return entry ? entry.label : code; } function sortRank(code) { var idx = LANG_PRIORITY.indexOf(code); return idx !== -1 ? idx : LANG_PRIORITY.length; } function compareLangs(a, b) { var ra = sortRank(a), rb = sortRank(b); if (ra !== rb) return ra - rb; // Both are outside priority list — sort by display name. return getLangName(a).localeCompare(getLangName(b), userLang); } // Insert the new section into the sorted list. var newSection = { code: newCode, content: wikitext }; var inserted = false; for (var i = 0; i < sections.length; i++) { if (compareLangs(newCode, sections[i].code) < 0) { sections.splice(i, 0, newSection); inserted = true; break; } } if (!inserted) sections.push(newSection); // Re-join: preamble + sections, each separated by exactly \n\n. // Trim trailing whitespace off each section's content first so we // control the spacing precisely. var joined = sections.map(function (s) { return s.content.replace(/\s+$/, ""); }).join("\n\n"); // Only add a separator after the preamble when it has real content. var preambleTrimmed = preamble.replace(/\s+$/, ""); var prefix = preambleTrimmed ? preambleTrimmed + "\n\n" : ""; return prefix + joined + "\n"; } // What [[MediaWiki:Gadget-wikt.start-wizard/form.js]] uses of this closure. What changes after loading is read and set // through getters and setters on the same variables. function formContext() { var ctx = { PER_SECTION_STEP_IDS: PER_SECTION_STEP_IDS, STEP_GROUPS: STEP_GROUPS, WIKTKU_SUGGESTIONS_KEY_PREFIX: WIKTKU_SUGGESTIONS_KEY_PREFIX, addLangMode: addLangMode, buildKeyInput: buildKeyInput, capitalize: capitalize, destroy: destroy, findLangHeader: findLangHeader, formCache: formCache, getValues: getValues, getValuesForStep: getValuesForStep, getWikitext: getWikitext, highlightLinebreak: highlightLinebreak, initSelects: initSelects, insertNewLangSection: insertNewLangSection, isWordGroupCls: isWordGroupCls, langHeaderSource: langHeaderSource, makeModifierLi: makeModifierLi, makeTrashButton: makeTrashButton, ordered_steps: ordered_steps, populateMenus: populateMenus, prepareInput: prepareInput, refresh: refresh, refreshForTextField: refreshForTextField, renderBesenDinPicker: renderBesenDinPicker, renderGroupTabs: renderGroupTabs, scrollTabBarIntoView: scrollTabBarIntoView, seedOpenedFromContent: seedOpenedFromContent, snapshotDfn: snapshotDfn, splitTercimeIzah: splitTercimeIzah, stepIdToGroup: stepIdToGroup, steps: steps, wiktkuCite: wiktkuCite, wiktkuEdit: wiktkuEdit, wiktkuInitManualResize: wiktkuInitManualResize, wiktkuLangs: wiktkuLangs, wiktkuNotify: wiktkuNotify, wiktkuPosparams: wiktkuPosparams, wiktkuSavePrefs: wiktkuSavePrefs, wiktkuUtil: wiktkuUtil, wiktkuWikitext: wiktkuWikitext, wordGroups: wordGroups }; Object.defineProperties(ctx, { activeSection: { get: function () { return activeSection; }, set: function (value) { activeSection = value; } }, autosaveDebounceTimer: { get: function () { return autosaveDebounceTimer; }, set: function (value) { autosaveDebounceTimer = value; } }, autosaveInterval: { get: function () { return autosaveInterval; }, set: function (value) { autosaveInterval = value; } }, continueBtn: { get: function () { return continueBtn; }, set: function (value) { continueBtn = value; } }, formDirty: { get: function () { return formDirty; }, set: function (value) { formDirty = value; } }, onSubmitError: { get: function () { return onSubmitError; }, set: function (value) { onSubmitError = value; } }, openedOtherSections: { get: function () { return openedOtherSections; }, set: function (value) { openedOtherSections = value; } }, sections: { get: function () { return sections; }, set: function (value) { sections = value; } }, viewingPron: { get: function () { return viewingPron; }, set: function (value) { viewingPron = value; } }, wiktkuMergeLanguages: { get: function () { return wiktkuMergeLanguages; } }, wiktkuPrefs: { get: function () { return wiktkuPrefs; }, set: function (value) { wiktkuPrefs = value; } } }); return ctx; } var wizardForm = require("./wikt.start-wizard/form.js")(formContext()); var build = wizardForm.build; var restoreSection = wizardForm.restoreSection; var saveCurrentSection = wizardForm.saveCurrentSection; // Only oojs-ui-core blocks the initial render now — icons-moderation // (the trash glyph) is fetched lazily by makeTrashButton() instead, so // slow connections aren't held up loading it before anyone needs it. mw.loader.using(["oojs-ui-core"], function () { function wiktkuProceedWithData(wiktkuData) { var d = wiktkuData || {}; // Populate metadata. if (d.metadata) $.extend(metadata, d.metadata); // Populate gender labels. if (d.genderLabels) $.extend(GENDER_BASE_LABELS, d.genderLabels); if (d.senseGroups) SENSE_GROUPS = d.senseGroups; // Headings for the page-level word lists, from the same table DefSideBoxes reads. $.each(d.pageSubsections || [], function (_, sub) { if (sub.kind !== "wordlist" || !steps[sub.id]) return; steps[sub.id].header = sub.header; steps[sub.id].name = decapitalize(sub.header); }); if (d.wordlistTemplates && d.wordlistTemplates.length) WORDLIST_TEMPLATE = d.wordlistTemplates[0]; if (d.deverQuickPicks) DEVER_QUICK_PICKS = d.deverQuickPicks; // Populate lang choices (pinned list). if (d.langChoices) steps.lang.choices = choicesFromObject(d.langChoices, true); // Populate langsByCode and merge full language list from d.ziman. // d.ziman is a {code: name} object with all known languages. $.each(steps.lang.choices, function (i, choice) { if (choice.sortKey === undefined) choice.sortKey = choice.label; langsByCode[choice.code] = choice; }); // Only the ~300 languages of ziman-core.json are merged in at load; the other ~3400 arrive // through wiktkuMergeLanguages() the first time somebody searches for one of them. The pinned // choices are kept aside so a second merge rebuilds the list instead of appending to it. var wiktkuPinnedChoices = steps.lang.choices; wiktkuMergeLanguages = function (zimanNames) { var pinnedCodes = {}; $.each(wiktkuPinnedChoices, function (i, c) { pinnedCodes[c.code] = true; }); var extraChoices = []; $.each(zimanNames || {}, function (code, name) { // A proto-language is never the language of an ordinary entry, but it is the whole point // of a reconstruction page, so ns 108 keeps them in the list. if (!isReconstruction && /-pro$/i.test(code)) return; if (/ziman[eê]\s+destan/i.test(name)) return; if (pinnedCodes[code]) return; var choice = { code: code, label: name, sortKey: name }; extraChoices.push(choice); langsByCode[code] = choice; }); // One Intl.Collator built once and reused for the whole sort, instead of localeCompare() rebuilding a collator on every pair -- same order, far faster over ~3700 entries. var langCollator = new Intl.Collator(userLang); extraChoices.sort(function (a, b) { return langCollator.compare(a.sortKey, b.sortKey); }); steps.lang.choices = wiktkuPinnedChoices.concat(extraChoices); // The translation/dialect steps reference any language, so they follow the same list. steps.trans.keys = steps.lang.choices; steps.dunde.keys = steps.lang.choices; steps.zarava.keys = getZaravaKeys(steps.lang.choices); return steps.lang.choices; }; wiktkuMergeLanguages(wiktkuLangs.names()); // Keep the full merged list around for trans/dunde/zarava — those // should still be able to reference any language even when the // Tercîh panel restricts the main "ziman" step below — then, if // the user set a restriction there, narrow steps.lang.choices // itself to just those codes. var wiktkuFullLangChoices = steps.lang.choices; if (wiktkuPrefs.langCodes && wiktkuPrefs.langCodes.length) { var wiktkuAllowedLangSet = {}; $.each(wiktkuPrefs.langCodes, function (i, code) { wiktkuAllowedLangSet[code] = true; }); var wiktkuRestrictedChoices = $.grep(wiktkuFullLangChoices, function (c) { return !!wiktkuAllowedLangSet[c.code]; }); // Fall back to the full list instead of showing an empty // dropdown if none of the saved codes matched anything // (typo'd code, data-main.json changed since, …). steps.lang.choices = wiktkuRestrictedChoices.length ? wiktkuRestrictedChoices : wiktkuFullLangChoices; } // Populate POS choices — also a plain {code: label} object, from data-sections.json. if (d.posChoices) steps.pos.choices = choicesFromObject(d.posChoices, false); // Populate template definitions for the dfn sense-block builder. if (d.tplDefs) steps.dfn.tplDefs = d.tplDefs; // Populate pos placeholder examples. if (d.posExamples) steps.pos.posExamples = d.posExamples; // Populate citation template definitions for the quote widget. if (d.jederDefs) steps.dfn.jederDefs = d.jederDefs; // Populate pronunciation and etymology template insert lists. if (d.pronTpls) { steps.pron.pronTpls = d.pronTpls; steps.pron.refreshTplSelect(); } if (d.etymTpls) { steps.etym.etymTpls = d.etymTpls; steps.etym.refreshTplSelect(); } // Sync trans/dunde/zarava keys with the full language list // (not the possibly Tercîh-restricted steps.lang.choices). steps.trans.keys = wiktkuFullLangChoices; steps.dunde.keys = wiktkuFullLangChoices; steps.zarava.keys = getZaravaKeys(wiktkuFullLangChoices); build(); } // The data ships inside wikt.edit, so it is already parsed and in memory. wiktkuProceedWithData(wiktkuEdit.data); }); }); //</nowiki> djoeuga2tvtqag73ejvh5gqnrr77ogq MediaWiki:Gadget-wikt.start-wizard-steps.js 8 1359513 6903104 6902289 2026-11-10T21:29:01Z Wikihez 7344 Script pages under 100 KB: part of the code moved to a second file; shared code in one place 6903104 javascript text/javascript // <nowiki> /* * Page: [[MediaWiki:Gadget-wikt.start-wizard-steps.js]] * * The step definitions of [[MediaWiki:Gadget-wikt.start-wizard.js]]: what each step asks, offers and writes. * A module of its own so that neither half passes the 100 KB the browser's module store keeps. The wizard * calls it with ctx, its own helpers and settings; the few settings it changes after loading * (DEVER_QUICK_PICKS, SENSE_GROUPS, catSrcTemplates, pageName, wiktkuLabelSets) are read through ctx as * they are at that moment. */ /* global wikt */ module.exports = function (ctx) { var addLangMode = ctx.addLangMode; var applyTplDescriptions = ctx.applyTplDescriptions; var bindMoreMenuCloser = ctx.bindMoreMenuCloser; var buildChipField = ctx.buildChipField; var buildCite = ctx.buildCite; var buildDeverTpl = ctx.buildDeverTpl; var buildEtiketTpl = ctx.buildEtiketTpl; var buildManeidTpl = ctx.buildManeidTpl; var buildSenseTpl = ctx.buildSenseTpl; var capitalize = ctx.capitalize; var findLangHeader = ctx.findLangHeader; var getLangLabel = ctx.getLangLabel; var getValues = ctx.getValues; var highlightLinebreak = ctx.highlightLinebreak; var initSelects = ctx.initSelects; var insertAtCursor = ctx.insertAtCursor; var insertLineSnippet = ctx.insertLineSnippet; var isReconstruction = ctx.isReconstruction; var isWordGroupCls = ctx.isWordGroupCls; var makeModifierLi = ctx.makeModifierLi; var makeTrashButton = ctx.makeTrashButton; var manePlaceholder = ctx.manePlaceholder; var markTemplateOptions = ctx.markTemplateOptions; var needsTransliteration = ctx.needsTransliteration; var nimuneTranslationPrompt = ctx.nimuneTranslationPrompt; var refresh = ctx.refresh; var senseHasExtraContent = ctx.senseHasExtraContent; var splitTercimeIzah = ctx.splitTercimeIzah; var stripWikilinks = ctx.stripWikilinks; var tplNameFromSnippet = ctx.tplNameFromSnippet; var wiktkuCite = ctx.wiktkuCite; var wiktkuEdit = ctx.wiktkuEdit; var wiktkuEnsureLabels = ctx.wiktkuEnsureLabels; var wiktkuLabelListeners = ctx.wiktkuLabelListeners; var wiktkuLabelSuggestions = ctx.wiktkuLabelSuggestions; var wiktkuPosparams = ctx.wiktkuPosparams; var wiktkuUtil = ctx.wiktkuUtil; var wiktkuWikitext = ctx.wiktkuWikitext; var wordGroups = ctx.wordGroups; var steps = { lang: { id: "lang", required: true, name: "ziman", question_fmt: '"' + ctx.pageName + '" bi çi zimanî ye?', // Populated from data-main.json (langChoices). choices: [], output: function (values) { return "== {{ziman|" + values.lang + "}} ==\n"; } }, pron: { id: "pron", name: "bilêvkirin", question_fmt: '"' + ctx.pageName + '" çawa tê telafûzkirin?', // pronTpls populated from data-main.json pronTpls: {}, visible: function (values) { if (!values.lang) return false; // addLangMode: skip pron only if this lang already has a section. if (addLangMode) { // Clearing the page makes existing content irrelevant. var clearPageCheck = $("#wiktku-clear-page-check"); if (clearPageCheck.length && clearPageCheck.prop("checked")) { return true; } var existing = $("#wpTextbox1").val() || ""; return !findLangHeader(existing, values.lang); } return true; }, // Fills the insert-dropdown from pronTpls, filtered by language. refreshTplSelect: function () { var sel = $("#wiktku-start-pron .wiktku-tpl-insert-select"); if (!sel.length) return; var lang = getValues().lang || ""; sel.find("option[value!='']").remove(); $.each(steps.pron.pronTpls, function (name, tpl) { // Filter: if tpl.langs exists, only show if lang is in that list; otherwise show always var allowed = !tpl.langs || tpl.langs.indexOf(lang) !== -1; if (allowed) { sel.append($("<option></option>") .val(tpl.snippet) .text(name + " — " + tpl.desc) .attr("data-nolang", tpl.noLang ? "1" : "")); } }); markTemplateOptions(sel, function (opt) { return tplNameFromSnippet(opt.val()); }); // Tell the picker the option list changed. sel.trigger("wiktku:refresh"); }, build: function (stepDiv) { if (stepDiv.find(".wiktku-pron-wrap").length) return null; var wrap = $("<div class='wiktku-pron-wrap'></div>"); // Reassures the user this field is optional — the dropdown // below writes the template for them, no syntax needed up front. wrap.append( $("<p class='wiktku-step-hint'></p>").text( "Ev qism ne mecbûrî ye. Heke tu nizanî kîjan şablonê hilbijêrî, vala bihêle." ) ); var ta = $("<textarea></textarea>") .attr("name", "wiktku-pron-" + $.now()) .attr("class", "wiktku-start-step-field wiktku-pron-text") .attr("rows", 4) .attr("placeholder", "Nimûne: {{IPA|ku|/pɛɪ̯n/}} yan {{kîte|ku|he|ce|ki|rin}}"); // Template insert dropdown — filtered by language. var sel = $("<select class='wiktku-tpl-insert-select'></select>"); sel.attr("data-placeholder", wiktkuUtil.msg.addTemplate); sel.append($("<option value=''>— şablonek lê zêde bike —</option>")); var lang = getValues().lang || ""; $.each(steps.pron.pronTpls, function (name, tpl) { // Filter: if tpl.langs exists, only show if lang is in that list; otherwise show always var allowed = !tpl.langs || tpl.langs.indexOf(lang) !== -1; if (allowed) { sel.append($("<option></option>") .val(tpl.snippet) .text(name + " — " + tpl.desc) .attr("data-nolang", tpl.noLang ? "1" : "")); } }); sel.on("change", function () { var snippet = $(this).val(); if (!snippet) return; var lang = getValues().lang || ""; // Check if the selected option has noLang flag var selectedOption = sel.find("option:selected"); var hasNoLang = selectedOption.attr("data-nolang") === "1"; if (!hasNoLang) { snippet = snippet.replace(/CODE/g, lang).replace(/SOURCELANG/g, "?"); insertLineSnippet(ta[0], snippet); $(this).val(""); // Clear the picker's search box sel.trigger("wiktku:refresh"); ta.focus(); return; } // The noLang templates (ku-IPA, ku-kîte, zza-IPA, zza-kîte) // auto-generate content from a Lua module reading the page // title, so show a live preview and require confirmation // before inserting. var tplLabel = selectedOption.text(); $(this).val(""); sel.trigger("wiktku:refresh"); steps.pron.showTplPreview(stepDiv, ta, snippet, tplLabel); }); wrap.append(sel).append(ta); stepDiv.append(wrap); // The palette lives in wikt.edit, shared with DefSideBoxes' own Bilêvkirin box. wrap.append(wiktkuUtil.buildIpaPalette({ insertText: function (text, back) { insertAtCursor(ta[0], text); if (back) { var pos = ta[0].selectionStart - back; ta[0].selectionStart = ta[0].selectionEnd = pos; } ta.focus(); } })); initSelects(wrap); return ta; }, // ── IPA character palette ji [[MediaWiki:Gadget-charinsert-core.js]]──────────── // Modelled on fr.wiktionary's: every character is a button that // drops itself in at the cursor. Two sub-tabs, because the full // IPA chart is ~180 symbols and roughly twenty of them ever // appear in Kurmancî — "IPA (kurdî)" opens by default and the // full chart is one click away for loanwords and other // languages. Grouped with a label down the left so it reads as // a chart rather than one long line of glyphs. /** Inline preview + confirm panel for a "noLang" (auto-content) * pronunciation template — fetches what the snippet expands to * for this page and only inserts it if the user confirms. */ showTplPreview: function (stepDiv, ta, snippet, tplLabel) { // Only one preview panel open at a time. stepDiv.find(".wiktku-pron-preview").remove(); var panel = $("<div class='wiktku-pron-preview'></div>"); panel.append($("<div class='wiktku-pron-preview-label'></div>") .text("Pêşdîtina " + tplLabel + ":")); var body = $("<div class='wiktku-pron-preview-body'></div>").text(wiktkuUtil.msg.loading); panel.append(body); var actions = $("<div class='wiktku-pron-preview-actions'></div>"); var confirmBtn = $("<button type='button' class='wiktku-pron-preview-confirm'>✓ Tevlî bike</button>") .prop("disabled", true); var cancelBtn = $("<button type='button' class='wiktku-pron-preview-cancel'></button>") .text("✕ " + wiktkuUtil.msg.cancel); actions.append(confirmBtn).append(cancelBtn); panel.append(actions); ta.before(panel); confirmBtn.on("click", function () { insertLineSnippet(ta[0], snippet); panel.remove(); ta.focus(); }); cancelBtn.on("click", function () { panel.remove(); }); // action=parse, not expandtemplates: the preview needs rendered HTML, not expanded // wikitext. The leading "* " is dropped so it renders as a line, not a bulleted list. new mw.Api().post({ action: "parse", title: ctx.pageName, text: snippet.replace(/^\*\s*/, ""), prop: "text", wrapoutputclass: "", disablelimitreport: 1, disableeditsection: 1, contentmodel: "wikitext", formatversion: 2 }).done(function (data) { var html = (data && data.parse && data.parse.text) || ""; if ($.trim($("<div>").html(html).text())) { body.html(html); // Category links render as nothing but still exist; // and any real link inside a preview should not // navigate away from a half-filled form. body.find("a").attr("target", "_blank").attr("rel", "noopener"); } else { body.text("(vala — ji bo vê rûpelê tiştek nehate çêkirin)"); } confirmBtn.prop("disabled", false); }).fail(function () { body.text("Pêşdîtin nehate barkirin — tu dikarî hîn jî wê tevlî bikî."); confirmBtn.prop("disabled", false); }); }, output: function () { var text = $.trim($("#wiktku-start-pron .wiktku-pron-text").val()); if (!text) return ""; var lines = text.split("\n"); var out = $.map(lines, function (line) { line = $.trim(line); if (!line) return undefined; return line; }).join("\n"); // "===" heading under Ziman and above the POS sections; the // field is shared across sections, so it's emitted once by // getWikitext() rather than per-POS-section (would otherwise // print the pronunciation twice for a two-POS entry). return "\n=== Bilêvkirin ===\n" + out + "\n"; } }, pos: { id: "pos", required: true, name: "Cureyê rêzimanî", question_fmt: 'Cureyê rêzimanî yê "' + ctx.pageName + '" çi ye?', visible: function (values) { return values.lang; }, // Populated from posChoices (data-sections.json) and posExamples (data-main.json). choices: [], // Definition of whichever cureyê rêzimanî is currently chosen, // shown as plain text right under the dropdown (see the // .wiktku-start-pos-example build/refresh() above). Kept clear of // the Mane textarea's placeholder, which answers "what to type // here" rather than "what this grammatical category means". posExamples: {}, example: function (values) { var pos = values.pos || ""; return steps.pos.posExamples[pos] || "Cureyê rêzimanî yê vê kelîmeyê hilbijêre — navdêr, lêker, rengdêr hwd."; }, output: function (values) { var pos = values.pos || ""; var capitalizedPos = capitalize(pos); // "form" opt-param (checked in posparams) marks this as a // form-of/inflected entry — the section title becomes // "Formeke <pos>ê" instead of the plain pos name. The // template call itself still uses the unchanged pos name. var isForm = $("#wiktku-start-posparams [data-pp-opt='form']").is(":checked"); var header = isForm ? "\n=== Formeke " + pos + "ê ===\n" : "\n=== " + capitalizedPos + " ===\n"; // Read the live wikitext from the posparams textarea. // Fall back to a plain {{pos|lang}} if the step hasn't been built yet. var tplLine = $.trim($("#wiktku-start-posparams .wiktku-posparams-text").val()); if (!tplLine) { tplLine = "{{" + pos + "|" + (values.lang || "") + "}}"; } // {{ku-tewîn-lk}} line for xwerû Kurmancî verbs, parsed straight out of tplLine (not the posparams input fields, which a cache/section restore doesn't repopulate) once niha/borî/transitivity are all present. var tewinLkLine = ""; if (pos === "lêker" && values.lang === "ku") { var niha = wiktkuWikitext.callParamValue(tplLine, "niha"); var borî = wiktkuWikitext.callParamValue(tplLine, "borî"); var lekturCode = wiktkuWikitext.callParamValue(tplLine, "c"); var cParts = lekturCode ? /^(?:([xphb])-)?(n?gh)$/.exec(lekturCode) : null; var lekturForm = cParts ? (cParts[1] || "") : ""; var lktransitivity = cParts ? cParts[2] : ""; var lkForm = lktransitivity === "gh" ? "gerguhêz" : (lktransitivity === "ngh" ? "negerguhêz" : ""); if (niha && borî && lekturForm === "x" && lkForm) { tewinLkLine = "{{ku-tewîn-lk|" + ctx.pageName + "|form=" + lkForm + "|niha=" + niha + "|borî=" + borî + "}}\n"; } } // Image line (between the conjugation line, if any, and the template call) var imageLine = steps.resim.output(values); return header + tewinLkLine + imageLine + tplLine + "\n"; } }, // cinsiyet (gender) — shown for navdêr/serenav only, with choices built // from metadata[lang].g since not every language has grammatical gender. cinsiyet: { id: "cinsiyet", name: "cinsiyet", question_fmt: 'Cinsiyeta "' + ctx.pageName + '" çi ye?', visible: function () { return false; }, // handled inside posparams // Single source of truth for the selected gender code. The visible // dropdown lives inside the posparams step (see steps.posparams), // but it must always read/write THIS hidden field — not a field of // its own — since getValuesForStep("cinsiyet") reads from here. build: function (stepDiv) { var field = stepDiv.find(".wiktku-start-step-field"); if (field.length) return field; field = $("<input type='hidden'>") .addClass("wiktku-start-step-field") .attr("id", "wiktku-start-cinsiyet-field") .val(""); stepDiv.append(field); return field; }, output: function () { return ""; } // consumed by posparams/pos.output }, // jimar (grammatical number) — same hidden-field pattern as cinsiyet // above, but writes into its own j= template parameter instead of // sharing c=. Shown for navdêr/serenav only, with the same choices // for every language — data-main.json's numberLabels (see syncHelpers). jimar: { id: "jimar", name: "jimar", question_fmt: 'Jimara "' + ctx.pageName + '" çi ye?', visible: function () { return false; }, // handled inside posparams build: function (stepDiv) { var field = stepDiv.find(".wiktku-start-step-field"); if (field.length) return field; field = $("<input type='hidden'>") .addClass("wiktku-start-step-field") .attr("id", "wiktku-start-jimar-field") .val(""); stepDiv.append(field); return field; }, output: function () { return ""; } // consumed by posparams/pos.output }, // Lêker transitivity — hidden step, handled inside posparams lektur: { id: "lektur", name: "Cureyê lêkerê (fiîlê)", question_fmt: "Cureyê lêkerê (fiîlê) çi ye?", visible: function () { return false; }, output: function () { return ""; } }, // tr — hidden step, handled inside posparams tr: { id: "tr", name: "transliterasyon", question_fmt: "Transliterasyona vê kelîmeyê", multiple: false, narrow: true, visible: function () { return false; } }, // The card itself lives in wikt.edit/posparams.js, shared with DefSideBoxes -- see there for the // fields and the per-language gating. This is only the wizard's side: which language and part of // speech it is for, and mirroring gender/number into the two hidden steps the wizard keeps. posparams: { id: "posparams", // Fused onto the "pos" card in styles.css so the two read as one question. The "xisûsiyet" // name is a no-CSS fallback: its h3 is hidden, and every field below is self-labelled. name: "xisûsiyet", question_fmt: "", card: null, visible: function (values) { return !!(values.lang && values.pos); }, build: function (stepDiv) { if (stepDiv.find(".wiktku-posparams-step").length) return null; var values = getValues(); steps.posparams.card = wiktkuPosparams.build({ lang: values.lang, pos: values.pos, onChange: refresh, // steps.cinsiyet/steps.jimar own these hidden fields; the card reports rather than // writes, so it needs no knowledge of them. onSync: function (cinsiyet, jimar) { $("#wiktku-start-cinsiyet-field").val(cinsiyet || ""); $("#wiktku-start-jimar-field").val(jimar || ""); } }); stepDiv.append(steps.posparams.card.$element); return null; }, rebuildTa: function () { if (steps.posparams.card) steps.posparams.card.rebuild(); }, /** Called from refresh() — re-gates every row for the current language/pos. */ syncHelpers: function () { if (!steps.posparams.card) return; var values = getValues(); steps.posparams.card.setContext(values.lang, values.pos); }, output: function () { return ""; } // pos.output reads the textarea directly }, dfn: { id: "dfn", name: "Mane", question_fmt: function (values) { var base = "Maneya kelîmeyê binivîse. Mane otomatîk tê lînkkirin!"; if (values.lang === "ku") { return base + " Ji bo tercimeyan beşa 'Werger' bi kar bîne!"; } return base; }, multiple: true, ordered: true, rows: 3, visible: function (values) { return values.lang && values.pos; }, // Builds one sense block (<li>): mane textarea, nimûne list, and // sub-lists for alternatîv/hevmane/dijmane/etc. buildSenseBlock: function () { var block = $("<li class='wiktku-dfn-sense'></li>"); // — Mane textarea (declared first so the template handler can reference it) — var dfnArea = $("<textarea></textarea>") .attr("name", "wiktku-dfn-mane-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("class", "wiktku-start-step-field wiktku-dfn-mane") .attr("rows", 6) .attr("placeholder", "Maneya vê kelîmeyê binivîse…"); // Nothing on keystrokes: only the Werger gloss list needs a live Mane edit, and rebuilding // it re-reads every sense. Deferred to blur; the wikitext is read fresh on output anyway. dfnArea.on("blur", function () { refresh(); }); // — Îzaha maneyî (optional second half of the mane) — // A non-Kurdish entry's gloss is a translation followed by {{îzah|…}} explaining it. The // mane box is always the mane box and the explanation opens under it when asked for, so // the pair reads as one gloss rather than as a different kind of field. getSenses() joins // the two back together, so every consumer downstream sees one gloss string. var izahArea = $("<textarea></textarea>") .attr("name", "wiktku-dfn-izah-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("class", "wiktku-dfn-subfield wiktku-dfn-izah") .attr("rows", 2) .attr("placeholder", "Îzaha maneyî… Ji bo school-a îngilîzî li vir 'cihê tehsîlgirtinê' binivîse"); var izahWrap = $("<div class='wiktku-dfn-izah-wrap'></div>") .append($("<span class='wiktku-dfn-sublabel'></span>") .append($("<span class='wiktku-dfn-sublabel-term'></span>").text("Îzaha maneyî")).append(": ")) .append(izahArea) .hide(); // Opening it from a restore, where the text decides rather than a click. var izahOpen = false; function setIzahOpen(open) { izahOpen = !!open; updateManeMode(); } block.data("setIzahOpen", setIzahOpen); izahArea.on("input", function () { // dfnArea drives the live preview and the auto-linker; the îzah is part of the gloss, // so a change to it has to reach them too. dfnArea.trigger("input"); }); izahArea.on("blur", function () { refresh(); }); // The mane box gives way to the template title box when a template is chosen; the îzah // goes with it, since a template gloss has no separate explanation. function updateManeMode() { var isTpl = !!tplSelect.val(); if (isTpl) { dfnArea.hide(); izahWrap.hide(); return; } dfnArea.show(); dfnArea.attr("placeholder", manePlaceholder(getValues().lang || "")); // {{îzah}} explains a translation, so it belongs to entries in another language; a // Kurdish sense is its own definition and is not offered one. A box that already holds // text stays on screen whatever the language -- getSenses() joins it into the gloss // either way, and hiding it would make that happen unseen. var lang = getValues().lang || ""; var offer = !!lang && lang !== "ku"; // A non-Kurdish gloss is a translation and its explanation, so the box opens with the // mane box rather than waiting to be asked for. var open = offer || izahOpen || $.trim(izahArea.val()) !== ""; izahWrap.toggle(open); } // Store on the block so refresh() can re-sync every block when // lang changes, and restoreDfn() can re-sync after restoring. block.data("updateManeMode", updateManeMode); // — Template selector (optional — replaces mane when chosen) — // Template definitions loaded from data-main.json. var tplDefs = steps.dfn.tplDefs || {}; // Cache of all non-placeholder option elements, so updateTplOptions // can detach/re-attach them without losing the full list. var _allTplOptions = []; var tplSelect = $(mw.html.element("select", { "class": "wiktku-dfn-tpl-select" })); tplSelect.attr("data-placeholder", wiktkuUtil.msg.searchTemplate); tplSelect.append($("<option value=''>— yan jî şablonek hilbijêre —</option>")); $.each(tplDefs, function (name, tpl) { // Shows "name — tpl.desc" when a tplDefs entry has a desc // in data-main.json, same pattern as steps.pron's template picker. var opt = $("<option></option>") .val(name) .text(tpl.desc ? name + " — " + tpl.desc : name) .attr("data-langs", tpl.langs ? tpl.langs.join(",") : "") .attr("data-nolang", tpl.noLang ? "1" : ""); tplSelect.append(opt); _allTplOptions.push(opt[0]); }); // Same status-dot / 🔗 / ⓘ row treatment as every other // template picker. This one carries the bare name in value=, // so the name mapper is the identity rather than a snippet // parse; descriptions come from each template's TemplateData. markTemplateOptions(tplSelect, function (opt) { return opt.val(); }); function updateTplOptions() { var lang = getValues().lang || ""; var currentVal = tplSelect.val(); var currentStillValid = false; // Detach all non-placeholder options, then re-attach only the visible // ones — native <select> won't reliably hide options any other way. tplSelect.find("option[value!='']").detach(); $.each(_allTplOptions, function (_, opt) { var allowed = $(opt).attr("data-langs"); var visible = !allowed || allowed.split(",").indexOf(lang) !== -1; if (visible) { tplSelect.append(opt); if ($(opt).val() === currentVal) currentStillValid = true; } }); // Options that were detached when the descriptions arrived // missed that pass, so describe whatever is attached now. // The cache is warm by this point, so this is synchronous. applyTplDescriptions(tplSelect); tplSelect.trigger("wiktku:refresh"); if (currentVal && !currentStillValid) { tplSelect.val(""); tplTitleInput.hide().val(""); updateManeMode(); } } // Store updater on the element so refresh() can call it for all blocks. tplSelect.data("updateOptions", updateTplOptions); var tplTitleInput = $("<input type='text'>") .addClass("wiktku-dfn-tpl-title wiktku-start-narrow") .attr("placeholder", "Navê rûpela hedefê…") .hide(); // Same reasoning as dfnArea above: refresh() (which rebuilds the // Werger/Bi zaravayên din gloss list) is deferred to blur // rather than run on every keystroke. tplTitleInput.on("blur", function () { refresh(); }); // Şablon senses have no plain gloss, so the word-list groups (hevmane, dijmane, formên // alternatîv…) make no sense for them — the class hides both those groups and their // entries in the "Hevmane û Bêhtir" menu, so they cannot be asked for either. Etîket, Kategorî, // Nimûne and Jêgirtin stay available. function updateFieldVisibility() { block.toggleClass("wiktku-sense-is-tpl", !!tplSelect.val()); } // Store on the block so restoreDfn() can re-sync visibility // after setting tplSelect's value. block.data("updateFieldVisibility", updateFieldVisibility); tplSelect.on("change", function () { var chosen = $(this).val(); if (chosen) { dfnArea.hide(); izahWrap.hide(); tplTitleInput.show().focus(); } else { tplTitleInput.hide().val(""); updateManeMode(); } updateFieldVisibility(); // Re-run refresh so the Werger step visibility updates. refresh(); }); // The template picker is an alternative to writing a // definition, so this quiet link sits above the Mane box // rather than getting buried under Dever/Etîket/Kategorî. // It only expands for the minority of senses that are a // pointer at another word (guharto, şaşnivîs, kevnbûyî) // rather than a meaning. var tplRow = $("<div></div>").addClass("wiktku-dfn-tpl-row") .append(tplSelect).append(" ").append(tplTitleInput) .hide(); var tplLink = $("<a href='#' class='wiktku-dfn-tpl-link'></a>") .text("Maneyê bi şablonan çêke (rastnivîs, şaşnivîs, awayekî din)"); tplLink.on("click", function (e) { e.preventDefault(); tplRow.show(); $(this).hide(); tplRow.find("input.wiktku-ac-input").first().focus(); }); /** Opens the picker without a click — used on restore, when a * sense already has a template and hiding it would make the * chosen value invisible. */ function revealTplRow() { tplRow.show(); tplLink.hide(); } block.data("revealTplRow", revealTplRow); // Dever/Şîwe, Etîket and Kategorî as chip+toggle fields, so a sense that leaves them empty // stays short. getOutput() folds Dever/Şîwe into {{dever|}} for ku, {{e|}} otherwise. // Each closes the other two on open; onOpen runs from a click, so forward references hold. var deverField = buildChipField({ wrapCls: "wiktku-dfn-dever", toggleLabel: "+ Dever/Şîwe", hint: "Bi vê maneyê li kû derê an di kîjan devokê de tê bikaranîn?", // Kurmancî's pills are the handful in data-main.json; another language gets the regions // Modul:labels tags for it. The rest are found by typing. quickPicks: function () { var lang = getValues().lang || "ku"; if (lang === "ku") return ctx.DEVER_QUICK_PICKS; return ctx.wiktkuLabelSets ? wiktkuEdit.labels.taggedRegionsFor(ctx.wiktkuLabelSets, lang) : []; }, suggestions: function () { return wiktkuLabelSuggestions("dever"); }, onOpen: function () { etiketField.close(); kategoriField.close(); wiktkuEnsureLabels(); } }); block.data("addDeverChip", deverField.addChip); var etiketField = buildChipField({ wrapCls: "wiktku-dfn-etiket", toggleLabel: "+ Etîket", hint: "xisûsen, Mêrdîn, anatomî, Badînî", suggestions: function () { return wiktkuLabelSuggestions("etiket"); }, onOpen: function () { deverField.close(); kategoriField.close(); wiktkuEnsureLabels(); } }); block.data("addEtiketChip", etiketField.addChip); var kategoriField = buildChipField({ wrapCls: "wiktku-dfn-kategori", toggleLabel: "+ Kategorî", hint: "wekî etîket lê xuya nabe tenê kategorîze dike", suggestions: function () { return wiktkuLabelSuggestions("kategori"); }, onOpen: function () { deverField.close(); etiketField.close(); wiktkuEnsureLabels(); } }); block.data("addKategoriChip", kategoriField.addChip); // Maneid -- the anchor another entry links a sense by, written "{{maneid|lang|x}}" at the // head of the line. One value rather than a list, so it is a plain box and not a chip // field; it still behaves like one to the "Hevmane û Bêhtir" menu through the small api below. var maneidInput = $("<input type='text'>") .addClass("wiktku-dfn-maneid-input wiktku-start-narrow") .attr("placeholder", "id"); var maneidWrap = $("<div class='wiktku-dfn-chipfield-wrap wiktku-dfn-maneid-wrap'></div>") .append($("<label class='wiktku-dfn-maneid-label'></label>") .text("Maneid ") .append(maneidInput)) .hide(); var maneidField = { wrap: maneidWrap, open: function () { maneidWrap.show(); maneidInput.focus(); }, values: function () { var v = $.trim(maneidInput.val() || ""); return v ? [v] : []; } }; // Dever/Şîwe starts open for ku (where it's the common case) // and closed otherwise; backs off once the person toggles it // themselves (see buildChipField's setOpenDefault). Re-synced // from refresh() whenever lang changes — see block.data below. // // Shown only for a language with regions: every Kurdish variety, and any other language // Modul:labels tags one for (read once the labels arrive). One that already has a value stays. function updateDeverOpenState() { var lang = getValues().lang || ""; var hasRegions = !lang || wiktkuEdit.labels.isKurdish(lang) || (ctx.wiktkuLabelSets && wiktkuEdit.labels.regionalFor(ctx.wiktkuLabelSets, lang).length > 0); if (!hasRegions && !ctx.wiktkuLabelSets) wiktkuEnsureLabels(); var show = hasRegions || deverField.values().length > 0; deverField.wrap.toggle(show); if (!show) deverField.panel.hide(); deverField.setOpenDefault(lang === "ku"); } block.data("updateDeverOpenState", updateDeverOpenState); wiktkuLabelListeners.push(updateDeverOpenState); updateDeverOpenState(); // Toggles always sit together in one row; panels render in their // own area below so opening one never displaces the others. // The widget is still built on a reconstruction page so every reference to it stays valid; // it just never reaches the DOM, so it shows nothing and contributes nothing to getOutput(). // // The row is the same shape DefSideBoxes puts under a sense -- a few chips for the common // things, everything else behind "Hevmane û Bêhtir" -- so the two gadgets are learned once. Etîket // and Kategorî start in the menu and their toggle only joins the row once picked. var chipFieldRow = $("<div class='wiktku-dfn-chipfield-row'></div>"); var chipFieldPanels = $("<div class='wiktku-dfn-chipfield-panels'></div>"); if (!isReconstruction) { chipFieldRow.append(deverField.wrap); chipFieldPanels.append(deverField.panel); } etiketField.wrap.hide(); kategoriField.wrap.hide(); chipFieldRow.append(etiketField.wrap).append(kategoriField.wrap).append(maneidWrap); chipFieldPanels.append(etiketField.panel).append(kategoriField.panel); block.append(tplLink); block.append(tplRow); block.append(dfnArea); block.append(izahWrap); block.append(chipFieldRow); block.append(chipFieldPanels); // Apply lang filter on initial build. updateTplOptions(); updateManeMode(); // Sub-field groups. Every one of them is built hidden and asked for by name -- from a chip // in the row above (Nimûne, Jêgirtin) or from the "Hevmane û Bêhtir" menu -- so a sense that needs // none of them is four boxes tall instead of ten. Jêgirtin is built after the loop as a // compound widget and inserted after Nimûne, so the visible order matches this list. var allGroups = [ { cls: "wiktku-dfn-nimune", label: "Nimûne", rows: 2, narrow: false, attested: true } ].concat($.map(wordGroups(), function (g) { return { cls: g.cls, label: g.label, rows: 1, narrow: true, izah: g.izah }; })); if (isReconstruction) { allGroups = $.grep(allGroups, function (g) { return !g.attested; }); } $.each(allGroups, function (_, g) { // wiktku-dfn-wordgroup marks the six word lists, which a şablon sense hides -- both the // group and its entry in the menu (see the .wiktku-sense-is-tpl rules). var groupDiv = $("<div></div>").addClass("wiktku-dfn-subgroup wiktku-dfn-subgroup-hidden " + g.cls + "-group"); if (g.cls !== "wiktku-dfn-nimune") groupDiv.addClass("wiktku-dfn-wordgroup"); // Declared once here rather than with `var` in each branch // below — both branches assign the same pair, just with a // different lbl and list, so one declaration avoids the // (harmless but lint-flagged) re-declaration. var lbl, list; // The language the example is to be written in, named in the box itself. Read fresh // rather than fixed at build time: the entry's language is often chosen later. function nimunePlaceholder() { var code = getValues().lang || ""; return wiktkuUtil.examplePrompt(code && getLangLabel(code), false); } function buildNimuneLi() { var lang = getValues().lang || ""; var li = $("<li></li>").addClass("wiktku-dfn-nimune-li"); var mainTa = $("<textarea></textarea>") .attr("name", "wiktku-dfn-nimune-text-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("rows", g.rows) .attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-text") .attr("placeholder", nimunePlaceholder()); mainTa.on("input", function () { highlightLinebreak(this); var val = $.trim($(this).val()); var isLast = li.is(list.children("li:last-child")); if (val && isLast) { list.append(buildNimuneLi()); } }); li.append(mainTa); var subWrap = $("<div></div>").addClass("wiktku-dfn-nimune-sub"); if (needsTransliteration(lang)) { subWrap.append( $("<textarea></textarea>") .attr("name", "wiktku-dfn-nimune-tr-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("rows", 5) .attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-tr") .attr("placeholder", "Transliterasyona bi alfabeya latînî") ); } if (lang) { subWrap.append( $("<textarea></textarea>") .attr("name", "wiktku-dfn-nimune-t-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("rows", 5) .attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-t") .attr("placeholder", nimuneTranslationPrompt(lang)) ); } if (subWrap.children().length) li.append(subWrap); return li; } // The placeholder names the current language, but that's // often not chosen yet when this block is first built — // so keep a function around that refresh() (and onSubInput, // for newly-grown fields) can call again once lang is set // or changes. function subPlaceholder() { if (g.noLangSuffix) return g.label + "…"; var lang = getValues().lang || ""; var langLabel = lang ? getLangLabel(lang) : ""; return g.label + (langLabel ? " bi " + langLabel : "") + "…"; } function updateSubPlaceholders() { list.find("> li > textarea.wiktku-dfn-subfield").attr("placeholder", subPlaceholder()); } if (g.cls === "wiktku-dfn-nimune") { // Nimûne — compound widget with tr/t sub-fields. lbl = $("<span></span>").addClass("wiktku-dfn-sublabel") .append($("<span></span>").addClass("wiktku-dfn-sublabel-term").text(g.label)) .append(": "); list = $("<ol></ol>").addClass("wiktku-dfn-sublist " + g.cls + "-list"); groupDiv.data("buildNimuneLi", buildNimuneLi); // Refreshed with the word groups' own boxes when the entry's language changes. list.data("updatePlaceholders", function () { list.find("> li > textarea.wiktku-dfn-nimune-text") .attr("placeholder", nimunePlaceholder()); }); list.append(buildNimuneLi()); groupDiv.append(lbl).append(list); block.append(groupDiv); } else { // Regular always-visible group — label + growing textarea list. lbl = $("<span></span>").addClass("wiktku-dfn-sublabel") .append($("<span></span>").addClass("wiktku-dfn-sublabel-term").text(g.label)) .append(": ") .append($("<span></span>").addClass("wiktku-dfn-sublabel-hint").text(g.izah)); var useModifiers = isWordGroupCls(g.cls); list = $("<ol></ol>").addClass("wiktku-dfn-sublist " + g.cls + "-list"); list.data("updatePlaceholders", updateSubPlaceholders); var firstInput = $("<textarea></textarea>") .attr("name", g.cls + "-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("rows", g.rows) .attr("class", "wiktku-dfn-subfield" + (g.narrow ? " wiktku-start-narrow" : "")) .attr("placeholder", subPlaceholder()); firstInput.on("input", function () { steps.dfn.onSubInput(list, this, g.rows, g.narrow, useModifiers); }); list.append(useModifiers ? makeModifierLi(firstInput) : $("<li></li>").append(firstInput)); groupDiv.append(lbl).append(list); block.append(groupDiv); } }); // — Jêder group — growing list, same structure as Nimûne. — if (!isReconstruction) (function () { var jederGroupDiv = $("<div></div>").addClass("wiktku-dfn-subgroup wiktku-dfn-subgroup-hidden wiktku-dfn-jeder-group"); var jederLbl = $("<span></span>").addClass("wiktku-dfn-sublabel") .append($("<span></span>").addClass("wiktku-dfn-sublabel-term").text("Jêgirtin")) .append(": "); var jederList = $("<ol></ol>").addClass("wiktku-dfn-sublist wiktku-dfn-jeder-list"); // Refreshed with every other sub-box when the entry's language changes. jederList.data("updatePlaceholders", function () { jederList.find("> li > textarea.wiktku-dfn-nimune-quote") .attr("placeholder", jederPlaceholder()); }); // Same shape as the nimûne box: the language belongs in the prompt, and is read fresh // because the entry's language is often chosen after the block was built. function jederPlaceholder() { var code = getValues().lang || ""; return wiktkuUtil.examplePrompt(code && getLangLabel(code), true); } function buildJederLi() { var lang = getValues().lang || ""; var li = $("<li></li>").addClass("wiktku-dfn-jeder-li"); // Quote text textarea var quoteTa = $("<textarea></textarea>") .attr("name", "wiktku-dfn-nimune-quote-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("rows", 2) .attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote") .attr("placeholder", jederPlaceholder()); quoteTa.on("input", function () { highlightLinebreak(this); var val = $.trim($(this).val()); var isLast = li.is(jederList.children("li:last-child")); if (val && isLast) { jederList.append(buildJederLi()); } }); li.append(quoteTa); // tr/t sub-fields for the quote — shown as needed for the language. var quoteSubWrap = $("<div></div>").addClass("wiktku-dfn-quote-sub"); if (needsTransliteration(lang)) { quoteSubWrap.append( $("<textarea></textarea>") .attr("name", "wiktku-dfn-nimune-quote-tr-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("rows", 2) .attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote-tr") .attr("placeholder", "Transliterasyona jêgirtinê…") ); } if (lang && lang !== "ku") { quoteSubWrap.append( $("<textarea></textarea>") .attr("name", "wiktku-dfn-nimune-quote-t-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("rows", 2) .attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote-t") .attr("placeholder", "Tercimeya jêgirtinê bi kurmancî") ); } if (quoteSubWrap.children().length) li.append(quoteSubWrap); // The source, in the add-examples form's order and wording: the template, then the link // with its Citoid button, then the template's own fields. var jederDefs = steps.dfn.jederDefs || {}; var srcSel = $("<select></select>").addClass("wiktku-dfn-quote-src-sel wiktku-plain-select"); // "" is auto-detect: a Citoid fetch picks the template. srcSel.append($("<option value=''></option>").text(wiktkuUtil.msg.citoidAuto)); srcSel.append($("<option></option>").val("__manual__").text(wiktkuUtil.msg.sourceByHand)); $.each(jederDefs, function (tplName, def) { srcSel.append($("<option></option>").val(tplName).text(def.label)); }); srcSel.append($("<option></option>").val("__category__").text(wiktkuUtil.msg.sourceFromCategory)); // Paste a citation built on another page into the quote box and have it saved exactly // as typed -- the quickest way to reuse one jêgirtin across many entries. srcSel.append($("<option></option>").val("__raw__").text(wiktkuUtil.msg.sourceRaw)); var srcSelWrap = $("<div></div>").addClass("wiktku-dfn-quote-field") .append($("<label></label>").text("Şablona çavkaniyê"), srcSel); // Saved as url= in every mode but Kategorî and raw, which have no box for it. var urlLbl = $("<label></label>"); var urlInput = $("<input type='text'>").addClass("wiktku-dfn-quote-url"); var citoidBtn = new OO.ui.ButtonWidget({ label: wiktkuUtil.msg.citoidFill, classes: [ "wiktku-dfn-quote-citoid-btn", "wiktku-add-sense-btn" ] }); var citoidStatus = $("<span></span>").addClass("wiktku-dfn-quote-citoid-status"); var urlHint = $("<p></p>").addClass("wiktku-step-hint wiktku-dfn-quote-url-hint"); var citoidWrap = $("<div></div>").addClass("wiktku-dfn-quote-field wiktku-dfn-quote-citoid") .append(urlLbl, urlInput, citoidBtn.$element, citoidStatus, urlHint); // The add-examples form's labels: plain "attach a link" wording for a hand-written // source, Citoid wording otherwise. function setUrlWording(chosen) { if (chosen === "__manual__") { urlLbl.text("Adresa webê ya nimûneyê"); urlInput.attr("placeholder", "URLyeke wekî https://…"); urlHint.text("Tenê binivîse eger lînk jixwe di referansa çavkaniyê de tinebe."); } else { urlLbl.text("Lînk, DOI an ISBNya çavkaniyê"); urlInput.attr("placeholder", "URLyek wekî https://… an jî DOI/ISBN"); urlHint.text(chosen ? "Ev dibe lînka çavkaniyê (url=). Ji bo şablonên jêder-*, dikare bi Citoid an bi destan jî bên dagirtin." : "Li qutiyê URL, DOI an ISBN binivîse û pê lê bike butona '" + wiktkuUtil.msg.citoidFill + "' da ku otomatîk çavkanî were dagirtin."); } } setUrlWording(""); // Fills only empty boxes, so a repeat fetch never overwrites what was typed by hand. function fillEmptyFields(fields) { $.each(fields, function (key, val) { var input = srcFields.find("[data-field-key='" + key + "']"); if (input.length && !$.trim(input.val())) { input.val(val); } }); } // Prefills Dem from the link's own "t=" once jêder-vîdeo is active. function autofillVideoTimestamp() { var citoid = window.wikt && window.wikt.citoid; var dem = srcFields.find("[data-field-key='dem']"); if (srcSel.val() !== "jêder-vîdeo" || !dem.length || $.trim(dem.val()) || !(citoid && citoid.extractVideoTimestamp)) { return; } var t = citoid.extractVideoTimestamp($.trim(urlInput.val())); if (t) dem.val(t); } function runCitoidFetch() { var query = $.trim(urlInput.val()); if (!query) { mw.notify(wiktkuUtil.msg.citoidNeedsUrl); return; } citoidBtn.setDisabled(true); citoidStatus.text("Tê barkirin…"); // Through wikt.cite rather than straight to the citoid gadget: the wrapper adds what // Citoid returns but citoid-helper's mapping drops -- the access date, the ISSN and // the DOI. wiktkuCite.fetchFromCitoid(query).then(function (result) { // Citoid picks the template only while none is chosen; a hand-picked one is kept. var current = srcSel.val(); if (!current && jederDefs[result.jederKey]) { srcSel.val(result.jederKey).trigger("change"); current = result.jederKey; } if (jederDefs[current]) { // Citoid shaped its fields for the type it detected; re-derived for another. fillEmptyFields(current === result.jederKey ? result.fields : wiktkuCite.citoidFieldsFor(current, result.raw, query)); autofillVideoTimestamp(); } else { // No template for this kind of source: a hand-written source line from what // Citoid returned, only while that box is still empty. if (current !== "__manual__") { srcSel.val("__manual__").trigger("change"); } var manualField = srcFields.find(".wiktku-dfn-quote-src-manual"); if (!$.trim(manualField.val())) { var parts = []; $.each(["nivîskar", "sernav", "websîte", "kovar", "weşanxane", "sal"], function (_, k) { if (result.fields[k]) parts.push(result.fields[k]); }); manualField.val(parts.join(", ")); } } citoidStatus.text(wiktkuUtil.msg.citoidFilled); }, function () { // Citoid found nothing: open the form the query points to (Kitêb for an ISBN, // Kovar for a DOI, Websîte for a link, a hand-written source otherwise). var id = wikt.citoid.identifierOf(query); var isLink = /^https?:\/\//i.test(query); var opened = !srcSel.val(); if (opened) { srcSel.val(id && jederDefs[id.jederKey] ? id.jederKey : isLink && jederDefs["jêder-websîte"] ? "jêder-websîte" : "__manual__").trigger("change"); } var current = srcSel.val(); // The identifier gets its own field, so the link box would only repeat it as url=. if (id && srcFields.find("[data-field-key='" + id.field + "']").length) { var idField = {}; idField[id.field] = id.value; fillEmptyFields(idField); urlInput.val(""); } var citoid = window.wikt && window.wikt.citoid; if (isLink && citoid && citoid.websiteNameForUrl) { fillEmptyFields({ "websîte": citoid.websiteNameForUrl(query) }); } if (current === "__manual__") { var manualField = srcFields.find(".wiktku-dfn-quote-src-manual"); if (!$.trim(manualField.val())) { manualField.val(query); } urlInput.val(""); citoidStatus.text("Ev ne lînk, DOI an ISBN e. Çavkaniyê bi destê xwe binivîse."); } else { citoidStatus.text("Citoid malûmat nedîtin. " + (opened ? "Forma «" + jederDefs[current].label + "» hate vekirin: qadên" : "Qadên") + " mayî bi destê xwe tijî bike."); } }).always(function () { citoidBtn.setDisabled(false); }); } citoidBtn.on("click", runCitoidFetch); urlInput.on("input", autofillVideoTimestamp); urlInput.on("keydown", function (e) { if (e.which === 13) { // Enter e.preventDefault(); runCitoidFetch(); } }); // Dynamic source fields container. var srcFields = $("<div></div>").addClass("wiktku-dfn-quote-src-fields"); // "Malûmatên çavkaniyê" over a jêder-* template's boxes, with the button that empties // them and the link. var clearBtn = new OO.ui.ButtonWidget({ label: "Vala bike", framed: false, title: "Lînkê û qadên jêder-* yên vê beşê vala bike", classes: [ "wiktku-dfn-quote-src-clear" ] }); clearBtn.on("click", function () { urlInput.val(""); citoidStatus.text(""); srcFields.find(".wiktku-dfn-quote-src-input").val(""); }); var srcHead = $("<div></div>").addClass("wiktku-dfn-quote-src-head") .append($("<span></span>").addClass("wiktku-dfn-quote-src-head-label").text("Malûmatên çavkaniyê"), clearBtn.$element) .hide(); // One labelled box for a citation parameter. Wide/narrow/medium layout: sernav-type // fields get a full-width multiline box, short ones like sal/rûpel/isbn stay narrow. // Sizes come from wikt.cite, so a Kategorî:Çavkanî template that happens to use the // same parameter names as jêder-* is laid out the same way rather than generically. function buildSrcFieldRow(field) { var size = wiktkuCite.fieldSize(field.key); var width = field.width || size.width || "medium"; var multiline = field.multiline !== undefined ? field.multiline : size.multiline; var row = $("<div></div>") .addClass("wiktku-dfn-quote-src-row wiktku-dfn-quote-src-row--" + width); var fieldLbl = $("<label></label>").text(field.label + ": "); var inp = $(multiline ? "<textarea></textarea>" : "<input type='text'>") .addClass("wiktku-dfn-quote-src-input") .attr("data-field-key", field.key) .attr("placeholder", field.label + "…"); if (multiline) { inp.attr("rows", 2).addClass("wiktku-dfn-quote-src-input--title"); } // sal and tarîx say the same thing twice and Modul:quote refuses both; filling // one clears the other rather than letting the citation break on save. var partner = wiktkuCite.exclusivePartner(field.key); if (partner) { inp.on("input", function () { if (!$.trim($(this).val())) return; $(this).closest(".wiktku-dfn-quote-src-fields") .find(".wiktku-dfn-quote-src-input[data-field-key='" + partner + "']") .val(""); }); } return row.append(fieldLbl).append(inp); } srcSel.on("change", function () { srcFields.empty(); var chosen = $(this).val(); // As in the add-examples form: no link for Kategorî or raw, no Citoid for a // hand-written source, and the fields' header only over a jêder-* template. citoidWrap.toggle(chosen !== "__category__" && chosen !== "__raw__"); citoidBtn.toggle(chosen !== "__manual__"); citoidStatus.text(""); setUrlWording(chosen); srcHead.toggle(!!jederDefs[chosen]); if (!chosen) { return; } if (chosen === "__raw__") { // Nothing to fill in: the quote box above already holds the finished // template, and it is written out exactly as typed. Only the entry name // inside its jêgirtin= is emboldened (see wikt.cite's boldQuoteParam). srcFields.append( $("<p></p>") .addClass("wiktku-step-hint wiktku-dfn-quote-raw-hint") .text("Şablona temam li qutiya jêgirtinê ya jorê binivîse — wek xwe tê qeydkirin.") ); return; } if (chosen === "__manual__") { // Freeform source, written by the user — becomes the 2nd // positional param of {{jêder|lang|…}} instead of çavkanî?. srcFields.append( $("<textarea></textarea>") .attr("rows", 4) .addClass("wiktku-dfn-subfield wiktku-dfn-quote-src-manual") .attr("placeholder", "Çavkaniyê bi destê xwe binivîse…") ); return; } if (chosen === "__category__") { // Same [[Kategorî:Çavkanî]] search widget DefSideBoxes' own Çavkanî subsection box // uses -- picking a result switches the row into the hidden manual mode with // {{Şablon}} pre-filled. The picked şablon becomes the citation template itself: // its own TemplateData parameters are drawn as fields below the picker, in the // same markup the jêder-* branch uses, so readJederList() reads both with one loop. var catPicker = wiktkuCite.buildCategoryTemplateSearch({ placeholder: wiktkuUtil.msg.searchTemplate, describe: false, onPick: function (entry) { li.data("catTemplate", entry.target); li.data("catLangParam", null); catPicker.$status.text("Qutiyên şablonê tên barkirin…"); srcFields.find(".wiktku-dfn-quote-src-row").remove(); wiktkuCite.fieldSpecs(entry.target).then(function (spec) { catPicker.$status.text(""); li.data("catLangParam", spec.langParamKey || null); $.each(spec.fields, function (_, field) { srcFields.append(buildSrcFieldRow(field)); }); }, function () { catPicker.$status.text("Qutiyên şablonê nehatin anîn."); }); } }); srcFields.append(catPicker.$element); return; } var def = jederDefs[chosen]; if (!def) return; $.each(def.fields, function (_, field) { srcFields.append(buildSrcFieldRow(field)); }); autofillVideoTimestamp(); }); // Exposed so draft restore can rebuild a saved category template's fields. // A whole citation line pasted into the quote box ("#* {{jêder|…}}") means it was // copied off another entry; switch to raw mode rather than wrapping it in a second // {{nimûne}}. Only while no source type has been chosen by hand. li.find(".wiktku-dfn-nimune-quote").on("input", function () { if (srcSel.val()) return; if (wiktkuCite.looksLikeCitationLine($(this).val())) { srcSel.val("__raw__").trigger("change"); } }); li.data("buildSrcFieldRow", buildSrcFieldRow); li.append(srcSelWrap, citoidWrap, srcHead, srcFields); return li; } jederGroupDiv.data("buildJederLi", buildJederLi); jederList.append(buildJederLi()); jederGroupDiv.append(jederLbl).append(jederList); // Straight after Nimûne rather than last: Jêgirtin is the // same kind of thing (an attested use of the word) and the // two belong together — see the allGroups comment above. var nimuneGroup = block.find(".wiktku-dfn-nimune-group"); if (nimuneGroup.length) jederGroupDiv.insertAfter(nimuneGroup); else block.append(jederGroupDiv); }()); // Apply the initial hide/show state for the sub-groups (matches // tplSelect's starting value, which is normally empty). updateFieldVisibility(); // ── Which sub-field goes where: chips in the row above for the two everyday ones, the rest // behind "Hevmane û Bêhtir". Same arrangement as DefSideBoxes' own sense menu, deliberately -- a // person who has used one gadget should not have to learn the other. (function () { var PICK_DEFS = [ { cls: "wiktku-dfn-nimune", label: "Nimûne", chip: true, attested: true }, { cls: "wiktku-dfn-jeder", label: "Jêgirtin", chip: true, attested: true } ].concat($.map(wordGroups(), function (g) { return { cls: g.cls, label: g.label, word: true }; })); // Etîket and Kategorî are chip fields, not sub-groups, so they are revealed by showing // their toggle rather than a whole box. var CHIP_FIELD_DEFS = [ { cls: "wiktku-dfn-etiket", label: "Etîket", field: etiketField }, { cls: "wiktku-dfn-kategori", label: "Kategorî", field: kategoriField }, { cls: "wiktku-dfn-maneid", label: "Maneid", field: maneidField } ]; function groupHasContent(groupDiv) { return groupDiv.find("textarea, input").filter(function () { return $.trim($(this).val() || "") !== ""; }).length > 0; } // Shows a group and takes its chip / menu entry out of the picker -- both carry the same // data-pick, so one selector covers whichever of the two this group had. // Which group carries no divider above it: the first one actually on screen, not the // first in the DOM -- see the .wiktku-dfn-subgroup-first rule. function updateFirstVisible() { var $all = block.find(".wiktku-dfn-subgroup").removeClass("wiktku-dfn-subgroup-first"); $all.not(".wiktku-dfn-subgroup-hidden").first().addClass("wiktku-dfn-subgroup-first"); } function revealGroup(cls, focus) { var groupDiv = block.find("." + cls + "-group"); if (!groupDiv.length) return; groupDiv.removeClass("wiktku-dfn-subgroup-hidden"); block.find("[data-pick='" + cls + "']").hide(); updateFirstVisible(); if (focus) groupDiv.find("textarea, input").first().focus(); } block.data("revealSenseGroup", revealGroup); // Hides it again and gives the picker its entry back. Anything typed is thrown away, so a // group holding something asks first. function hideGroup(cls) { var groupDiv = block.find("." + cls + "-group"); if (!groupDiv.length) return; if (groupHasContent(groupDiv) && !window.confirm("Tiştên di vê qutiyê de winda dibin. Dewam bike?")) { return; } groupDiv.children("ol").children("li").not(":first").remove(); groupDiv.find("textarea, input").val(""); // The jêgirtin row's source <select> owns the fields under it; putting it back to "" // is what clears them. groupDiv.find("select.wiktku-dfn-quote-src-sel").val("").trigger("change"); groupDiv.addClass("wiktku-dfn-subgroup-hidden"); block.find("[data-pick='" + cls + "']").show(); updateFirstVisible(); } // Drafts, restoreDfn() and the ⊕-grown lists all put content in without going through // revealGroup(), so anything that ended up filled is shown on the next refresh pass. function syncPicks() { $.each(PICK_DEFS, function (_, def) { var groupDiv = block.find("." + def.cls + "-group"); if (groupDiv.length && groupDiv.hasClass("wiktku-dfn-subgroup-hidden") && groupHasContent(groupDiv)) { revealGroup(def.cls, false); } }); $.each(CHIP_FIELD_DEFS, function (_, def) { if (def.field.values().length) { def.field.wrap.show(); block.find("[data-pick='" + def.cls + "']").hide(); } }); } block.data("syncSensePicks", syncPicks); // Every group gets an × in its label, so a box opened by mistake can go away again -- // the counterpart of DefSideBoxes' own box-remove link. $.each(PICK_DEFS, function (_, def) { var groupDiv = block.find("." + def.cls + "-group"); if (!groupDiv.length) return; var $x = $("<button type='button' class='wiktku-dfn-subgroup-remove'>&#215;</button>") .attr("title", def.label + " veşêre"); $x.on("click", function (e) { e.preventDefault(); hideGroup(def.cls); }); groupDiv.children(".wiktku-dfn-sublabel").first().append($x); }); // — the chips, beside Dever/Şîwe — $.each(PICK_DEFS, function (_, def) { if (!def.chip || (isReconstruction && def.attested)) return; var $chip = $("<button type='button' class='wiktku-dfn-chipfield-toggle'></button>") .attr("data-pick", def.cls) .text("+ " + def.label); $chip.on("click", function (e) { e.preventDefault(); revealGroup(def.cls, true); }); chipFieldRow.append($chip); }); // — "Hevmane û Bêhtir" — var moreArrow = $("<span class='wiktku-dfn-more-arrow'>&#9656;</span>"); var moreToggle = $("<button type='button' class='wiktku-dfn-chipfield-toggle wiktku-dfn-more-toggle'></button>") .append(document.createTextNode(wiktkuUtil.msg.moreMenu + " ")).append(moreArrow); var moreMenu = $("<div class='wiktku-dfn-more-menu'></div>").hide(); function closeMenu() { moreMenu.hide(); moreArrow.html("&#9656;"); } moreToggle.on("click", function (e) { e.preventDefault(); $(".wiktku-dfn-more-menu").not(moreMenu).hide(); moreMenu.toggle(); moreArrow.html(moreMenu.is(":visible") ? "&#9662;" : "&#9656;"); }); function menuEntry(label, cls, extraCls) { var $link = $("<a href='#' class='wiktku-dfn-more-item'></a>").text(label); if (cls) $link.attr("data-pick", cls); if (extraCls) $link.addClass(extraCls); moreMenu.append($link); return $link; } // Group 1: the two label fields. Their toggle only joins the chip row once asked for. $.each(CHIP_FIELD_DEFS, function (_, def) { var $link = menuEntry("+ " + def.label, def.cls); $link.on("click", function (e) { e.preventDefault(); closeMenu(); def.field.wrap.show(); def.field.open(); $link.hide(); }); }); // Group 2: the word lists. wiktku-dfn-pick-word so a şablon sense hides these entries // along with the groups themselves. var firstWord = true; $.each(PICK_DEFS, function (_, def) { if (!def.word) return; var $link = menuEntry("+ " + def.label, def.cls, "wiktku-dfn-pick-word" + (firstWord ? " wiktku-dfn-more-group-start" : "")); firstWord = false; $link.on("click", function (e) { e.preventDefault(); closeMenu(); revealGroup(def.cls, true); }); }); // Group 3: no "Maneê rake" counterpart -- the tab row already carries one bin for the // open sense, and a second delete in here would be a second way to do the same thing. var $newSense = menuEntry(wiktkuUtil.msg.addAnotherSense, null, "wiktku-dfn-more-group-start"); $newSense.on("click", function (e) { e.preventDefault(); closeMenu(); steps.dfn.addSense($("#wiktku-start-dfn"), block); }); chipFieldRow.append($("<span class='wiktku-dfn-more-wrap'></span>").append(moreToggle, moreMenu)); bindMoreMenuCloser(); syncPicks(); updateFirstVisible(); }()); return block; }, /** Shows only `block` among the sense list; hides every other one. */ setOpenSense: function (stepDiv, block) { stepDiv.find("li.wiktku-dfn-sense").each(function () { $(this).toggleClass("wiktku-sense-collapsed", !$(this).is(block)); }); steps.dfn.renderSenseTabs(stepDiv); }, /** Rebuilds the "Mane 1 / Mane 2 / [trash] / + Maneyeke din" tab row from the current sense list. */ renderSenseTabs: function (stepDiv) { var tabsDiv = stepDiv.find(".wiktku-dfn-sense-tabs"); if (!tabsDiv.length) return; tabsDiv.empty(); var blocks = stepDiv.find("li.wiktku-dfn-sense"); var openLi = null; blocks.each(function (i) { var li = $(this); var isOpen = !li.hasClass("wiktku-sense-collapsed"); if (isOpen) openLi = li; var tab = $("<span class='wiktku-dfn-sense-tab'></span>") .text("Mane " + (i + 1)) .toggleClass("wiktku-tab-active", isOpen); tab.on("click", function () { steps.dfn.setOpenSense(stepDiv, li); }); tabsDiv.append(tab); }); // One delete button for the whole bar — not one per tab — so // there's only ever a single, deliberate hit-target for a // destructive action, positioned away from the tabs people // click to switch senses. Always keep at least one Mane. if (blocks.length > 1 && openLi) { var openIdx = blocks.index(openLi); var trash = makeTrashButton("Vê maneyê jê bibe (Mane " + (openIdx + 1) + ")"); trash.on("click", function () { if (!window.confirm("Tu dixwazî Mane " + (openIdx + 1) + " jê bibî? Ev tişt nayê vegerandin.")) return; steps.dfn.removeSense(stepDiv, openLi); }); tabsDiv.append(trash.$element); } var addBtn = $("<button type='button' class='wiktku-add-sense'></button>") .append($("<span class='wiktku-add-sense-plus'>+</span>")) .append(document.createTextNode(wiktkuUtil.msg.anotherSense)); addBtn.on("click", function (e) { e.preventDefault(); steps.dfn.addSense(stepDiv); }); tabsDiv.append(addBtn); }, /** Adds a sense, opens it, and hides the others. With afterBlock, the new one lands directly * under that sense rather than at the end — "Maneyeke din zêde bike" in a sense's own * "Hevmane û Bêhtir" menu is asking for one next to *this* meaning. */ addSense: function (stepDiv, afterBlock) { var list = stepDiv.find("ol.wiktku-dfn-list"); var newBlock = steps.dfn.buildSenseBlock(stepDiv); if (afterBlock && afterBlock.length) newBlock.insertAfter(afterBlock); else list.append(newBlock); initSelects(newBlock); steps.dfn.setOpenSense(stepDiv, newBlock); newBlock.find("textarea.wiktku-dfn-mane").first().focus(); refresh(); return newBlock; }, /** Removes one sense block. Assumes the caller already confirmed * and already checked more than one sense remains. If the * removed sense was the open one, opens whichever sense ends up * taking its place (or the last one, if it was the last). */ removeSense: function (stepDiv, li) { var list = stepDiv.find("ol.wiktku-dfn-list"); var blocks = list.find("li.wiktku-dfn-sense"); var removedIdx = blocks.index(li); var wasOpen = !li.hasClass("wiktku-sense-collapsed"); li.remove(); if (wasOpen) { var remaining = list.find("li.wiktku-dfn-sense"); var next = remaining.eq(removedIdx); if (!next.length) next = remaining.last(); if (next.length) steps.dfn.setOpenSense(stepDiv, next); } steps.dfn.renderSenseTabs(stepDiv); refresh(); }, /** Grows a sub-list (nimûne/formên alternatîv/hevmane/dijmane/termên koordîne) on input. */ onSubInput: function (list, input, rows, narrow, modifiers) { var val = $.trim($(input).val()); var item = $(input).parent(); var isLast = item.is(list.children("li:last-child")); if (val && isLast) { var next = $("<textarea></textarea>") .attr("name", "wiktku-dfn-sub-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("rows", rows) .attr("class", "wiktku-dfn-subfield" + (narrow ? " wiktku-start-narrow" : "")); next.on("input", function () { steps.dfn.onSubInput(list, this, rows, narrow, modifiers); }); list.append(modifiers ? makeModifierLi(next) : $("<li></li>").append(next)); // New field starts with no placeholder — give it (and any // others in the list) the current one. var updatePlaceholders = list.data("updatePlaceholders"); if (typeof updatePlaceholders === "function") updatePlaceholders(); } }, // Populated from data-main.json (tplDefs, jederDefs). tplDefs: {}, jederDefs: {}, build: function (stepDiv) { if (stepDiv.find("ol.wiktku-dfn-list").length) return null; var tabsDiv = $("<div class='wiktku-dfn-sense-tabs'></div>"); stepDiv.append(tabsDiv); var list = $("<ol class='wiktku-dfn-list'></ol>"); list.append(steps.dfn.buildSenseBlock(stepDiv)); stepDiv.append(list); steps.dfn.renderSenseTabs(stepDiv); return null; }, /** Reads all sense blocks from the DOM and returns an array of sense objects. */ getSenses: function (stepDiv) { var senses = []; stepDiv.find("li.wiktku-dfn-sense").each(function () { var block = $(this); var tplSel = block.find("select.wiktku-dfn-tpl-select"); var tpl = tplSel.val() || ""; // noLang: template already encodes the language — don't add |lang| param. var tplNoLang = tpl ? tplSel.find("option[value='" + tpl + "']").attr("data-nolang") === "1" : false; var tplTitle = $.trim(block.find("input.wiktku-dfn-tpl-title").val()); // The mane box and the îzah box are one gloss on the page: "tercime {{îzah|…}}". Joined // here so everything downstream (getOutput, the linkify pass, the cache) keeps seeing a // single string, exactly as it did when the pair was two boxes. var mane = $.trim(block.find("textarea.wiktku-dfn-mane").val()); var izah = $.trim(block.find("textarea.wiktku-dfn-izah").val()); if (izah) { mane = $.trim(mane + " {{îzah|" + izah + "}}"); } function readList(cls) { var vals = []; // Matches only the canonical field, not the visible word-input mirror makeModifierLi() adds alongside it. block.find("." + cls + "-list textarea.wiktku-dfn-subfield").each(function () { var v = $.trim($(this).val()); if (v) vals.push(v); }); return vals; } // Nimûne items are compound: { text, tr?, t? } function readNimuneList() { var items = []; block.find(".wiktku-dfn-nimune-list li.wiktku-dfn-nimune-li").each(function () { var text = $.trim($(this).find(".wiktku-dfn-nimune-text").val()); if (!text) return; var obj = { text: text }; var tr = $.trim($(this).find(".wiktku-dfn-nimune-tr").val()); var t = $.trim($(this).find(".wiktku-dfn-nimune-t").val()); if (tr) obj.tr = tr; if (t) obj.t = t; items.push(obj); }); return items; } // Jêder is a growing list of citations, one per li — mirrors readNimuneList(). function readJederList() { var items = []; block.find(".wiktku-dfn-jeder-list li.wiktku-dfn-jeder-li").each(function () { var li = $(this); var quote = $.trim(li.find(".wiktku-dfn-nimune-quote").val()); if (!quote) return; var obj = { quote: quote }; var qtr = $.trim(li.find(".wiktku-dfn-nimune-quote-tr").val()); var qt = $.trim(li.find(".wiktku-dfn-nimune-quote-t").val()); if (qtr) obj.quoteTr = qtr; if (qt) obj.quoteT = qt; var tplName = li.find(".wiktku-dfn-quote-src-sel").val(); var url = tplName === "__raw__" || tplName === "__category__" ? "" : $.trim(li.find(".wiktku-dfn-quote-url").val()); if (url) obj.url = url; if (tplName === "__manual__") { var manual = $.trim(li.find(".wiktku-dfn-quote-src-manual").val()); if (manual) obj.jederManual = manual; } else if (tplName === "__raw__") { // The quote box already holds a finished citation template; it is written // out untouched, so there is nothing else on this row to read. obj.raw = true; } else if (tplName === "__category__") { // A picked Kategorî:Çavkanî template IS the citation template, with its own // TemplateData parameters -- rendered into the same markup the jêder-* branch // below uses, so one loop reads both. var catName = li.data("catTemplate"); if (catName) { obj.jederCatTpl = catName; var catFields = {}; li.find(".wiktku-dfn-quote-src-input").each(function () { var ck = $(this).attr("data-field-key"); var cv = $.trim($(this).val()); if (ck && cv) catFields[ck] = cv; }); // A restore in progress has no boxes yet -- its TemplateData is still on // the way -- so the values it is restoring stand in until they exist. // See restoreDfn, which parks them here and clears them once they land. if (!Object.keys(catFields).length) { catFields = li.data("catFields") || {}; } obj.jederCatFields = catFields; var catLang = li.data("catLangParam"); if (catLang) obj.jederCatLangParam = catLang; } } else if (tplName) { obj.jederTpl = tplName; var fields = {}; li.find(".wiktku-dfn-quote-src-input").each(function () { var k = $(this).attr("data-field-key"); var v = $.trim($(this).val()); if (k && v) fields[k] = v; }); obj.jêderFields = wiktkuCite.withLinkAsUrl(tplName, fields, url); } items.push(obj); }); return items; } // Dever/Şîwe, Etîket and Kategorî are chip fields (buildChipField), // not textareas, so readList() doesn't reach them — each chip's // value lives in data-value, scoped by the field's wrap class. function readChipList(wrapCls) { var vals = []; block.find("." + wrapCls + "-wrap .wiktku-dfn-chipfield-chip").each(function () { var v = $.trim($(this).attr("data-value") || ""); if (v) vals.push(v); }); return vals; } var sense = { tpl: tpl, tplNoLang: tplNoLang, tplTitle: tplTitle, mane: mane, maneid: $.trim(block.find("input.wiktku-dfn-maneid-input").val() || ""), etiket: readChipList("wiktku-dfn-etiket"), kategori: readChipList("wiktku-dfn-kategori"), nimune: readNimuneList(), jeder: readJederList(), dever: readChipList("wiktku-dfn-dever") }; // alternativ, hevmane, dijmane, koordine, jornav, jernav — one property each, named by // senseGroups' "field" so getOutput() and restoreDfn() find them under the same names. $.each(wordGroups(), function (_, g) { sense[g.field] = readList(g.cls); }); senses.push(sense); }); return senses; }, getOutput: function (values, callback) { var stepDiv = $("#wiktku-start-dfn"); var senses = steps.dfn.getSenses(stepDiv); var lang = values.lang; // A sense counts as "filled" if it has a mane, a chosen template + title, // or any other content (etîket/hevmane/nimûne/jêder…) worth keeping. var hasMane = senses.some(function (s) { return s.mane !== "" || (s.tpl && s.tplTitle) || senseHasExtraContent(s); }); var defLines = []; // one entry per kept sense (goes to linkify) var citeLines = []; // parallel — nimûne+jêder for that same sense var izahLines = []; // parallel — raw îzah gloss for that same sense // True only once some sense's *actual* dfn text (post tercime/îzah // split) is non-empty. var hasLinkableMane = false; $.each(senses, function (_, sense) { // Declared once here rather than with `var` in each branch // below — the template-sense branch returns early, so only // one of the two assignments ever actually runs, but both // are lexically in this same function scope. var idx, cite; // — Template sense (e.g. {{guharto|ku|somepage}}) — if (sense.tpl) { if (!sense.tplTitle) return; // skip empty template senses var tplManeidTpl = buildManeidTpl(sense, lang); var tplKatTpl = buildSenseTpl(sense, "kategori", lang, "K"); var tplDeverTpl = buildDeverTpl(sense, lang); var tplLabelTpl = buildEtiketTpl(sense, lang); var tplCall = sense.tplNoLang ? "{{" + sense.tpl + "|" + sense.tplTitle + "}}" : "{{" + sense.tpl + "|" + lang + "|" + sense.tplTitle + "}}"; idx = defLines.length; cite = buildCite(sense, lang); defLines.push("# " + (tplManeidTpl ? tplManeidTpl + " " : "") + (tplKatTpl ? tplKatTpl + " " : "") + (tplDeverTpl ? tplDeverTpl + " " : "") + (tplLabelTpl ? tplLabelTpl + " " : "") + tplCall + (cite ? "{{wiktku-cite-" + idx + "}}" : "")); citeLines.push(cite); izahLines.push(""); // template senses have no îzah return; } // — Normal mane sense — var dfn = sense.mane; if (!dfn && !senseHasExtraContent(sense)) return; // truly empty // Pull the îzah gloss (if any) out of the combined tercime+îzah // string — it's spliced back in separately below, after its // own linkify pass (see izahLines / the second linkifyText call). var izahRaw = null; var tercimeIzah = splitTercimeIzah(dfn); if (tercimeIzah) { dfn = tercimeIzah.tercime; izahRaw = stripWikilinks(tercimeIzah.izah); } if (dfn) { dfn = stripWikilinks(dfn); // Sentence-case + trailing period are for ku definitions, // which are written as full glosses. if (lang === "ku") { if (dfn[0] !== dfn[0].toUpperCase()) dfn = capitalize(dfn); if (!dfn.match(/[.…?!)\]]$/)) dfn += "."; } // Real prose — this is what makes the sense worth running // through the main linkify pass. hasLinkableMane = true; } else if (izahRaw === null) { // No mane at all — this is just the "{{mane?|lang}}" // placeholder, a bare template call with no plain words // in it. Do NOT count it as linkable: sending nothing but // bracketed templates through the main linkify pass hangs // the linkify gadget (see the hasLinkableMane/hasIzah // check below). // // Every language, not just ku: a sense kept for its jêgirtin or word lists // alone still needs the marker, or the line goes out as a bare "# ". dfn = "{{mane?|" + lang + "}}"; } var maneidTpl = buildManeidTpl(sense, lang); var katTpl = buildSenseTpl(sense, "kategori", lang, "K"); var deverTpl = buildDeverTpl(sense, lang); var labelTpl = buildEtiketTpl(sense, lang); var out = "# " + (maneidTpl ? maneidTpl + " " : "") + (katTpl ? katTpl + " " : "") + (deverTpl ? deverTpl + " " : "") + (labelTpl ? labelTpl + " " : "") + dfn; idx = defLines.length; var hasThisIzah = izahRaw !== null && $.trim(izahRaw) !== ""; // Bare {{...}} token — like {{wiktku-cite-N}}, this survives // linkify's first pass untouched (it treats template calls as // opaque), then gets swapped for {{îzah|…}} once the actual // gloss text has been linkified in its own separate pass. if (hasThisIzah) { out += (dfn ? " " : "") + "{{wiktku-izah-" + idx + "}}"; } function wordLine(tpl, words) { if (!words.length) return ""; return "\n#: {{" + tpl + "|" + lang + "|" + words.join("|") + "}}"; } // Template name and sense property both come from senseGroups, so this and // DefSideBoxes write the same "#:" lines. $.each(ctx.SENSE_GROUPS, function (_, g) { out += wordLine(g.key, sense[g.field] || []); }); cite = buildCite(sense, lang); defLines.push(out + (cite ? "{{wiktku-cite-" + idx + "}}" : "")); citeLines.push(cite); izahLines.push(hasThisIzah ? izahRaw : ""); }); // Splice citeLines back into a linkified string, replacing each // {{wiktku-cite-N}} token with its stored nimûne/jêder block. function restoreCites(t) { return t.replace(/\{\{wiktku-cite-(\d+)\}\}/g, function (_, n) { return citeLines[parseInt(n, 10)] || ""; }); } // Runs text through the linkify gadget if it's loaded, otherwise // just passes it through unchanged. Shared by both linkify passes // below (main mane text, and — separately — îzah glosses). var linkifyText = wiktkuUtil.linkifyText; var hasIzah = izahLines.some(function (t) { return t && $.trim(t) !== ""; }); var text = defLines.join("\n") + "\n"; if (!hasMane) { // Nothing written yet: the sense is still marked as needing one, but no example // placeholder goes with it. text = "# {{mane?|" + lang + "}}\n"; callback(text); return; } // Runs the îzah-only linkify pass against `base` (either the raw // text, when there's no linkable mane to run the main pass on // first, or the already-linkified main text). Shared by both // branches below so the token-splice logic only lives once. function runIzahPass(base) { var izahJoined = $.map(izahLines, function (raw, idx) { if (!raw) return undefined; return "{{wiktku-izah-open-" + idx + "}}" + raw + "{{wiktku-izah-close-" + idx + "}}"; }).join("\n"); linkifyText(izahJoined, function (izahLinked) { var izahByIdx = {}; var re = /\{\{wiktku-izah-open-(\d+)\}\}([\s\S]*?)\{\{wiktku-izah-close-\1\}\}/g; var m; while ((m = re.exec(izahLinked)) !== null) { izahByIdx[parseInt(m[1], 10)] = m[2]; } var withIzah = base.replace(/\{\{wiktku-izah-(\d+)\}\}/g, function (_, n) { var linked = izahByIdx[parseInt(n, 10)]; return linked !== undefined ? "{{îzah|" + linked + "}}" : ""; }); callback(restoreCites(withIzah)); }); } if (!hasLinkableMane) { if (!hasIzah) { // All filled senses are template senses (or otherwise have // no plain text at all) — nothing here needs linkify. callback(restoreCites(text)); return; } // There's no plain mane text anywhere (e.g. tercime left blank // with only îzah filled in), so every defLine is just a bare // template call with nothing to word-link. Sending a line // like "# {{wiktku-izah-0}}" through the main linkify pass // gives it no plain words to work with, makes the linkify // gadget hang — so skip that pass entirely and run only // the îzah pass, splicing straight into the unlinkified text. var loadIzahOnly = function () { runIzahPass(text); }; mw.loader.using("ext.gadget.linkify", loadIzahOnly, loadIzahOnly); return; } var didTryToLoad = function () { // First pass: the main definition text (mane), same as before. On failure the text // comes back unchanged, so the save carries on with what the user wrote -- only the // automatic linking is lost, and saying so is better than losing it silently. linkifyText(text, function (mainLinked, error) { if (error) { mw.notify(error, { type: "warn" }); } if (!hasIzah) { callback(restoreCites(mainLinked)); return; } // Second pass: îzah glosses, linkified via their own // separate call. Each gloss is wrapped in its own // {{wiktku-izah-open/close-N}} pair; like the existing // {{wiktku-cite-N}} tokens, these sit inside double // braces so linkify leaves the tokens themselves alone — // only the raw gloss text between each pair gets linked. runIzahPass(mainLinked); }); }; mw.loader.using("ext.gadget.linkify", didTryToLoad, didTryToLoad); } }, }; // The sections other than Mane: [[MediaWiki:Gadget-wikt.start-wizard-steps/sections.js]]. require("./wikt.start-wizard-steps/sections.js")(ctx, steps); return steps; }; // </nowiki> c52g7g1fvwbn4t37y4yhv4zl694bpqx MediaWiki:Gadget-definition-links.js 8 1359530 6903096 6902522 2026-11-10T15:44:01Z Wikihez 7344 definition-links: a link to the page itself in another language's section jumps to this page's Kurmancî section 6903096 javascript text/javascript // <nowiki> // Adds #Kurmancî to links in definition lines, so following one lands on (and on mobile opens) the Kurmancî // section. Links inside another language's text, and links with a fragment, query or namespace, are left alone. // A link to the page itself, which MediaWiki leaves without an address, jumps to this page's Kurmancî section // when it sits in another language's section. $(function () { var kurmanci = document.getElementById("Kurmancî"); var headings = document.querySelectorAll(".mw-parser-output h2"); // The id of the language heading that comes before el, or "" when there is none. function languageOf(el) { var id = ""; Array.prototype.forEach.call(headings, function (h2) { if (h2.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING) { id = h2.id; } }); return id; } var links = document.querySelectorAll(".mw-parser-output ol:not(.references) a:not(.mw-parser-output [lang] a), " + ".mw-parser-output ol:not(.references) [lang|=ku] a"); Array.prototype.forEach.call(links, function (a) { var href = a.getAttribute("href"); if (href && /^\/wiki\/[^#?:]+$/.test(href)) { a.setAttribute("href", href + "#Kurmancî"); } else if (!href && a.classList.contains("mw-selflink") && kurmanci && languageOf(a) !== "Kurmancî") { a.setAttribute("href", "#Kurmancî"); } }); }); // </nowiki> 3jxccrgoooirvcktyy649g0euaouacy 6903098 6903096 2026-11-10T18:24:54Z Wikihez 7344 definition-links: a self-link turned into a jump looks like a normal link, not bold text 6903098 javascript text/javascript // <nowiki> // Adds #Kurmancî to links in definition lines, so following one lands on (and on mobile opens) the Kurmancî // section. Links inside another language's text, and links with a fragment, query or namespace, are left alone. // A link to the page itself, which MediaWiki leaves without an address, jumps to this page's Kurmancî section // when it sits in another language's section. $(function () { var kurmanci = document.getElementById("Kurmancî"); var headings = document.querySelectorAll(".mw-parser-output h2"); // The id of the language heading that comes before el, or "" when there is none. function languageOf(el) { var id = ""; Array.prototype.forEach.call(headings, function (h2) { if (h2.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING) { id = h2.id; } }); return id; } var links = document.querySelectorAll(".mw-parser-output ol:not(.references) a:not(.mw-parser-output [lang] a), " + ".mw-parser-output ol:not(.references) [lang|=ku] a"); Array.prototype.forEach.call(links, function (a) { var href = a.getAttribute("href"); if (href && /^\/wiki\/[^#?:]+$/.test(href)) { a.setAttribute("href", href + "#Kurmancî"); } else if (!href && a.classList.contains("mw-selflink") && kurmanci && languageOf(a) !== "Kurmancî") { a.setAttribute("href", "#Kurmancî"); // The self-link style shows it as plain bold text; dropped so it looks like the link it now is. a.classList.remove("mw-selflink", "selflink"); } }); }); // </nowiki> 4cuzhwekravig94lmrzlbv0zp598m7m Baxo 0 1359532 6903088 2026-11-10T12:24:10Z Keyomerd 8814 Rûpel bi "== {{ziman|ku}} == === Navdêr === {{navdêr|ku|z=n}} # [[Xweda]] #* {{jêder|ku|jêgirtin=Bê rahet û bê sekne yî</br>Yan aşiqê '''Baxoyê''' xwe yî</br>Yan şubhetê qelbê me yî</br>Ji emrekî bi lez ketî|{{Jêgirtin/Feqiyê Teyran/Dîwan}}}} ==== Etîmolojî ==== Hevreha {{g|ku|beg}}, bo agahiyên zêdetir lê binêre. ==== Werger ==== {{werger-ser}} {{werger-bin}}" hat çêkirin 6903088 wikitext text/x-wiki == {{ziman|ku}} == === Navdêr === {{navdêr|ku|z=n}} # [[Xweda]] #* {{jêder|ku|jêgirtin=Bê rahet û bê sekne yî</br>Yan aşiqê '''Baxoyê''' xwe yî</br>Yan şubhetê qelbê me yî</br>Ji emrekî bi lez ketî|{{Jêgirtin/Feqiyê Teyran/Dîwan}}}} ==== Etîmolojî ==== Hevreha {{g|ku|beg}}, bo agahiyên zêdetir lê binêre. ==== Werger ==== {{werger-ser}} {{werger-bin}} 7nbi8vbc6gwpiorb3b9qdnvbvd63cgr xewsar 0 1359533 6903092 2026-11-10T12:43:37Z Keyomerd 8814 Rûpel bi "== {{ziman|ku}} == === Navdêr === {{navdêr|ku|z=m}} # {{guharto|ku|xûsî}} #* {{jêder|ku|jêgirtin=Bûye payîz li van waran</br>Xezam weşyan binê daran</br>Ji min çi dikin gelî yaran</br>Li mêrgan bûye '''xewsar''' e|{{Jêgirtin/Feqiyê Teyran/Dîwan}}}}" hat çêkirin 6903092 wikitext text/x-wiki == {{ziman|ku}} == === Navdêr === {{navdêr|ku|z=m}} # {{guharto|ku|xûsî}} #* {{jêder|ku|jêgirtin=Bûye payîz li van waran</br>Xezam weşyan binê daran</br>Ji min çi dikin gelî yaran</br>Li mêrgan bûye '''xewsar''' e|{{Jêgirtin/Feqiyê Teyran/Dîwan}}}} qtqtdv4o7m4mlsjqp976poxcsrk429o Şablon:Jêgirtin/Feqiyê Teyran/Dîwan 10 1359534 6903095 2026-11-10T15:43:22Z Wikihez 7344 Ji bo [[Şablon:J:Feqiyê Teyran]] hat beralîkirin 6903095 wikitext text/x-wiki #BERALÎKIRIN [[Şablon:J:Feqiyê Teyran]] gnr4x2xghihkx424t74llj800d9sqq1 MediaWiki:Gadget-wikt.start-wizard/form.js 8 1359535 6903100 2026-11-10T21:26:08Z Wikihez 7344 Split the wizard's script pages under 100 KB; identifierOf shared from citoid-helper 6903100 javascript text/javascript // <nowiki> /* * Page: [[MediaWiki:Gadget-wikt.start-wizard/form.js]] * * The parts of [[MediaWiki:Gadget-wikt.start-wizard.js]] that build the form, restore a draft into it and * open Tercîh. A file of the same module, called with ctx: the wizard's helpers and state. */ module.exports = function (ctx) { var PER_SECTION_STEP_IDS = ctx.PER_SECTION_STEP_IDS; var STEP_GROUPS = ctx.STEP_GROUPS; var WIKTKU_SUGGESTIONS_KEY_PREFIX = ctx.WIKTKU_SUGGESTIONS_KEY_PREFIX; var addLangMode = ctx.addLangMode; var buildKeyInput = ctx.buildKeyInput; var capitalize = ctx.capitalize; var destroy = ctx.destroy; var findLangHeader = ctx.findLangHeader; var formCache = ctx.formCache; var getValues = ctx.getValues; var getValuesForStep = ctx.getValuesForStep; var getWikitext = ctx.getWikitext; var highlightLinebreak = ctx.highlightLinebreak; var initSelects = ctx.initSelects; var insertNewLangSection = ctx.insertNewLangSection; var isWordGroupCls = ctx.isWordGroupCls; var langHeaderSource = ctx.langHeaderSource; var makeModifierLi = ctx.makeModifierLi; var makeTrashButton = ctx.makeTrashButton; var ordered_steps = ctx.ordered_steps; var populateMenus = ctx.populateMenus; var prepareInput = ctx.prepareInput; var refresh = ctx.refresh; var refreshForTextField = ctx.refreshForTextField; var renderBesenDinPicker = ctx.renderBesenDinPicker; var renderGroupTabs = ctx.renderGroupTabs; var scrollTabBarIntoView = ctx.scrollTabBarIntoView; var seedOpenedFromContent = ctx.seedOpenedFromContent; var snapshotDfn = ctx.snapshotDfn; var splitTercimeIzah = ctx.splitTercimeIzah; var stepIdToGroup = ctx.stepIdToGroup; var steps = ctx.steps; var wiktkuCite = ctx.wiktkuCite; var wiktkuEdit = ctx.wiktkuEdit; var wiktkuInitManualResize = ctx.wiktkuInitManualResize; var wiktkuLangs = ctx.wiktkuLangs; var wiktkuNotify = ctx.wiktkuNotify; var wiktkuPosparams = ctx.wiktkuPosparams; var wiktkuSavePrefs = ctx.wiktkuSavePrefs; var wiktkuUtil = ctx.wiktkuUtil; var wiktkuWikitext = ctx.wiktkuWikitext; var wordGroups = ctx.wordGroups; // Clears every draft cache and resize height this script writes, for all pages. Tercîh itself is // left alone: it is the setting being edited when this button is reached. // The state it touches is assigned by build(), which has always run by the time this is callable. function wiktkuClearAllScriptCache() { // Cancel the pending autosave, or it would rewrite the cache from the still-filled form. if (ctx.autosaveDebounceTimer) { clearTimeout(ctx.autosaveDebounceTimer); ctx.autosaveDebounceTimer = null; } ctx.formDirty = false; $("#wiktku-last-saved").text(""); // Keeps formCache's in-memory copy in step with the localStorage sweep below. try { formCache.Set({}); } catch (e) { /* storage unavailable — the raw sweep below still runs */ } if (!window.localStorage) return; var toRemove = []; try { for (var i = 0; i < localStorage.length; i++) { var key = localStorage.key(i); if (!key) continue; // ObjectStorage wraps the draft key as "enwikt/ObjectStorage/<name>?v=<n>&Type=…", so it // is matched as a substring; the mw.storage keys below are plain prefixes. if (key.indexOf("wiktku_start_draft_") !== -1 || key.indexOf("wiktku_resize_height_") === 0 || key.indexOf(WIKTKU_SUGGESTIONS_KEY_PREFIX) === 0) { toRemove.push(key); } } } catch (e) { return; } $.each(toRemove, function (_, key) { try { localStorage.removeItem(key); } catch (e) { /* skip this key, keep clearing the rest */ } }); } // Lazily built the first time it's opened; reused after that (see // wiktkuOpenPrefsDialog below, defined once we know OOUI is available). var wiktkuPrefsWindowManager = null; var wiktkuPrefsDialog = null; function wiktkuOpenPrefsDialog() { mw.loader.using(["oojs-ui-core", "oojs-ui-windows"], function () { function WiktkuPrefsDialog(config) { WiktkuPrefsDialog.super.call(this, config); } if (!wiktkuPrefsDialog) { OO.inheritClass(WiktkuPrefsDialog, OO.ui.ProcessDialog); WiktkuPrefsDialog.static.name = "wiktkuPrefsDialog"; WiktkuPrefsDialog.static.title = "Tercîhên min"; WiktkuPrefsDialog.static.actions = [ { action: "save", label: "Qeyd bike", flags: ["primary", "progressive"] }, { action: "cancel", label: wiktkuUtil.msg.cancel, flags: "safe" }, { action: "clearcache", label: "Hemû cache paqij bike", flags: "destructive" } ]; WiktkuPrefsDialog.prototype.initialize = function () { WiktkuPrefsDialog.super.prototype.initialize.call(this); this.langCodesInput = new OO.ui.TextInputWidget({ placeholder: "ku, zza, ckb (vala bihêle ji bo hemû zimanan)" }); this.onlyOnClickCheckbox = new OO.ui.CheckboxInputWidget({}); this.quietCheckbox = new OO.ui.CheckboxInputWidget({}); this.portletDropdown = new OO.ui.DropdownInputWidget({ options: [ { data: "auto", label: "Otomatîk (li gorî rûyê wîkiyê)" }, { data: "p-cactions", label: "p-cactions - Li qismê Amûran" }, { data: "left-navigation", label: "left-navigation - Li qismê Gotar-Gotûbêj" }, { data: "p-tb", label: "p-tb - Li qismê Giştî" }, { data: "p-views", label: "p-views - Li qismê Bixwîne" } ] }); // Two named groups rather than one "Eyar" heap: which languages the wizard offers, then // how it behaves. The dialog title already says these are preferences. var langSet = new OO.ui.FieldsetLayout({ label: "Ziman" }); langSet.addItems([ new OO.ui.FieldLayout(this.langCodesInput, { label: "Kodên zimanan ên ku tê xuyakirin li qismê \"ziman\"", align: "top", help: "Kodan bi virgulê (,) ji hev veqetîne. Vala bihêle da ku hemû ziman werin nîşandan.", helpInline: true }) ]); var behaviourSet = new OO.ui.FieldsetLayout({ label: "Wextê xebitînê" }); behaviourSet.addItems([ new OO.ui.FieldLayout(this.onlyOnClickCheckbox, { label: "Tenê veke wextê \"+Ziman/beşeke din\" were tikandin", align: "inline", help: "Eger were hilbijartin, ev amûr êdî otomatîk li ser rûpelên nû dest pê nake — divê meriv her carê pêl \"+Ziman/beşeke din\" bike ku bişixule.", helpInline: true }), new OO.ui.FieldLayout(this.portletDropdown, { label: "Cihê lînka \"+Ziman/beşeke din\"", align: "top", help: "\"Otomatîk\" li gorî rûyê wîkiyê hildibijêre.", helpInline: true }), new OO.ui.FieldLayout(this.quietCheckbox, { label: "Moda bêdeng (kêmtir agahdarî nîşan bide)", align: "inline", help: "Agahdariyên normal nîşan nede. Xeta her tim tên xuyakirin.", helpInline: true }) ]); this.$element.addClass("wiktku-prefs-dialog"); this.$body.addClass("wiktku-prefs-dialog-body") .append(langSet.$element, behaviourSet.$element); }; WiktkuPrefsDialog.prototype.getSetupProcess = function (data) { return WiktkuPrefsDialog.super.prototype.getSetupProcess.call(this, data) .next(function () { this.langCodesInput.setValue(ctx.wiktkuPrefs.langCodes.join(", ")); this.onlyOnClickCheckbox.setSelected(!!ctx.wiktkuPrefs.onlyOnClick); this.portletDropdown.setValue(ctx.wiktkuPrefs.portlet || "auto"); this.quietCheckbox.setSelected(!!ctx.wiktkuPrefs.quiet); }, this); }; WiktkuPrefsDialog.prototype.getActionProcess = function (action) { var dialog = this; if (action === "save") { return new OO.ui.Process(function () { var codes = dialog.langCodesInput.getValue() .split(/[,\s]+/) .map(function (c) { return c.trim().toLowerCase(); }) .filter(function (c) { return !!c; }); ctx.wiktkuPrefs = { langCodes: codes, onlyOnClick: dialog.onlyOnClickCheckbox.isSelected(), portlet: dialog.portletDropdown.getValue() || "auto", quiet: dialog.quietCheckbox.isSelected() }; wiktkuSavePrefs(ctx.wiktkuPrefs); mw.notify( "Tercîh hatin qeydkirin. Rûpelê ji nû ve bar bike (F5).", { type: "success", tag: "wiktku-prefs-save" } ); dialog.close({ action: "save" }); }); } if (action === "cancel") { return new OO.ui.Process(function () { dialog.close({ action: "cancel" }); }); } if (action === "clearcache") { return new OO.ui.Process(function () { if (!window.confirm( "Tu dixwazî hemû cache-ya vê amûrê ji her rûpelê paqij bikî " + "(danûstandinên qeydkirî yên hemû rûpelan, bilindahiyên qutiyan)? " + "Tercîhên li jor nayên jêbirin. Ev tişt nayê vegerandin." )) return; wiktkuClearAllScriptCache(); mw.notify("Hemû cache hate paqijkirin.", { type: "success", tag: "wiktku-cache-clear-all" }); dialog.close({ action: "clearcache" }); }); } return WiktkuPrefsDialog.super.prototype.getActionProcess.call(this, action); }; wiktkuPrefsWindowManager = new OO.ui.WindowManager(); $(document.body).append(wiktkuPrefsWindowManager.$element); wiktkuPrefsDialog = new WiktkuPrefsDialog(); wiktkuPrefsWindowManager.addWindows([wiktkuPrefsDialog]); } wiktkuPrefsWindowManager.openWindow(wiktkuPrefsDialog); }); } // Rebuilds the dfn DOM from a previously snapshotted senses array. function restoreDfn(senses) { var stepDiv = $("#wiktku-start-dfn"); // Tear down the existing tab bar + list so build() recreates them cleanly. stepDiv.find(".wiktku-dfn-sense-tabs").remove(); stepDiv.find("ol.wiktku-dfn-list").remove(); steps.dfn.build(stepDiv); var list = stepDiv.find("ol.wiktku-dfn-list"); $.each(senses, function (i, sense) { // Ensure enough blocks exist (build() seeds one). var blocks = list.find("li.wiktku-dfn-sense"); var block = blocks.eq(i); if (!block.length) { var nb = steps.dfn.buildSenseBlock(stepDiv); list.append(nb); block = list.find("li.wiktku-dfn-sense").last(); } // Template selector. block.find("select.wiktku-dfn-tpl-select").val(sense.tpl || ""); if (sense.tpl) { block.find("textarea.wiktku-dfn-mane").hide(); block.find("input.wiktku-dfn-tpl-title").val(sense.tplTitle || "").show(); // The picker is collapsed behind a link by default; a restored // sense that already uses a template has to show it, or the // chosen template is invisible and looks lost. var reveal = block.data("revealTplRow"); if (typeof reveal === "function") reveal(); } else { block.find("textarea.wiktku-dfn-mane").val(sense.mane || "").show(); block.find("input.wiktku-dfn-tpl-title").hide().val(""); } // dfnArea is the only thing actually saved, so split it back into // tercime/îzah for editing (mirrors syncNonKuToMane's join format). var maneParts = splitTercimeIzah(sense.mane); block.find("textarea.wiktku-dfn-mane").val(maneParts ? $.trim(maneParts.tercime) : (sense.mane || "")); block.find(".wiktku-dfn-izah").val(maneParts ? $.trim(maneParts.izah) : ""); var setIzahOpen = block.data("setIzahOpen"); if (setIzahOpen) setIzahOpen(!!(maneParts && $.trim(maneParts.izah))); // Re-sync the formên alternatîv/hevmane/… hide state now that // tplSelect's value was set (its own "change" // handler doesn't fire from .val(), so this doesn't happen otherwise). var updateVis = block.data("updateFieldVisibility"); if (updateVis) updateVis(); // Same for the mane-box mode (plain vs tercime+îzah), since this // also depends on tplSelect's (and lang's) value. var updateMane = block.data("updateManeMode"); if (updateMane) updateMane(); // Restore a sub-list (etiket, formên alternatîv, hevmane …) from a values array. function restoreSubList(cls, values) { if (!values || !values.length) return; var groupDiv = block.find("." + cls + "-group"); var subList = groupDiv.find("ol, ul").first(); if (!subList.length) return; // group not found var useModifiers = isWordGroupCls(cls); // Fill existing items, append new ones as needed. $.each(values, function (j, val) { var items = subList.find("li"); if (j < items.length) { // useModifiers rows keep the word and its modifier tags in // separate elements (see makeModifierLi) — setRawValue() // re-splits val() the same way parsing it fresh would, // instead of dumping the raw "word<key:value>" string into // the visible word box. var setRaw = items.eq(j).data("setRawValue"); if (useModifiers && typeof setRaw === "function") { setRaw(val); } else { items.eq(j).find("textarea").val(val); } } else { var ta = $("<textarea></textarea>") .attr("name", "wiktku-dfn-restore-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("rows", 1) .attr("class", "wiktku-dfn-subfield wiktku-start-narrow") .val(val); ta.on("input", function () { steps.dfn.onSubInput(subList, this, 1, true, useModifiers); }); subList.append(useModifiers ? makeModifierLi(ta) : $("<li></li>").append(ta)); } }); if (!useModifiers) return; // A fresh empty row goes on the end so there's always somewhere // to add the next word without first having to click into a // filled one. var filled = subList.find("li.wiktku-word-li"); if (filled.last().is(":last-child")) { var nextTa = $("<textarea></textarea>") .attr("name", "wiktku-dfn-restore-next-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("rows", 1) .attr("class", "wiktku-dfn-subfield wiktku-start-narrow"); nextTa.on("input", function () { steps.dfn.onSubInput(subList, this, 1, true, useModifiers); }); subList.append(makeModifierLi(nextTa)); } } // Populate the word lists that the draft has something for. senseGroups' "field" is both the // property getSenses() read them into and the tail of the box's own class. $.each(wordGroups(), function (_, g) { var vals = sense[g.field]; if (vals && vals.length) restoreSubList(g.cls, vals); }); // Dever/Şîwe, Etîket, Kategorî — chip fields, not textarea // sub-lists, restored via the adders buildSenseBlock stashed on // the block. block.find("input.wiktku-dfn-maneid-input").val(sense.maneid || ""); var chipRestoreMap = { addDeverChip: sense.dever, addEtiketChip: sense.etiket, addKategoriChip: sense.kategori }; $.each(chipRestoreMap, function (dataKey, vals) { var addChip = block.data(dataKey); if (addChip && vals && vals.length) { $.each(vals, function (_, v) { addChip(v); }); } }); // Nimûne items are objects { text, tr?, t? } — restore using the builder. if (sense.nimune && sense.nimune.length) { var nimuneGroupDiv = block.find(".wiktku-dfn-nimune-group"); var nimuneList = nimuneGroupDiv.find("ol.wiktku-dfn-nimune-list"); var buildFn = nimuneGroupDiv.data("buildNimuneLi"); nimuneList.empty(); $.each(sense.nimune, function (_, n) { var text = typeof n === "object" ? (n.text || "") : n; if (!text) return; var li = buildFn ? buildFn() : $("<li class='wiktku-dfn-nimune-li'></li>"); li.find(".wiktku-dfn-nimune-text").val(text).each(function () { highlightLinebreak(this); }); if (n.tr) li.find(".wiktku-dfn-nimune-tr").val(n.tr); if (n.t) li.find(".wiktku-dfn-nimune-t").val(n.t); nimuneList.append(li); }); // Always leave a blank trailing item. if (buildFn) nimuneList.append(buildFn()); } // Restore jêder entries — accepts current array shape or a legacy // single object (older saved sections) for backwards compat. var jederArr = $.isArray(sense.jeder) ? sense.jeder : (sense.jeder ? [sense.jeder] : []); if (jederArr.length) { var jederGroupDiv = block.find(".wiktku-dfn-jeder-group"); var jederList = jederGroupDiv.find("ol.wiktku-dfn-jeder-list"); var buildJederFn = jederGroupDiv.data("buildJederLi"); jederList.empty(); $.each(jederArr, function (_, j) { if (!j || !j.quote) return; var li = buildJederFn ? buildJederFn() : $("<li class='wiktku-dfn-jeder-li'></li>"); li.find(".wiktku-dfn-nimune-quote").val(j.quote).each(function () { highlightLinebreak(this); }); if (j.quoteTr) li.find(".wiktku-dfn-nimune-quote-tr").val(j.quoteTr); if (j.quoteT) li.find(".wiktku-dfn-nimune-quote-t").val(j.quoteT); if (j.url) li.find(".wiktku-dfn-quote-url").val(j.url); if (j.jederManual) { var manualSel = li.find(".wiktku-dfn-quote-src-sel"); manualSel.val("__manual__").trigger("change"); li.find(".wiktku-dfn-quote-src-manual").val(j.jederManual); } else if (j.raw) { li.find(".wiktku-dfn-quote-src-sel").val("__raw__").trigger("change"); } else if (j.jederCatTpl) { // Reopen the category picker on the saved template and put its fields back. The // fields arrive asynchronously (TemplateData), so the saved values are applied // once they exist rather than straight away. var catSelEl = li.find(".wiktku-dfn-quote-src-sel"); catSelEl.val("__category__").trigger("change"); li.data("catTemplate", j.jederCatTpl); if (j.jederCatLangParam) li.data("catLangParam", j.jederCatLangParam); li.find(".wiktku-dfn-quote-src-cat-input").val(j.jederCatTpl); // Kept on the row from the moment the restore starts. Submitting walks every section // -- restore, then read -- and the read is synchronous while the boxes below only // exist once TemplateData comes back, so without this the values are read from boxes // that are not there yet and every parameter but the quote is lost. readJederList() // falls back to these until the boxes catch up. li.data("catFields", j.jederCatFields || {}); wiktkuCite.fieldSpecs(j.jederCatTpl).then(function (spec) { var srcFields = li.find(".wiktku-dfn-quote-src-fields"); li.data("catLangParam", j.jederCatLangParam || spec.langParamKey || null); $.each(spec.fields, function (_, field) { srcFields.append(li.data("buildSrcFieldRow")(field)); }); $.each(j.jederCatFields || {}, function (key, val) { li.find(".wiktku-dfn-quote-src-input[data-field-key='" + key + "']").val(val); }); // The boxes hold them now, so they are the truth again -- clearing this keeps a // field the user empties from being refilled from the stale copy. li.removeData("catFields"); }); } else if (j.jederTpl) { var srcSel = li.find(".wiktku-dfn-quote-src-sel"); srcSel.val(j.jederTpl).trigger("change"); if (j.jêderFields) { $.each(j.jêderFields, function (key, val) { li.find(".wiktku-dfn-quote-src-input[data-field-key='" + key + "']").val(val); }); } } jederList.append(li); }); // Always leave a blank trailing item, same as Nimûne. if (buildJederFn) jederList.append(buildJederFn()); } }); // Unlike addSense(), build()/buildSenseBlock() above don't wrap their own <select>s -- do it once for the whole restored list (idempotent, so already-wrapped selects are skipped). initSelects(list); // Everything above filled boxes that are still behind the chip row, so each block is asked to // show whichever of its sub-fields the draft actually had something in. list.find("li.wiktku-dfn-sense").each(function () { var syncPicks = $(this).data("syncSensePicks"); if (syncPicks) syncPicks(); }); // Restoring a draft opens the first sense's tab by default. steps.dfn.setOpenSense(stepDiv, list.find("li.wiktku-dfn-sense").first()); } // Snapshots simple (non-dfn) per-section steps from the DOM into obj. function saveCurrentSection() { var snap = {}; $.each(PER_SECTION_STEP_IDS, function (_, id) { if (id === "dfn") { snap.dfn = snapshotDfn(); return; } if (id === "etym") { snap.etym = $.trim($("#wiktku-start-etym .wiktku-etym-text").val()); return; } if (id === "cavkani") { snap.cavkani = $.trim($("#wiktku-start-cavkani .wiktku-cavkani-text").val()); return; } if (id === "posparams") { snap.posparams = $.trim($("#wiktku-start-posparams .wiktku-posparams-text").val()); return; } if (id === "resim") { // One entry per row, including blank ones, so a mid-edit draft // (e.g. a filename typed but no caption yet) survives a cache // restore instead of being silently dropped. snap.resim = $("#wiktku-start-resim li.wiktku-resim-item").map(function () { return { file: $.trim($(this).find(".wiktku-resim-file").val()), caption: $.trim($(this).find(".wiktku-resim-caption").val()), alt: $.trim($(this).find(".wiktku-resim-alt").val()) }; }).get(); return; } var step = steps[id]; if (!step) return; var stepDiv = $("#wiktku-start-" + id); if (id === "trans") { snap.trans = steps.trans.getTranslations(stepDiv); return; } if (id === "zarava") { snap.zarava = steps.zarava.getTranslations(stepDiv); return; } if (id === "dunde") { snap.dunde = steps.dunde.getRows(stepDiv); return; } // Generic: read all step fields. var fields = stepDiv.find(".wiktku-start-step-field"); if (step.multiple) { snap[id] = fields.map(function () { return $(this).val(); }).get(); } else { snap[id] = fields.val(); } }); // Which "Beşên din" sub-sections this section has already opened // via the picker — per-section, like everything else here. snap.openedOtherSections = $.extend({}, ctx.openedOtherSections); ctx.sections[ctx.activeSection] = snap; } // Restores a previously saved snapshot into the form DOM. function restoreSection(snap) { $.each(PER_SECTION_STEP_IDS, function (_, id) { if (id === "dfn") { restoreDfn(snap.dfn || []); return; } if (id === "etym") { $("#wiktku-start-etym .wiktku-etym-wrap").remove(); steps.etym.build($("#wiktku-start-etym")); $("#wiktku-start-etym .wiktku-etym-text").val(snap.etym || ""); return; } if (id === "cavkani") { $("#wiktku-start-cavkani .wiktku-cavkani-wrap").remove(); steps.cavkani.build($("#wiktku-start-cavkani")); $("#wiktku-start-cavkani .wiktku-cavkani-text").val(snap.cavkani || ""); return; } if (id === "posparams") { var ppDiv = $("#wiktku-start-posparams"); ppDiv.find(".wiktku-posparams-step").remove(); steps.posparams.build(ppDiv); var savedTa = snap.posparams || ""; ppDiv.find(".wiktku-posparams-text").val(savedTa); // Re-parse saved textarea to restore helper widget states. if (savedTa) { // Read with the shared parser, so a nested "c=" is never taken for the call's own, and a // saved "z=" restores the gender the same as "c=". var savedC = wiktkuWikitext.callParamValue(savedTa, "c"); var savedJ = wiktkuWikitext.callParamValue(savedTa, "j"); var savedTr = wiktkuWikitext.callParamValue(savedTa, "tr"); // Write into the shared hidden field (not the visible // select, which has no <option>s yet) — syncHelpers() // below reads it back to restore the dropdown once the // gender/number options for this language are populated. if (savedC) $("#wiktku-start-cinsiyet-field").val(savedC); if (savedJ) $("#wiktku-start-jimar-field").val(savedJ); if (savedTr) ppDiv.find(".wiktku-posparams-field[data-pp-key='tr']").val(savedTr); // For lêker, c= holds the verb-type code instead of a // gender — split it back across the two dropdowns it is // now assembled from ("x-gh" → form "x" + base "gh"). if (savedC && /^lêker/.test(snap.pos || "")) { var lekturParts = /^(?:([xphb])-)?(n?gh)$/.exec(savedC); if (lekturParts) { ppDiv.find(".wiktku-posparams-lektur-sel").val(lekturParts[2]); ppDiv.find(".wiktku-posparams-lekturform-sel").val(lekturParts[1] || ""); } } // Restore checked optional params via the same param/value overrides rebuildTa uses. ppDiv.find(".wiktku-posparams-opt-panel input[type='checkbox']").each(function () { var key = $(this).val(); var override = wiktkuPosparams.OPT_PARAM_OVERRIDES[key]; var needle = override ? "|" + override.param + "=" + override.value : "|" + key + "=1"; $(this).prop("checked", savedTa.indexOf(needle) !== -1); }); } steps.posparams.syncHelpers(ppDiv); return; } if (id === "resim") { var resimDiv = $("#wiktku-start-resim"); resimDiv.find(".wiktku-resim-wrap").remove(); steps.resim.build(resimDiv); // Accept both the new array-of-rows format and the older // single {file, caption} object saved before multi-image // support existed. var resimSnap = snap.resim; var rows = $.isArray(resimSnap) ? resimSnap : (resimSnap ? [resimSnap] : []); // setValues() keeps a blank trailing row of its own, same as Nimûne/Dûnde. if (rows.length && steps.resim.field) steps.resim.field.setValues(rows); return; } if (id === "trans") { // Rebuild trans groups (dfn must already be restored by this // point in the $.each loop, so glossList() reflects it). var transDiv = $("#wiktku-start-trans"); transDiv.find(".wiktku-trans-group").remove(); steps.trans.build(transDiv); var groupsSnap = snap.trans || []; $.each(groupsSnap, function (gi, rows) { var group = transDiv.find(".wiktku-trans-group[data-gloss-pos='" + gi + "']"); if (!group.length) return; // gloss no longer exists — drop its rows var list = group.find("ul.wiktku-trans-list"); $.each(rows, function (j, row) { var lang = row[0], words = row[1]; var lastRow = list.find("li.wiktku-trans-row").last(); lastRow.find("select").val(lang).trigger("change"); var wordList = lastRow.find("ul.wiktku-trans-words"); $.each(words, function (k, word) { var wordItems = wordList.find("li"); if (k < wordItems.length) { wordItems.eq(k).find("textarea").val(word); } else { // .val() below doesn't fire "input", so the normal // auto-grow never runs — append a matching item // ourselves instead of silently dropping the word. var extraInput = steps.trans.makeWordInput(); wordList.append($("<li></li>").append(extraInput)); extraInput.val(word); } }); // Leave one trailing empty field so the row can still grow // when the user types, matching a freshly-built row. if ($.trim(wordList.children("li").last().find("textarea").val())) { wordList.append($("<li></li>").append(steps.trans.makeWordInput())); } }); }); return; } if (id === "zarava") { // Rebuild zarava groups (dfn must already be restored by this // point in the $.each loop, so glossList() reflects it). var zaravaDiv = $("#wiktku-start-zarava"); zaravaDiv.find(".wiktku-zarava-group").remove(); steps.zarava.build(zaravaDiv); var zaravaGroupsSnap = snap.zarava || []; $.each(zaravaGroupsSnap, function (gi, rows) { var group = zaravaDiv.find(".wiktku-zarava-group[data-gloss-pos='" + gi + "']"); if (!group.length) return; // gloss no longer exists — drop its rows var list = group.find("ul.wiktku-zarava-list"); $.each(rows, function (j, row) { var lang = row[0], words = row[1]; var lastRow = list.find("li.wiktku-zarava-row").last(); lastRow.find("select").val(lang).trigger("change"); var wordList = lastRow.find("ul.wiktku-zarava-words"); $.each(words, function (k, word) { var wordItems = wordList.find("li"); if (k < wordItems.length) { wordItems.eq(k).find("textarea").val(word); } else { var extraInput = steps.zarava.makeWordInput(); wordList.append($("<li></li>").append(extraInput)); extraInput.val(word); } }); if ($.trim(wordList.children("li").last().find("textarea").val())) { wordList.append($("<li></li>").append(steps.zarava.makeWordInput())); } }); }); return; } if (id === "dunde") { // Rebuild dunde rows. var dundeDiv = $("#wiktku-start-dunde"); dundeDiv.find("ul.wiktku-dunde-list").remove(); steps.dunde.build(dundeDiv); var dundeRows = snap.dunde || []; $.each(dundeRows, function (i, row) { var lang = row[0], words = row[1]; var list = dundeDiv.find("ul.wiktku-dunde-list"); var lastRow = list.find("li.wiktku-dunde-row").last(); lastRow.find("select").val(lang).trigger("change"); var wordList = lastRow.find("ul.wiktku-dunde-words"); $.each(words, function (j, word) { var wordItems = wordList.find("li"); if (j < wordItems.length) { wordItems.eq(j).find("textarea").val(word); } else { // .val() below doesn't fire "input", so the normal // auto-grow never runs — append a matching item // ourselves instead of silently dropping the word. var extraInput = steps.dunde.makeWordInput(); wordList.append($("<li></li>").append(extraInput)); extraInput.val(word); } }); // Leave one trailing empty field so the row can still grow // when the user types, matching a freshly-built row. if ($.trim(wordList.children("li").last().find("textarea").val())) { wordList.append($("<li></li>").append(steps.dunde.makeWordInput())); } }); return; } var step = steps[id]; if (!step) return; var stepDiv = $("#wiktku-start-" + id); var fields = stepDiv.find(".wiktku-start-step-field"); var val = snap[id]; if (val === undefined || val === null) return; if (step.multiple) { var arr = val; fields.each(function (i) { if (i < arr.length) $(this).val(arr[i]); }); } else { fields.val(val); } // Tell the pickers about the programmatic value change. fields.filter("select").trigger("wiktku:refresh"); }); // Restore which "Beşên din" sub-sections were already opened for // this section, then seed in anything the DOM restore above just // filled in but that wasn't marked opened yet. ctx.openedOtherSections = $.extend({}, snap.openedOtherSections || {}); seedOpenedFromContent(); renderBesenDinPicker(); } function build() { $(".nowizard, .nowizard .noafterwizard").hide(); var callToAction = $("#wiktku-calltoaction"); var origCallToActionText = callToAction.text(); $(mw.util.addPortletLink("p-views", "#", "Edîtora çavkaniyê veke", "ca-wiktku-nowizard", "Vê rûpelê bi edîtora çavkaniyê çêke", undefined, "#ca-watch")).click(function (evt) { evt.preventDefault(); callToAction.text(origCallToActionText); destroy(); }); if (callToAction.length) { callToAction.text(origCallToActionText + " Forma li jêr ji bo çêkirina madeyeke nû bi kar bîne."); } var startForm = $("<form id='wiktku-start-form'></form>"); var clearPageCheck = null; if (addLangMode) { startForm.append($("<p class='wiktku-addlang-notice'></p>").html( "Bi vê formê tu yê zimanekî nû li rûpelê îlawe bikî. Eger ev ziman jixwe hebe, beşa nû li dawiya vî zimanî dê were îlawekirin. " + "Ji kerema xwe netîceya dawî kontrol bike, eger problemek derkeve, li <a href='" + mw.util.getUrl('Mediawiki talk:Gadget-start.js') + "'>nîqaşa vê amûrê</a> problemê ragihîne." )); clearPageCheck = $("<input type='checkbox' id='wiktku-clear-page-check'>"); var clearPageLabel = $("<label for='wiktku-clear-page-check'></label>") .text(" Muhtewaya rûpelê paqij bike (her tiştî jê bibe û rûpelê ji nû ve binivîse)"); startForm.append( $("<p class='wiktku-addlang-notice'></p>") .append(clearPageCheck) .append(clearPageLabel) ); // Re-evaluate step visibility (e.g. the pron step) whenever this // is toggled, since it changes what counts as "existing content". clearPageCheck.on("change", function () { refresh(); }); } var spinner; // Tab bar for switching between POS sections. var sectionTabsDiv = $("<div id='wiktku-section-tabs'></div>"); // Tab bar for switching between step-groups (topics) within a section. // renderGroupTabs()/updateGroupTabIndicators() (which populate and // maintain it) live outside build() — see near "var activeSection". var groupTabsDiv = $("<div id='wiktku-group-tabs'></div>"); // Both tab bars are sticky. group-tabs sits below section-tabs' rendered height, which varies // when the bar wraps, so it is measured and published as a CSS custom property. // Vector 2022's own sticky header would cover section-tabs, so its occupied height goes into // --wiktku-siteheader-h. It slides with scroll, hence the re-measure on scroll as well as resize. function siteHeaderOffset() { var el = document.querySelector(".vector-sticky-header"); if (!el) return 0; var rect = el.getBoundingClientRect(); // Present in the DOM at all times on Vector 2022, but only // actually occupying screen space once scrolled into place — // before that it sits translated above the viewport (negative/ // zero rect), which must not add a phantom gap under our bars. if (rect.bottom <= 0 || rect.top > 4) return 0; return Math.max(0, rect.bottom); } var stickyOffsetQueued = false; function syncStickyTabsOffset() { var h = sectionTabsDiv.is(":visible") ? (sectionTabsDiv.outerHeight() || 0) : 0; startForm.css("--wiktku-stabs-h", h + "px"); startForm.css("--wiktku-siteheader-h", siteHeaderOffset() + "px"); } function queueStickyTabsOffsetSync() { if (stickyOffsetQueued) return; stickyOffsetQueued = true; window.requestAnimationFrame(function () { stickyOffsetQueued = false; syncStickyTabsOffset(); }); } $(window).on("resize.wiktkuStickyTabs scroll.wiktkuStickyTabs", queueStickyTabsOffsetSync); // "Beşên din" picker — the search box for adding a section, at the top of the form; // renderBesenDinPicker() fills it. var besenDinPickerDiv = $("<div id='wiktku-besendin-picker' class='wiktku-besendin-picker'></div>"); /** Puts the form back in a usable state after a failed submit and * says what happened. Without this the button stays disabled and * the spinner spins forever on any exception — the person is left * staring at a form that looks busy but is dead, with the reason * visible only in the browser console. */ function submitFailed(err) { spinner.hide(); ctx.continueBtn.setDisabled(false); ctx.continueBtn.$element.removeClass("wiktku-btn-sending"); var detail = (err && (err.message || err.toString())) || "sedem nediyar"; mw.notify( "Şandin bi ser neket: " + detail + " — tiştê ku te nivîsî winda nebûye. Heke ev dubare bibe, " + "bi 'Edîtora çavkaniyê veke' berdewam bike.", { type: "error", tag: "wiktku-submit", autoHide: false } ); // Deliberately console.error as well: the notification is for the // person, this is for whoever they report the bug to. if (window.console && console.error) console.error("[wiktku] submit failed", err); } ctx.onSubmitError = submitFailed; startForm.submit(function (evt) { evt.preventDefault(); ctx.continueBtn.setDisabled(true); ctx.continueBtn.$element.addClass("wiktku-btn-sending"); spinner.show(); try { getWikitext(function (wikitext) { try { if (addLangMode && clearPageCheck && clearPageCheck.prop("checked")) { // User asked to wipe the page — write fresh content. $("#wpTextbox1").val(wikitext); } else if (addLangMode) { var textbox = $("#wpTextbox1"); var existing = textbox.val() || ""; // ── h-bnr merge ────────────────────────────────────────── // If the user filled bnr-h inputs, either merge into the // existing {{h-bnr|…}} call or prepend a new one. var bnrhOut = steps.bnrh.output(getValues()); if (bnrhOut) { var newPages = bnrhOut.replace(/^\{\{h-bnr\|/, "").replace(/\}\}\n?$/, "").split("|"); var bnrhRe = /\{\{h-bnr\|([^}]*)\}\}/i; if (bnrhRe.test(existing)) { existing = existing.replace(bnrhRe, function (_, inner) { var existingPages = inner.split("|").map(function (p) { return p.trim(); }); $.each(newPages, function (_, p) { p = p.trim(); if (p && existingPages.indexOf(p) === -1) existingPages.push(p); }); return "{{h-bnr|" + existingPages.join("|") + "}}"; }); } else { existing = bnrhOut + existing; } } // If this language's section already exists (whitespace // differences aside), merge into it; otherwise insert new. var langHeaderMatch = wikitext.match(new RegExp("^(\n?" + langHeaderSource("[^}]+") + "\\n)")); if (langHeaderMatch) { var langHeader = langHeaderMatch[1]; // e.g. "== {{ziman|ku}} ==\n" var newCode = langHeaderMatch[2]; var existingHeader = findLangHeader(existing, newCode); if (existingHeader) { // Strip the header from the new output. var newContent = wikitext.slice(langHeader.length); // Find the insertion point: end of the existing lang section. // A new L2 heading marks the start of the next language block. var searchFrom = existingHeader.index + existingHeader.match.length; // Find the next == … == heading after this section's start. var nextL2 = existing.indexOf("\n== ", searchFrom); var insertAt = nextL2 !== -1 ? nextL2 : existing.length; // Ensure there's a blank line before the inserted block. var before = existing.slice(0, insertAt); var after = existing.slice(insertAt); var sep = /\n\s*$/.test(before) ? "" : "\n"; var merged = before + sep + newContent + after; // The section just added may repeat a part of speech the page already had. Both // are renumbered across the whole language section, so a lone "Rengdêr" that was // there before becomes "Rengdêr 1" and the new one "Rengdêr 2" -- leaving the old // one unnumbered would make the pair unreadable on the page. var secStart = existingHeader.index; var nextLang = merged.indexOf("\n== ", secStart + existingHeader.match.length); var secEnd = nextLang === -1 ? merged.length : nextLang; textbox.val( merged.slice(0, secStart) + wiktkuEdit.wt.numberRepeatedPosHeadings(merged.slice(secStart, secEnd)) + merged.slice(secEnd) ); } else { // New language — insert at the correct sorted position. textbox.val(insertNewLangSection(existing, wikitext)); } } else { // Unexpected format — insert at sorted position anyway. textbox.val(insertNewLangSection(existing, wikitext)); } } else { $("#wpTextbox1").val(wikitext); } $("html, body").animate({scrollTop: 0}); callToAction.text("Hindik ma!"); // The draft is now live in the editor, submitted or not — keeping // it cached would just offer to restore already-used content // the next time the wizard opens on this page. try { formCache.Set({}); } catch (e) { /* storage unavailable — harmless, the content is already in the editor */ } destroy(); } catch (e) { // Thrown while writing the result into #wpTextbox1. submitFailed(e); } }); } catch (e) { // Catches anything thrown synchronously inside getWikitext. submitFailed(e); } }); // The step the "Beşên din" picker is inserted directly above. var PICKER_BEFORE_STEP = "tewandin"; // Tracks whether groupTabsDiv has been inserted yet. var groupTabsInserted = false; $.each(ordered_steps, function (i, step) { var stepDiv = $(mw.html.element("div", { "class": "wiktku-start-step", id: "wiktku-start-" + step.id })); var name = capitalize(step.name); var nameH3 = $("<h3></h3>").addClass("wiktku-start-step-name"); var nameLabel = $("<label></label>") .attr("for", "wiktku-start-" + step.id + "-field") .text(name); nameH3.append(nameLabel); stepDiv.append(nameH3); if (step.question_fmt) { // question_fmt can be a plain string, or a function of the // current values (used by steps whose question text depends // on e.g. the chosen language) — refresh() keeps it in sync // afterwards via the .wiktku-start-step-question class. var questionText = typeof step.question_fmt === "function" ? step.question_fmt(getValues()) : step.question_fmt; var questionLabel = $(mw.html.element("label", { "class": "wiktku-start-step-question", "for": "wiktku-start-" + step.id + "-field" }, questionText)); stepDiv.append(questionLabel); } var input; if (step.choices) { input = $(mw.html.element("select", { "class": "wiktku-start-step-field", id: "wiktku-start-" + step.id + "-field" })); input.attr("data-placeholder", capitalize(step.name) + " hilbijêre "); populateMenus(input, step.choices); // The language step ships ~300 languages; if a search comes up empty, fetch the other ~3400 // and refill this select before telling the user there is no such language. if (step.id === "lang") { input.data("wiktkuOnNoMatch", function (query, refresh) { if (!ctx.wiktkuMergeLanguages || wiktkuLangs.isFull()) { return; } wiktkuLangs.ensureFull().then(function (all) { populateMenus(input, ctx.wiktkuMergeLanguages(all)); refresh(); }); }); } if (step.multiple) input.attr("multiple", true); // initSelects() runs over the whole form once it is in the DOM. stepDiv.append(input); // Pos-specific: the definition/example for whichever cureyê // rêzimanî is currently chosen, as plain text right under the // dropdown — kept in sync by refresh() via .wiktku-start-pos-example. if (step.id === "pos") { stepDiv.append( $("<p class='wiktku-step-hint wiktku-start-pos-example'></p>").text(step.example(getValues())) ); } } else if (typeof(step.build) === "function") { input = step.build(stepDiv); } else { input = $("<textarea></textarea>") .attr("class", "wiktku-start-step-field") .attr("id", "wiktku-start-" + step.id + "-field") .attr("rows", step.rows || 2); if (step.narrow) input.addClass("wiktku-start-narrow"); if (step.placeholder) { input.attr("placeholder", typeof step.placeholder === "function" ? step.placeholder() : step.placeholder); } if (step.multiple) { input.on("input", function (evt) { refreshForTextField(evt.target, step); }); var keyInput = buildKeyInput(step); // Same one-box-per-word layout as the per-sense ⊕-modifier // groups (see makeModifierLi) when this step has modifiers. var list = $(step.ordered ? "<ol></ol>" : "<ul></ul>") .append(step.modifiers ? makeModifierLi(input) : $("<li></li>").append(keyInput).append(input)); stepDiv.append(list); } else stepDiv.append(input); } prepareInput(input, step); stepDiv.hide(); // Section tabs (switch between POS sections) sit above the // per-section fields, starting at "pos". if (step.id === "pos") { startForm.append(sectionTabsDiv); } // Step-group tabs sit just before the first grouped step in // ordered_steps. Derived from stepIdToGroup so this keeps // working no matter how STEP_GROUPS/ordered_steps get reordered. if (!groupTabsInserted && stepIdToGroup[step.id] !== undefined) { // With a single group there is nothing to switch between, so the bar is never built -- // rather than built and then hidden, which left it on screen whenever the form was // rebuilt before refresh() next ran. if (STEP_GROUPS.length > 1) { startForm.append(groupTabsDiv); } groupTabsInserted = true; } // The picker sits between the senses and Tewandin: sections are added underneath it, so a // newly picked one lands right here and pushes the rest of the form down. if (step.id === PICKER_BEFORE_STEP) { startForm.append(besenDinPickerDiv); } startForm.append(stepDiv); }); // ── Section tab helpers ────────────────────────────────────────────── // ── Add-section button — lives in the tab bar, disabled until pos chosen ── var addSectionBtn = new OO.ui.ButtonWidget({ label: "+ Beşeke din", disabled: true, classes: [ "wiktku-add-sense-btn" ] }); /** Adds a new, empty POS section and switches to it. */ function addPosSection() { saveCurrentSection(); ctx.viewingPron = false; ctx.sections.push({}); ctx.activeSection = ctx.sections.length - 1; $.each(PER_SECTION_STEP_IDS, function (_, id) { // Declared once here rather than with `var` in the "dfn" // branch and again in the fallback branch below — each // branch returns before the other runs, but both are // lexically in this same function scope. var stepDiv; if (id === "dfn") { stepDiv = $("#wiktku-start-dfn"); stepDiv.find(".wiktku-dfn-sense-tabs").remove(); stepDiv.find("ol.wiktku-dfn-list").remove(); steps.dfn.build(stepDiv); return; } if (id === "etym") { $("#wiktku-start-etym .wiktku-etym-wrap").remove(); steps.etym.build($("#wiktku-start-etym")); return; } if (id === "cavkani") { $("#wiktku-start-cavkani .wiktku-cavkani-wrap").remove(); steps.cavkani.build($("#wiktku-start-cavkani")); return; } if (id === "posparams") { var ppDiv = $("#wiktku-start-posparams"); ppDiv.find(".wiktku-posparams-step").remove(); return; } if (id === "resim") { $("#wiktku-start-resim .wiktku-resim-wrap").remove(); return; } if (id === "trans") { var transDiv = $("#wiktku-start-trans"); transDiv.find(".wiktku-trans-group").remove(); return; } if (id === "zarava") { var zaravaDiv = $("#wiktku-start-zarava"); zaravaDiv.find(".wiktku-zarava-group").remove(); return; } if (id === "dunde") { var dundeDiv = $("#wiktku-start-dunde"); dundeDiv.find("ul.wiktku-dunde-list").remove(); return; } stepDiv = $("#wiktku-start-" + id); stepDiv.find(".wiktku-start-step-field").val(""); stepDiv.find("select.wiktku-start-step-field").trigger("wiktku:refresh"); }); // Fresh section — no "Beşên din" sub-section has been opened yet. ctx.openedOtherSections = {}; renderBesenDinPicker(); renderSectionTabs(); refresh(); } addSectionBtn.on("click", addPosSection); /** Removes one POS section. Assumes the caller already confirmed and * already checked more than one section remains. If the removed * section was the one on screen, its in-progress DOM edits are * discarded along with it (never saved back into sections[]) and a * neighboring section is restored in its place. */ function removePosSection(idx) { ctx.sections.splice(idx, 1); if (idx === ctx.activeSection) { ctx.viewingPron = false; ctx.activeSection = Math.min(idx, ctx.sections.length - 1); restoreSection(ctx.sections[ctx.activeSection]); refresh(); } else if (idx < ctx.activeSection) { ctx.activeSection -= 1; } renderSectionTabs(); } /** Re-render the tab bar to reflect current sections[] and activeSection. */ function renderSectionTabs() { // Detach (not remove) the button first: .empty() strips bound // event data from any children left inside it, which would // silently kill addSectionBtn's click handler on every re-render. addSectionBtn.$element.detach(); sectionTabsDiv.empty(); var lang = getValues().lang || ""; if (!lang) { sectionTabsDiv.hide(); syncStickyTabsOffset(); return; } sectionTabsDiv.show(); // Bilêvkirin leads the bar, ahead of the POS sections. var pronTab = $("<span></span>") .addClass("wiktku-section-tab wiktku-section-tab-pron") .text("Bilêvkirin") .toggleClass("wiktku-tab-active", ctx.viewingPron); pronTab.on("click", function () { if (ctx.viewingPron) return; saveCurrentSection(); ctx.viewingPron = true; refresh(); renderSectionTabs(); scrollTabBarIntoView("wiktku-section-tabs"); }); sectionTabsDiv.append(pronTab); $.each(ctx.sections, function (i) { var posVal = ctx.sections[i].pos || ""; // Numbered while there is more than one section of this part of speech, so two tabs are // never both "Rengdêr" -- the same numbering the headings get on save. var samePos = $.grep(ctx.sections, function (s) { return (s.pos || "") === posVal; }); var nth = 0; $.each(ctx.sections, function (j, s) { if ((s.pos || "") === posVal && j <= i) nth++; }); var tabLabel = posVal ? capitalize(posVal) + (samePos.length > 1 ? " " + nth : "") : ("Beş " + (i + 1)); var tab = $("<span></span>") .addClass("wiktku-section-tab") .text(tabLabel) .toggleClass("wiktku-tab-active", i === ctx.activeSection && !ctx.viewingPron); (function (idx) { tab.on("click", function () { if (idx === ctx.activeSection && !ctx.viewingPron) return; // Leaving Bilêvkirin doesn't need a save — its field // is shared and never part of a section snapshot. if (!ctx.viewingPron) saveCurrentSection(); ctx.viewingPron = false; ctx.activeSection = idx; restoreSection(ctx.sections[ctx.activeSection]); refresh(); renderSectionTabs(); scrollTabBarIntoView("wiktku-section-tabs"); }); }(i)); sectionTabsDiv.append(tab); }); // One delete button for the whole bar — not one per tab — same // reasoning as the Mane tabs' trash button: deletes whichever // Beş is currently open, so there's only one thing a click on it // can possibly remove. Not shown on Bilêvkirin (nothing to // delete there) or when it's the only Beş left. if (ctx.sections.length > 1 && !ctx.viewingPron) { var activeTabLabel = ctx.sections[ctx.activeSection] && ctx.sections[ctx.activeSection].pos ? capitalize(ctx.sections[ctx.activeSection].pos) : ("Beş " + (ctx.activeSection + 1)); var sectionTrash = makeTrashButton("Vê beşê jê bibe (" + activeTabLabel + ")"); sectionTrash.on("click", function () { if (!window.confirm("Tu dixwazî \"" + activeTabLabel + "\" jê bibî? Ev tişt nayê vegerandin.")) return; removePosSection(ctx.activeSection); }); sectionTabsDiv.append(sectionTrash.$element); } // Enable "+ Beşeke din" only when the active section has a pos chosen. var activePos = ctx.sections[ctx.activeSection] && ctx.sections[ctx.activeSection].pos ? ctx.sections[ctx.activeSection].pos : getValues().pos; addSectionBtn.setDisabled(!activePos); sectionTabsDiv.append(addSectionBtn.$element); syncStickyTabsOffset(); } renderSectionTabs(); /** * Re-populates a growing "multiple" field list (bnrh, je, tekildar, …) from a saved array. * Values are set with .val() plus an "input" event so refreshForTextField() grows the rows, * exactly as typing into the last row does. */ function restoreMultiField(id, values) { var container = $("#wiktku-start-" + id); $.each(values || [], function (i, v) { var field = container.find(".wiktku-start-step-field").eq(i); if (!field.length || !v) return; field.val(v).trigger("input"); }); } /** * Restores a full cached draft: shared lang/pron/bnrh fields, then * every section (via the same restoreSection()/refresh()/ * renderSectionTabs() sequence already used when switching tabs). * @param cached {{lang: string, pron: string, bnrh: Array, sections: Array, activeSection: number}} */ function restoreFromCache(cached) { if (cached.lang) { $("#wiktku-start-lang-field").val(cached.lang); $("#wiktku-start-lang-field").trigger("wiktku:refresh"); } // Rebuild dependent steps (pos, pron wrap, etc.) now that lang is set. refresh(); if (cached.pron !== undefined) { $("#wiktku-start-pron .wiktku-pron-text").val(cached.pron); } if (cached.bnrh) { restoreMultiField("bnrh", cached.bnrh); } ctx.sections = (cached.sections && cached.sections.length) ? cached.sections : [{}]; ctx.activeSection = (typeof cached.activeSection === "number" && cached.activeSection >= 0 && cached.activeSection < ctx.sections.length) ? cached.activeSection : 0; restoreSection(ctx.sections[ctx.activeSection]); refresh(); renderSectionTabs(); updateLastSavedIndicator(cached.savedAt || null); ctx.formDirty = false; wiktkuNotify("Danûstandina berê ya qeydkirî ji cache hate barkirin.", { type: "info", tag: "wiktku-cache-restore" }); } // Update the active tab's label whenever pos changes. startForm.on("change", "#wiktku-start-pos-field", function () { // renderSectionTabs() labels each tab from sections[i].pos, which // is only synced from the DOM by saveCurrentSection() (on tab // switch/section add). Without this, the active tab's own label // stays on the previous pos (or "Beş N") until the user switches // away and back. Update it directly so the rename is immediate. if (ctx.sections[ctx.activeSection]) ctx.sections[ctx.activeSection].pos = $(this).val(); renderSectionTabs(); // A new cureyê rêzimanî means any previously chosen cinsiyet/ // cureyê lêkerê no longer applies — clear both selections (and // the shared hidden cinsiyet field, since rebuildTa()/syncHelpers() // fall back to it) before wiping the textarea, so nothing stale // gets carried over into the rebuilt template. var ppDiv = $("#wiktku-start-posparams"); ppDiv.find(".wiktku-posparams-cinsiyet-sel").val(""); ppDiv.find(".wiktku-posparams-jimar-sel").val(""); ppDiv.find(".wiktku-posparams-lektur-sel").prop("selectedIndex", 0); ppDiv.find(".wiktku-posparams-lekturform-sel").prop("selectedIndex", 0); $("#wiktku-start-cinsiyet-field").val(""); $("#wiktku-start-jimar-field").val(""); ppDiv.find(".wiktku-posparams-text").val(""); // refresh() runs steps.posparams.syncHelpers(), which toggles the // param rows for the new pos and — since the textarea is now // empty — calls rebuildTa() to repopulate it. refresh(); }); // Show the tab bar as soon as a language is selected. startForm.on("change", "#wiktku-start-lang-field", function () { renderSectionTabs(); }); var footer = $("<div id='wiktku-start-footer'></div>"); spinner = $(mw.html.element("img", { id: "wiktku-start-spinner", src: "//upload.wikimedia.org/wikipedia/commons/d/de/Ajax-loader.gif", alt: "Tê barkirin…", title: "Tê barkirin…" })); footer.append(spinner.hide()); ctx.continueBtn = new OO.ui.ButtonWidget({ id: "wiktku-unhide-editform", label: "Bişîne", flags: [ "primary", "progressive" ] }); ctx.continueBtn.on("click", function () { // Immediate feedback that the click registered ctx.continueBtn.$element.addClass("wiktku-btn-flash"); setTimeout(function () { ctx.continueBtn.$element.removeClass("wiktku-btn-flash"); }, 500); wiktkuNotify("Tê şandin…", { type: "info", tag: "wiktku-submit" }); startForm.trigger("submit"); }); ctx.continueBtn.$element.addClass("wiktku-footer-primary"); footer.append(ctx.continueBtn.$element); // "Edîtora çavkaniyê veke" button — inline fallback for the portlet link var sourceEditorBtn = new OO.ui.ButtonWidget({ label: "Edîtora çavkaniyê veke", flags: [ "destructive" ], }); sourceEditorBtn.on("click", function () { destroy(); }); sourceEditorBtn.$element.addClass("wiktku-footer-primary"); footer.append(sourceEditorBtn.$element); // ── Cache save/clear buttons ───────────────────────────────────────── // Manual, page-scoped save of the entire wizard (all sections, not // just the active one) via [[MediaWiki:Gadget-StorageUtils.js]]'s // ObjectStorage. Restoring happens silently on load at the end of // build(), once the full language list is available. // // Saving also happens automatically (debounced while typing, plus a // periodic fallback) — see "Autosave" below — since relying on the // person to remember to click "Li cache qeyd bike" before an // accidental tab close/reload defeats the point of having a cache // at all. function buildCacheSnapshot() { return { lang: getValues().lang || "", pron: $("#wiktku-start-pron .wiktku-pron-text").val() || "", // bnrh is shared (page-level), same as lang/pron above — not // per-section, so it doesn't live in sections[]. bnrh: getValuesForStep("bnrh"), sections: ctx.sections, activeSection: ctx.activeSection, savedAt: Date.now() }; } // ── Last-saved indicator ───────────────────────────────────────────── var lastSavedEl = $("<span id='wiktku-last-saved'></span>"); function formatSavedTime(ts) { try { return new Date(ts).toLocaleTimeString(mw.config.get("wgUserLanguage"), { hour: "2-digit", minute: "2-digit" }); } catch (e) { return new Date(ts).toLocaleTimeString(); } } function updateLastSavedIndicator(ts) { lastSavedEl.text(ts ? ("Cache: qeydkirî di saet " + formatSavedTime(ts) + " de") : ""); } // ── Dirty-state tracking + autosave ────────────────────────────────── // formDirty mirrors "does the live form differ from what's in cache". // Anything the person types/selects sets it; a successful save (auto // or manual) clears it; destroy() (final submit, or switching to the // source editor) also clears it since the wizard's job is over at // that point either way. formDirty/autosaveDebounceTimer/autosaveInterval // are declared in the outer scope (near activeGroup) — assigned here, // not re-declared — so destroy() can reach them too. var AUTOSAVE_DEBOUNCE_MS = 8000; // save shortly after typing pauses var AUTOSAVE_INTERVAL_MS = 60000; // …and at least this often regardless function performAutosave() { if (!ctx.formDirty) return; saveCurrentSection(); try { formCache.Set(buildCacheSnapshot()); ctx.formDirty = false; updateLastSavedIndicator(Date.now()); } catch (e) { // Silent on autosave failure (storage blocked/full, etc.) — the // manual button still surfaces a proper error if the person // tries it directly, no need to interrupt typing with a toast. } } function scheduleAutosave() { ctx.formDirty = true; if (ctx.autosaveDebounceTimer) clearTimeout(ctx.autosaveDebounceTimer); ctx.autosaveDebounceTimer = setTimeout(performAutosave, AUTOSAVE_DEBOUNCE_MS); } startForm.on("input change", "input, textarea, select", scheduleAutosave); ctx.autosaveInterval = setInterval(performAutosave, AUTOSAVE_INTERVAL_MS); // Warn on accidental tab close/reload while there's anything unsaved. $(window).on("beforeunload.wiktkuUnsaved", function (e) { if (!ctx.formDirty) return undefined; e.preventDefault(); e.returnValue = ""; return ""; }); var saveCacheBtn = new OO.ui.ButtonWidget({ label: "Li cache qeyd bike", title: "Halê vê formê yê niha (ziman, bilêvkirin û hemû beş) li cache (localStorage) qeyd bike, da ku eger rûpel bêyî xwestin were girtin an ji nû ve were barkirin, tu karibî berdewam bikî." }); saveCacheBtn.on("click", function () { saveCurrentSection(); // ObjectStorage.Set() doesn't return a success/failure value, so // the only way to detect a real failure (storage blocked, quota // exceeded, JSON.stringify choking on something unexpected) is to // catch it here rather than inspect a (nonexistent) return value. try { var snapshot = buildCacheSnapshot(); formCache.Set(snapshot); ctx.formDirty = false; updateLastSavedIndicator(snapshot.savedAt); wiktkuNotify("Halê formê li cache hate qeydkirin.", { type: "success", tag: "wiktku-cache-save" }); } catch (e) { mw.notify("Qeydkirina li cache bi ser neket.", { type: "error", tag: "wiktku-cache-save" }); } }); var clearCacheBtn = new OO.ui.ButtonWidget({ label: "Cache paqij bike", flags: [ "destructive" ], title: "Datayên qeydkirî yên vê rûpelê ji cache jê bibe (forma ku niha xuya dike naguherîne)." }); clearCacheBtn.on("click", function () { formCache.Set({}); // Cancel the scheduled autosave, or it would rewrite the cache from the still-filled form. // The form is left clean, so autosave resumes only once something is edited again. if (ctx.autosaveDebounceTimer) { clearTimeout(ctx.autosaveDebounceTimer); ctx.autosaveDebounceTimer = null; } ctx.formDirty = false; updateLastSavedIndicator(null); wiktkuNotify("Cache hate paqijkirin.", { type: "success", tag: "wiktku-cache-clear" }); }); // Everything sits on the bar: no overflow menu, nothing a click away. // Settings button — opens the Tercîh dialog. Plain OOUI icon names depend on which icon-pack // module happens to be loaded, so this uses the actual Codex "settings" (color-subtle) SVG from // Commons directly, the same way the spinner above hotlinks its gif. var prefsBtn = $("<button></button>", { type: "button", "class": "wiktku-prefs-btn", title: "Tercîh — sazkariyên vê amûrê biguherîne", "aria-label": "Tercîh" }); prefsBtn.on("click", function (e) { e.preventDefault(); wiktkuOpenPrefsDialog(); }); clearCacheBtn.$element.addClass("wiktku-btn-danger"); saveCacheBtn.$element.addClass("wiktku-footer-cache"); clearCacheBtn.$element.addClass("wiktku-footer-cache"); footer.append(saveCacheBtn.$element) .append(clearCacheBtn.$element) .append(prefsBtn) .append($("<span class='wiktku-cache-note'></span>").append(lastSavedEl)); startForm.append(footer); // Moved off-screen rather than display:none. WikiEditor and the mobile editor measure // #wpTextbox1 while the wizard is up, and a hidden ancestor reports zero, which they then keep. // Off-screen keeps it laid out and measurable. $("#editform").addClass("wiktku-offscreen").before(startForm); // renderGroupTabs() looks up #wiktku-group-tabs via a live DOM // selector, so it must run after startForm is actually in the document. renderGroupTabs(); renderBesenDinPicker(); // One pass over the whole form: every select that hasn't already been // initialised, and isn't opted out with .wiktku-plain-select, gets // the searchable picker. initSelects(startForm); refresh(); // Give every draggable textarea a custom resize grip (works // consistently on Safari/iOS, unlike the native corner grip — see // the "Manual drag-to-resize handle" comment in styles.css), and // keep applying it to textareas added later on (new sense blocks, // quote-source fields, nimûne rows, …). wiktkuInitManualResize(startForm); new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { $(mutation.addedNodes).each(function () { if (this.nodeType !== 1) return; wiktkuInitManualResize(this); }); }); }).observe(startForm[0], { childList: true, subtree: true }); // Now that the full language list is loaded, silently restore any // draft the editor previously saved for this page. var cachedDraft = formCache.Get(); if (cachedDraft && cachedDraft.sections && cachedDraft.sections.length) { restoreFromCache(cachedDraft); } } return { build: build, restoreSection: restoreSection, saveCurrentSection: saveCurrentSection }; }; // </nowiki> l9lde63h0wlcvy0qbnu66bdr1zxu9c5 MediaWiki:Gadget-wikt.start-wizard-steps/sections.js 8 1359536 6903101 2026-11-10T21:26:13Z Wikihez 7344 Split the wizard's script pages under 100 KB; identifierOf shared from citoid-helper 6903101 javascript text/javascript // <nowiki> /* * Page: [[MediaWiki:Gadget-wikt.start-wizard-steps/sections.js]] * * The step definitions after the senses: the sections of an entry other than Mane. A file of the * wikt.start-wizard-steps module, called with the wizard's ctx and the steps object it adds to. */ module.exports = function (ctx, steps) { var GENDER_BASE_LABELS = ctx.GENDER_BASE_LABELS; var ZARAVA_LANG_CODES = ctx.ZARAVA_LANG_CODES; var decapitalize = ctx.decapitalize; var fetchCatSrcTemplates = ctx.fetchCatSrcTemplates; var getLangLabel = ctx.getLangLabel; var getValues = ctx.getValues; var initSelects = ctx.initSelects; var insertAtCursor = ctx.insertAtCursor; var langHasMultipleScripts = ctx.langHasMultipleScripts; var langsByCode = ctx.langsByCode; var makeWordListStep = ctx.makeWordListStep; var markTemplateOptions = ctx.markTemplateOptions; var populateMenus = ctx.populateMenus; var refresh = ctx.refresh; var senseHasExtraContent = ctx.senseHasExtraContent; var stripWikilinks = ctx.stripWikilinks; var tplNameFromSnippet = ctx.tplNameFromSnippet; var wiktkuCite = ctx.wiktkuCite; var wiktkuUtil = ctx.wiktkuUtil; var wiktkuWikitext = ctx.wiktkuWikitext; // The {{werger-ser}} boxes of a step that lists words under each sense (zarava, trans), below its heading. function glossBoxes(id, heading) { var stepDiv = $("#wiktku-start-" + id); var glosses = steps.trans.glossList(); // Every sense is a template sense — there's no real gloss // anywhere and no group in the DOM either (glossList() only // returns empty in that case), so skip the section entirely. if (!glosses.length) return ""; // A completely empty entry (nothing filled in dfn at all, no // şablon chosen either) still gets a placeholder section // to fill in later. Only // skip an individual placeholder gloss below when some other // sense actually has real content, making that particular // placeholder redundant. var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn")); var hasRegularMane = dfnSenses.some(function (s) { return !s.tpl && (s.mane !== "" || senseHasExtraContent(s)); }); // Each gloss now has its own group of translation rows — // attach every gloss's own words under its own werger-ser/-bin // pair instead of dumping everything under the first one. var blocks = $.map(glosses, function (gloss, gi) { var group = stepDiv.find(".wiktku-" + id + "-group[data-gloss-pos='" + gi + "']"); var rows = group.length ? steps[id].getGroupTranslations(group) : []; // Placeholder gloss (no real mane entered yet) with nothing // translated into it. If some other sense already has real // content, this placeholder is redundant — skip it. If the // whole entry is empty, keep it so the heading still gets a // section to fill in. if (!gloss.text && !rows.length && hasRegularMane) return undefined; if (rows.length) { // Sort by Kurdish sort key. rows.sort(function (a, b) { var aLang = langsByCode[a[0]] || {}; var bLang = langsByCode[b[0]] || {}; return (aLang.sortKey || "").localeCompare(bLang.sortKey || "", "ku"); }); } var wordLines = $.map(rows, function (row) { var lang = row[0]; var words = row[1]; var wikiWords = words.map(function (w) { return "{{W|" + lang + "|" + w + "}}"; }).join(", "); return "* {{Z|" + lang + "}}: " + wikiWords; }).join("\n"); var block = "{{werger-ser|" + (gloss.text || "?") + "|" + gloss.index + "}}"; if (wordLines) block += "\n" + wordLines; block += "\n{{werger-bin}}"; return block; }); if (!blocks.length) return ""; return "\n==== " + heading + " ====\n" + blocks.join("\n") + "\n"; } $.extend(steps, { tewandin: { id: "tewandin", name: "tewandin", question_fmt: 'Tewandina "' + ctx.pageName + '" binivîse', visible: function (values) { return values.lang && values.pos; }, build: function (stepDiv) { if (stepDiv.find(".wiktku-tewandin-wrap").length) return null; var wrap = $("<div class='wiktku-tewandin-wrap'></div>"); var ta = $("<textarea></textarea>") .attr("name", "wiktku-tewandin-" + $.now()) .attr("class", "wiktku-start-step-field wiktku-tewandin-text") .attr("rows", 4); wrap.append(ta); stepDiv.append(wrap); function autoPopulate() { if (!ta.length) return; var vals = getValues(); var c = vals.cinsiyet || ""; var p = vals.pos || ""; if (!ctx.pageName.includes(" ") && c && GENDER_BASE_LABELS[c] && (p === "navdêr" || p === "serenav") && vals.lang === "ku" && c !== "mndudil") { var label = (c === "mnmane") ? "mê û nêr" : GENDER_BASE_LABELS[c]; var out = "{{ku-tewîn-nav|" + label; if (p === "serenav") out += "|y"; out += "}}"; if (!$.trim(ta.val())) ta.val(out); } } // The example placeholder only fits Kurmancî nouns — showing it // for a verb or another language would suggest the wrong template. function updatePlaceholder() { if (!ta.length) return; var vals = getValues(); ta.attr("placeholder", (vals.lang === "ku" && vals.pos === "navdêr") ? "{{ku-tewîn-nav|nêr}}" : ""); } setTimeout(function () { autoPopulate(); updatePlaceholder(); $("#wiktku-start-form").on("change.tewandin", "#wiktku-start-lang-field, #wiktku-start-cinsiyet-field, #wiktku-start-pos-field", function () { autoPopulate(); updatePlaceholder(); }); }, 0); return ta; }, output: function () { var manualText = $.trim($("#wiktku-start-tewandin .wiktku-tewandin-text").val()); if (manualText) { return "\n==== Tewandin ====\n" + manualText + "\n"; } return ""; } }, // resim — one or more images resim: { id: "resim", name: "resim", question_fmt: "Resmekê lê zêde bike", visible: function (values) { return !!(values.lang && values.pos); }, // The rows are wikt.edit's, shared with DefSideBoxes, which offers the same three boxes. field: null, build: function (stepDiv) { if (stepDiv.find(".wiktku-resim-wrap").length) return null; steps.resim.field = wiktkuUtil.buildImageListField({ prefix: "wiktku-", rowClass: "wiktku-posparams-row", inputClass: "wiktku-start-step-field wiktku-start-narrow", pageName: ctx.pageName }); stepDiv.append($("<div class='wiktku-resim-wrap'></div>").append(steps.resim.field.$element)); return null; }, /** {file, caption, alt} per filled row, with the caption wrapped for a non-Kurdish entry. */ getImages: function (values) { var lang = (values && values.lang) || getValues().lang || ""; if (!steps.resim.field) return []; return $.map(steps.resim.field.values(), function (image) { return { file: image.file, caption: wiktkuWikitext.captionForLang(image.caption, lang), alt: image.alt, size: image.size, extra: image.extra }; }); }, output: function (values) { var images = steps.resim.getImages(values); if (!images.length) return ""; return $.grep($.map(images, wiktkuWikitext.buildImageLine), Boolean).join("\n") + "\n"; } }, // h-bnr — {{h-bnr|Page1|Page2|…}} prepended before the lang section (shared, not per-section) bnrh: { id: "bnrh", name: "herwiha binêre (h-bnr)", question_fmt: "Şablona h-bnr li qismê herî jor a rûpelê", //visible: function (values) { return !!values.lang; }, multiple: true, narrow: true, placeholder: "Ji bo 'test' bnr 'Test', 'teşt'", output: function (values) { var call = wiktkuWikitext.buildHbnrCall(values.bnrh); return call ? call + "\n" : ""; } }, bikaranin: { id: "bikaranin", name: "Notên bikaranînê", question_fmt: "Notên bikaranînê yên li ser vê kelîmeyê", visible: function (values) { return values.lang && values.pos; }, multiple: false, narrow: false, output: function (values) { var text = $.trim(values.bikaranin || ""); if (!text) return ""; return "\n==== Notên bikaranînê ====\n" + text + "\n"; }, }, xwendin: { id: "xwendin", name: "Bêhtir bixwîne", question_fmt: "Lînkên derveyî eleqedarî vê madeyê", visible: function (values) { return values.lang && values.pos; }, multiple: false, narrow: false, placeholder: function (values) { if (!values || !values.lang) { return `* {{wpedî}} ji bo ku meqaleya Wîkîpediyayê lînk bidî`; } const ziman = values.lang === "ku" ? "" : `|ziman=${values.lang}`; return `* {{wpedî${ziman}}} ji bo ku meqaleya Wîkîpediyayê lînk bidî`; }, output: function (values) { var text = $.trim(values.xwendin || ""); if (!text) return ""; return "\n==== Bêhtir bixwîne ====\n" + text + "\n"; }, }, alfabeyendin: { id: "alfabeyendin", name: "Bi alfabeyên din", question_fmt: function (values) { var base = "Nivîsîna vê kelîmeyê bi alfabeyên din" var langLabel = values.lang ? getLangLabel(values.lang) : ""; if (langLabel) { return base + " ên " + langLabel; } return base; }, // Latin-script Kurdish itself, and languages with only one script // on record, don't need this — but any language wikt.lang-metadata // lists more than one script for (its "sc") clearly does. visible: function (values) { if (!(values.lang && values.pos)) return false; var lang = values.lang; return lang === "ku" || ZARAVA_LANG_CODES.indexOf(lang) !== -1 || langHasMultipleScripts(lang); }, multiple: false, narrow: false, placeholder: "Ji bo kurmancî: * {{ku-ar|şiklê alfaba erebî}}", output: function (values) { var text = $.trim(values.alfabeyendin || ""); if (!text) return ""; return "\n==== Bi alfabeyên din ====\n" + text + "\n"; }, }, formalt: makeWordListStep("formalt", "Formên alternatîv ên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"), hevmane: makeWordListStep("hevmane", "Hevmaneyên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"), dijmane: makeWordListStep("dijmane", "Dijmaneyên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"), jornav: makeWordListStep("jornav", "Jornavên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"), jernav: makeWordListStep("jernav", "Jêrnavên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"), je: makeWordListStep("je", "Kelîmeyên jê çêbûyî binivîse"), tekildar: makeWordListStep("tekildar", "Kelîmeyên ji aliyê etîmolojiyê ve têkildarî vê kelîmeyê binivîse"), binereherwiha: makeWordListStep("binereherwiha", "Kelîmeyên din ên eleqedar binivîse"), etym: { id: "etym", name: "etîmolojî", question_fmt: "Etîmolojiya kelîmeyê binivîse", // etymTpls populated from data-main.json etymTpls: {}, visible: function (values) { return values.lang && values.pos; }, // Fills the insert-dropdown from the current etymTpls object. refreshTplSelect: function () { var sel = $("#wiktku-start-etym .wiktku-tpl-insert-select"); if (!sel.length) return; sel.find("option[value!='']").remove(); $.each(steps.etym.etymTpls, function (name, tpl) { sel.append($("<option></option>").val(tpl.snippet).text(name + " — " + tpl.desc)); }); markTemplateOptions(sel, function (opt) { return tplNameFromSnippet(opt.val()); }); // Tell the picker the option list changed. sel.trigger("wiktku:refresh"); }, build: function (stepDiv) { if (stepDiv.find(".wiktku-etym-wrap").length) return null; var wrap = $("<div class='wiktku-etym-wrap'></div>"); var ta = $("<textarea></textarea>") .attr("name", "wiktku-etym-" + $.now()) .attr("class", "wiktku-start-step-field wiktku-etym-text") .attr("rows", 4) .attr("placeholder", "Ji {{ji|en|ku|apple}}. yan rasterast binivîse…"); // Template insert dropdown — options also refreshed after data-main.json loads. var sel = $("<select class='wiktku-tpl-insert-select'></select>"); sel.attr("data-placeholder", wiktkuUtil.msg.addTemplate); sel.append($("<option value=''>— şablonek lê zêde bike —</option>")); $.each(steps.etym.etymTpls, function (name, tpl) { sel.append($("<option></option>").val(tpl.snippet).text(name + " — " + tpl.desc)); }); sel.on("change", function () { var snippet = $(this).val(); if (!snippet) return; var lang = getValues().lang || ""; snippet = snippet.replace(/CODE/g, lang).replace(/SOURCELANG/g, "?"); $(this).val(""); sel.trigger("wiktku:refresh"); // Same param dialog DefSideBoxes' own Etîmolojî editor opens on a fresh insert: filled // from the template's own TemplateData, with CODE/SOURCELANG already substituted above // as the fields' starting values, so the user only has to replace the "?" placeholders // rather than hunt for them in raw wikitext afterward. wiktkuCite.openTemplateDialog(snippet).then(function (edited) { if (!edited) return; // cancelled, or "Jê bibe" on a call that was never inserted insertAtCursor(ta[0], edited); ta.focus(); }); }); wrap.append(sel); wrap.append(ta); stepDiv.append(wrap); initSelects(wrap); return ta; }, output: function (values) { var text = $.trim($("#wiktku-start-etym .wiktku-etym-text").val()); if (text) { return "\n==== Etîmolojî ====\n" + text + "\n"; } // {{ji?}} (a redirect to {{bêetîmolojî}}) is only ever added automatically to Kurdish // entries. if (values.lang !== "ku") return ""; // Multi-word page names skip the {{ji?|lang}} placeholder entirely -- "source unknown?" doesn't apply to a phrase. if (ctx.pageName.includes(" ")) return ""; // No etymology entered — emit {{ji?|lang}} as a placeholder. // Skip it only when every filled sense is a şablon sense // a completely empty entry with no şablon chosen still gets the placeholder var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn")); var hasFilledTplSense = dfnSenses.some(function (s) { return !!s.tpl && !!s.tplTitle; }); var hasRegularMane = dfnSenses.some(function (s) { return !s.tpl && (s.mane !== "" || senseHasExtraContent(s)); }); if (hasFilledTplSense && !hasRegularMane) return ""; return "\n==== Etîmolojî ====\n{{ji?|" + values.lang + "}}\n"; } }, dunde: { id: "dunde", name: "dûnde", question_fmt: "Eger ev kelîme ji vî zimanî ketibe zimanekî din, vî zimanî û kelîmeya wî binivîse", visible: function (values) { return !!values.pos; }, disabledKeys: function (values) { return {"navz": true, [values.lang]: true} }, // Language dropdown + growing word-input list; a new row appends // when the dropdown gets a value. Mirrors steps.trans, but flat. buildRow: function (stepDiv) { var row = $("<li class='wiktku-dunde-row wiktku-lang-row'></li>"); // Language selector — the searchable picker is applied after insertion into DOM. var langSel = $(mw.html.element("select", { "class": "wiktku-start-step-keyfield wiktku-start-narrow" })); langSel.attr("data-placeholder", "Li zimanekî bigere"); populateMenus(langSel, steps.dunde.keys || [], steps.dunde.disabledKeys); // When a language is chosen, ensure a next empty row exists. langSel.on("change", function () { steps.dunde.onLangChange(stepDiv, row); }); // Word list for this language. var wordList = $("<ul class='wiktku-dunde-words wiktku-lang-row-words'></ul>"); var firstWord = steps.dunde.makeWordInput(); wordList.append($("<li></li>").append(firstWord)); // The searchable picker is attached by initSelects() once the row // is in the DOM (see onLangChange/build below). row.append(langSel).append(wordList); return row; }, makeWordInput: function () { var input = $("<textarea></textarea>") .attr("name", "wiktku-dunde-word-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("rows", 1) .attr("class", "wiktku-dfn-subfield wiktku-start-narrow") .attr("placeholder", "Kelîme…"); input.on("input", function () { var list = $(this).closest("ul.wiktku-dunde-words"); var item = $(this).parent(); var val = $.trim($(this).val()); if (val && item.is(list.children("li:last-child"))) { var next = steps.dunde.makeWordInput(); list.append($("<li></li>").append(next)); } }); return input; }, onLangChange: function (stepDiv, row) { var list = stepDiv.find("ul.wiktku-dunde-list"); var isLast = row.is(list.children("li.wiktku-dunde-row:last-child")); if (isLast) { var newRow = steps.dunde.buildRow(stepDiv); list.append(newRow); // Attach the searchable picker now the row is in the DOM. initSelects(newRow); } // Keep disabled keys in sync across all lang selectors. steps.dunde.syncDisabledKeys(stepDiv); }, /** Disables already-selected languages in all other dropdowns. */ syncDisabledKeys: function (stepDiv) { var taken = $.extend({}, steps.dunde.disabledKeys); stepDiv.find("li.wiktku-dunde-row select").each(function () { var v = $(this).val(); if (v) taken[v] = true; }); stepDiv.find("li.wiktku-dunde-row select").each(function () { var self = $(this); var own = self.val(); self.find("option").each(function () { var code = $(this).val(); if (!code) return; $(this).prop("disabled", taken[code] && code !== own); }); self.trigger("wiktku:refresh"); }); }, build: function (stepDiv) { if (stepDiv.find("ul.wiktku-dunde-list").length) return null; var list = $("<ul class='wiktku-dunde-list wiktku-lang-row-list'></ul>"); list.append(steps.dunde.buildRow(stepDiv)); stepDiv.append(list); // Attach the searchable picker now the select is in the DOM. initSelects(list); return null; }, /** Reads all [langcode, words[]] rows from the DOM. */ getRows: function (stepDiv) { var result = []; stepDiv.find("li.wiktku-dunde-row").each(function () { var lang = $(this).find("select").val(); if (!lang) return; var words = []; $(this).find("ul.wiktku-dunde-words textarea").each(function () { var v = $.trim($(this).val()); if (v) words.push(v); }); if (words.length) result.push([lang, words]); }); return result; }, output: function () { var stepDiv = $("#wiktku-start-dunde"); var rows = steps.dunde.getRows(stepDiv); if (!rows.length) return ""; var lines = $.grep($.map(rows, function (row) { return wiktkuWikitext.buildDundeLine(row[0], row[1]); }), Boolean); return "\n==== Dûnde ====\n" + lines.join("\n") + "\n"; } }, zarava: { id: "zarava", name: "bi zaravayên din", question_fmt: "'"+ ctx.pageName + "' di zaravayên din de çawa tê gotin?", visible: function (values) { return steps.trans.visible(values); }, disabledKeys: {}, buildRow: function (group) { var row = $("<li class='wiktku-zarava-row wiktku-lang-row'></li>"); var langSel = $(mw.html.element("select", { "class": "wiktku-start-step-keyfield wiktku-start-narrow" })); langSel.attr("data-placeholder", "Li zaravayekî bigere"); populateMenus(langSel, steps.zarava.keys || [], steps.zarava.disabledKeys); langSel.on("change", function () { steps.zarava.onLangChange(group, row); refresh(); }); var wordList = $("<ul class='wiktku-zarava-words wiktku-lang-row-words'></ul>"); var firstWord = steps.zarava.makeWordInput(); wordList.append($("<li></li>").append(firstWord)); row.append(langSel).append(wordList); return row; }, makeWordInput: function () { var input = $("<textarea></textarea>") .attr("name", "wiktku-zarava-word-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("rows", 1) .attr("class", "wiktku-dfn-subfield wiktku-start-narrow") .attr("placeholder", "Tercime…"); input.on("input", function () { var list = $(this).closest("ul.wiktku-zarava-words"); var item = $(this).parent(); var val = $.trim($(this).val()); if (val && item.is(list.children("li:last-child"))) { var next = steps.zarava.makeWordInput(); list.append($("<li></li>").append(next)); } }); // Blur, not input -- refresh() re-syncs the whole form, same // reasoning as the Mane textareas' blur-only refresh below. input.on("blur", function () { refresh(); }); return input; }, onLangChange: function (group, row) { var list = group.find("ul.wiktku-zarava-list"); var isLast = row.is(list.children("li.wiktku-zarava-row:last-child")); if (isLast) { var newRow = steps.zarava.buildRow(group); list.append(newRow); // Attach the searchable picker now the row is in the DOM. initSelects(newRow); } steps.zarava.syncDisabledKeys(group); }, /** Disables already-selected dialects in all other dropdowns within one group. */ syncDisabledKeys: function (group) { var taken = $.extend({}, steps.zarava.disabledKeys); group.find("li.wiktku-zarava-row select").each(function () { var v = $(this).val(); if (v) taken[v] = true; }); group.find("li.wiktku-zarava-row select").each(function () { var self = $(this); var own = self.val(); self.find("option").each(function () { var code = $(this).val(); if (!code) return; $(this).prop("disabled", taken[code] && code !== own); }); self.trigger("wiktku:refresh"); }); }, /** Builds (or fetches) the .wiktku-zarava-group for one gloss. */ buildGroup: function (stepDiv, gi) { var group = $("<div class='wiktku-zarava-group'></div>").attr("data-gloss-pos", gi); var list = $("<ul class='wiktku-zarava-list wiktku-lang-row-list'></ul>"); list.append(steps.zarava.buildRow(group)); group.append(list); stepDiv.append(group); // Attach the searchable picker now the select is in the DOM. initSelects(list); return group; }, /** Syncs one .wiktku-zarava-group per current gloss (same logic as steps.trans.build). */ build: function (stepDiv) { var glosses = steps.trans.glossList(); if (!glosses.length) { stepDiv.find(".wiktku-zarava-group").remove(); return null; } var existingGroups = stepDiv.find(".wiktku-zarava-group"); if (existingGroups.length > glosses.length) { existingGroups.slice(glosses.length).remove(); } $.each(glosses, function (gi, gloss) { var group = stepDiv.find(".wiktku-zarava-group[data-gloss-pos='" + gi + "']"); if (!group.length) group = steps.zarava.buildGroup(stepDiv, gi); var label = group.find(".wiktku-zarava-group-label"); if (glosses.length > 1) { if (!label.length) { label = $("<div class='wiktku-zarava-group-label'></div>"); group.prepend(label); } label.text(gloss.index + " " + gloss.text); } else if (label.length) { label.remove(); } }); return null; }, /** Reads the dialect rows for a single gloss group. */ getGroupTranslations: function (group) { var result = []; group.find("li.wiktku-zarava-row").each(function () { var lang = $(this).find("select").val(); if (!lang) return; var words = []; $(this).find("ul.wiktku-zarava-words textarea").each(function () { var v = $.trim($(this).val()); if (v) words.push(v); }); if (words.length) result.push([lang, words]); }); return result; }, /** Reads all rows from the DOM, grouped by gloss position. */ getTranslations: function (stepDiv) { var result = []; stepDiv.find(".wiktku-zarava-group").each(function () { result.push(steps.zarava.getGroupTranslations($(this))); }); return result; }, output: function (values) { return values.lang === "ku" ? glossBoxes("zarava", "Bi zaravayên din") : ""; } }, trans: { id: "trans", name: "werger", question_fmt: "Tercimeyên vê maneyê bi zimanên din binivîse", visible: function (values) { if (!(values.lang === "ku" && values.pos)) return false; // Hide only when every sense is a filled template sense (no plain // gloss to attach translations to). A mix, or nothing filled in // yet, still shows Werger. var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn")); var hasRealSense = dfnSenses.some(function (s) { return !s.tpl && (s.mane !== "" || senseHasExtraContent(s)); }); var hasFilledTplSense = dfnSenses.some(function (s) { return s.tpl && s.tplTitle; }); return hasRealSense || !hasFilledTplSense; }, disabledKeys: { "navz": true, "ku": true, "ckb": true, "sdh": true, "lki": true, "hac": true, "zza": true }, // Language dropdown + growing word-input list, scoped to one gloss's // group (each gloss has its own group and row list). buildRow: function (group) { var row = $("<li class='wiktku-trans-row wiktku-lang-row'></li>"); // Language selector — the searchable picker is applied after insertion into DOM. var langSel = $(mw.html.element("select", { "class": "wiktku-start-step-keyfield wiktku-start-narrow" })); langSel.attr("data-placeholder", "Li zimanekî bigere"); populateMenus(langSel, steps.trans.keys || [], steps.trans.disabledKeys); // When a language is chosen, ensure a next empty row exists. langSel.on("change", function () { steps.trans.onLangChange(group, row); refresh(); }); // Word list for this language. var wordList = $("<ul class='wiktku-trans-words wiktku-lang-row-words'></ul>"); var firstWord = steps.trans.makeWordInput(); wordList.append($("<li></li>").append(firstWord)); // The searchable picker is attached by initSelects() once the row // is in the DOM (see onLangChange/buildGroup below). row.append(langSel).append(wordList); return row; }, makeWordInput: function () { var input = $("<textarea></textarea>") .attr("name", "wiktku-trans-word-" + $.now() + "-" + Math.random().toString(36).substr(2, 9)) .attr("rows", 1) .attr("class", "wiktku-dfn-subfield wiktku-start-narrow") .attr("placeholder", "Tercime…"); input.on("input", function () { var list = $(this).closest("ul.wiktku-trans-words"); var item = $(this).parent(); var val = $.trim($(this).val()); if (val && item.is(list.children("li:last-child"))) { var next = steps.trans.makeWordInput(); list.append($("<li></li>").append(next)); } }); // Blur, not input -- refresh() re-syncs the whole form, same // reasoning as the Mane textareas' blur-only refresh below. input.on("blur", function () { refresh(); }); return input; }, onLangChange: function (group, row) { var list = group.find("ul.wiktku-trans-list"); var isLast = row.is(list.children("li.wiktku-trans-row:last-child")); if (isLast) { var newRow = steps.trans.buildRow(group); list.append(newRow); // Attach the searchable picker now the row is in the DOM. initSelects(newRow); } // Keep disabled keys in sync across this gloss's lang selectors // only — the same language can be used for different senses. steps.trans.syncDisabledKeys(group); }, /** Disables already-selected languages in all other dropdowns within one group. */ syncDisabledKeys: function (group) { var taken = $.extend({}, steps.trans.disabledKeys); group.find("li.wiktku-trans-row select").each(function () { var v = $(this).val(); if (v) taken[v] = true; }); group.find("li.wiktku-trans-row select").each(function () { var self = $(this); var own = self.val(); self.find("option").each(function () { var code = $(this).val(); if (!code) return; $(this).prop("disabled", taken[code] && code !== own); }); self.trigger("wiktku:refresh"); }); }, // Builds (or fetches) the .wiktku-trans-group for one gloss. buildGroup: function (stepDiv, gi) { var group = $("<div class='wiktku-trans-group'></div>").attr("data-gloss-pos", gi); var list = $("<ul class='wiktku-trans-list wiktku-lang-row-list'></ul>"); list.append(steps.trans.buildRow(group)); group.append(list); stepDiv.append(group); // Attach the searchable picker now the select is in the DOM. initSelects(list); return group; }, // Syncs one .wiktku-trans-group per current gloss, keyed by // data-gloss-pos so groups stay matched across rebuilds. build: function (stepDiv) { var glosses = steps.trans.glossList(); if (!glosses.length) { stepDiv.find(".wiktku-trans-group").remove(); return null; } // Drop groups for glosses that no longer exist (always from the // tail — groups are only ever appended in gloss order below). var existingGroups = stepDiv.find(".wiktku-trans-group"); if (existingGroups.length > glosses.length) { existingGroups.slice(glosses.length).remove(); } $.each(glosses, function (gi, gloss) { var group = stepDiv.find(".wiktku-trans-group[data-gloss-pos='" + gi + "']"); if (!group.length) group = steps.trans.buildGroup(stepDiv, gi); // Only label groups when there's more than one gloss to // distinguish between — a single gloss needs no caption. var label = group.find(".wiktku-trans-group-label"); if (glosses.length > 1) { if (!label.length) { label = $("<div class='wiktku-trans-group-label'></div>"); group.prepend(label); } label.text(gloss.index + " " + gloss.text); } else if (label.length) { label.remove(); } }); return null; }, /** Reads the translation rows for a single gloss group. */ getGroupTranslations: function (group) { var result = []; group.find("li.wiktku-trans-row").each(function () { var lang = $(this).find("select").val(); if (!lang) return; var words = []; $(this).find("ul.wiktku-trans-words textarea").each(function () { var v = $.trim($(this).val()); if (v) words.push(v); }); if (words.length) result.push([lang, words]); }); return result; }, // Reads all translation rows, grouped by gloss position. getTranslations: function (stepDiv) { var result = []; stepDiv.find(".wiktku-trans-group").each(function () { result.push(steps.trans.getGroupTranslations($(this))); }); return result; }, // Builds the numbered "{ index, gloss }" list, one per dfn sense // with a real mane (skips template senses). index is 1-based and // matches the sense's position in the "#" definition list. // When nothing's filled in, returns one placeholder entry so // Werger still has a group to attach to; when every sense is a // filled template sense, stays empty (matches trans.visible()). glossList: function () { var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn")); var glosses = []; var hasFilledTplSense = false; $.each(dfnSenses, function (i, s) { if (s.tpl) { if (s.tplTitle) hasFilledTplSense = true; return; // template sense — no gloss to caption with } var dfn = s.mane; if (!dfn) { // No mane, but other fields are filled — still gets a // gloss group since dfn.getOutput() emits {{mane?}} for it. if (senseHasExtraContent(s)) glosses.push({ index: i + 1, text: "" }); return; } if (dfn[0] !== dfn[0].toLowerCase()) dfn = decapitalize(dfn); dfn = stripWikilinks(dfn).replace(/[.…?!)\]،؟]+$/, ""); glosses.push({ index: i + 1, text: dfn }); }); if (!glosses.length && !hasFilledTplSense) { glosses.push({ index: 1, text: "" }); } return glosses; }, output: function (values) { return values.lang === "ku" ? glossBoxes("trans", "Werger") : ""; } }, cavkani: { id: "cavkani", name: "çavkanî", question_fmt: "Çavkaniyên din (ne jêgirtin) lê zêde bike", visible: function (values) { return !!values.pos; }, // Fills the insert-dropdown from the members of [[Kategorî:Çavkanî]]. refreshTplSelect: function () { var sel = $("#wiktku-start-cavkani .wiktku-tpl-insert-select"); if (!sel.length) return; sel.find("option[value!='']").remove(); $.each(ctx.catSrcTemplates || [], function (_, entry) { sel.append($("<option></option>").val(entry.target).text(entry.label)); }); markTemplateOptions(sel, function (opt) { return opt.val(); }); // Tell the picker the option list changed. sel.trigger("wiktku:refresh"); }, build: function (stepDiv) { if (stepDiv.find(".wiktku-cavkani-wrap").length) return null; var wrap = $("<div class='wiktku-cavkani-wrap'></div>"); var ta = $("<textarea></textarea>") .attr("name", "wiktku-cavkani-" + $.now()) .attr("class", "wiktku-start-step-field wiktku-cavkani-text") .attr("rows", 4) .attr("placeholder", wiktkuUtil.msg.pickOrTypeTemplate); // Template insert dropdown, populated from [[Kategorî:Çavkanî]] — // options refreshed once the category members are fetched. var sel = $("<select class='wiktku-tpl-insert-select'></select>"); sel.attr("data-placeholder", wiktkuUtil.msg.addTemplate); sel.append($("<option value=''>— şablonek lê zêde bike —</option>")); var status = $("<span class='wiktku-cavkani-status'></span>"); // Same param dialog DefSideBoxes' own Çavkanî picker opens: the chosen template's // TemplateData becomes a form, so "rûpel=", "sernav=" and the rest are filled in before // anything lands in the box, instead of a bare {{Ç:…}} to go back and complete by hand. sel.on("change", function () { var name = $(this).val(); $(this).val(""); sel.trigger("wiktku:refresh"); if (!name) return; wiktkuCite.openTemplateDialog("{{" + name + "}}").then(function (edited) { if (!edited) { return; // cancelled, or removed a call that was never inserted } insertAtCursor(ta[0], "* " + edited + "\n"); ta.trigger("focus"); }); }); wrap.append(sel); wrap.append(status).append(ta); stepDiv.append(wrap); // build() runs again for every new beş (see the "+ Beşeke din" // handler), after the initial page-wide pass — so this select needs // its own init or it stays a plain, unsearchable native <select>. initSelects(wrap); if (ctx.catSrcTemplates) { steps.cavkani.refreshTplSelect(); } else { status.text(wiktkuUtil.msg.templatesLoading); fetchCatSrcTemplates().then(function () { steps.cavkani.refreshTplSelect(); status.text(""); }, function () { status.text(wiktkuUtil.msg.templatesFailed); }); } return ta; }, output: function () { var text = $.trim($("#wiktku-start-cavkani .wiktku-cavkani-text").val()); if (!text) return ""; return "\n==== Çavkanî ====\n" + text + "\n"; } }, }); }; // </nowiki> 41qu73uqjujtkmhmcblor40h5fepob6