mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-02-01 03:12:42 +01:00
Merge new release into master branch!
This commit is contained in:
commit
89ad3cb809
32 changed files with 2032 additions and 211 deletions
|
@ -1,3 +1,11 @@
|
||||||
|
# 1.6.3
|
||||||
|
* SECURITY: Update ejs
|
||||||
|
* SECURITY: xss vulnerability when reading window.location.href
|
||||||
|
* SECURITY: sanitize jsonp
|
||||||
|
* NEW: Catch SIGTERM for graceful shutdown
|
||||||
|
* NEW: Show actual applied text formatting for caret position
|
||||||
|
* NEW: Add settings to improve scrolling of viewport on line changes
|
||||||
|
|
||||||
# 1.6.2
|
# 1.6.2
|
||||||
* NEW: Added pad shortcut disabling feature
|
* NEW: Added pad shortcut disabling feature
|
||||||
* NEW: Create option to automatically reconnect after a few seconds
|
* NEW: Create option to automatically reconnect after a few seconds
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"node": ">=0.6.10"
|
"node": ">=0.6.10"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"marked": "~0.1.9"
|
"marked": ">=0.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {},
|
"devDependencies": {},
|
||||||
"optionalDependencies": {},
|
"optionalDependencies": {},
|
||||||
|
|
|
@ -150,6 +150,34 @@
|
||||||
/* Time (in seconds) to automatically reconnect pad when a "Force reconnect"
|
/* Time (in seconds) to automatically reconnect pad when a "Force reconnect"
|
||||||
message is shown to user. Set to 0 to disable automatic reconnection */
|
message is shown to user. Set to 0 to disable automatic reconnection */
|
||||||
"automaticReconnectionTimeout" : 0,
|
"automaticReconnectionTimeout" : 0,
|
||||||
|
/*
|
||||||
|
* By default, when caret is moved out of viewport, it scrolls the minimum height needed to make this
|
||||||
|
* line visible.
|
||||||
|
*/
|
||||||
|
"scrollWhenFocusLineIsOutOfViewport": {
|
||||||
|
/*
|
||||||
|
* Percentage of viewport height to be additionally scrolled.
|
||||||
|
* E.g use "percentage.editionAboveViewport": 0.5, to place caret line in the
|
||||||
|
* middle of viewport, when user edits a line above of the viewport
|
||||||
|
* Set to 0 to disable extra scrolling
|
||||||
|
*/
|
||||||
|
"percentage": {
|
||||||
|
"editionAboveViewport": 0,
|
||||||
|
"editionBelowViewport": 0
|
||||||
|
},
|
||||||
|
/* Time (in milliseconds) used to animate the scroll transition. Set to 0 to disable animation */
|
||||||
|
"duration": 0,
|
||||||
|
/*
|
||||||
|
* Flag to control if it should scroll when user places the caret in the last line of the viewport
|
||||||
|
*/
|
||||||
|
"scrollWhenCaretIsInTheLastLineOfViewport": false,
|
||||||
|
/*
|
||||||
|
* Percentage of viewport height to be additionally scrolled when user presses arrow up
|
||||||
|
* in the line of the top of the viewport.
|
||||||
|
* Set to 0 to let the scroll to be handled as default by the Etherpad
|
||||||
|
*/
|
||||||
|
"percentageToScrollWhenUserPressesArrowUp": 0
|
||||||
|
},
|
||||||
|
|
||||||
/* Users for basic authentication. is_admin = true gives access to /admin.
|
/* Users for basic authentication. is_admin = true gives access to /admin.
|
||||||
If you do not uncomment this, /admin will not be available! */
|
If you do not uncomment this, /admin will not be available! */
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
"Mushviq Abdulla",
|
"Mushviq Abdulla",
|
||||||
"Wertuose",
|
"Wertuose",
|
||||||
"Mastizada",
|
"Mastizada",
|
||||||
"Archaeodontosaurus"
|
"Archaeodontosaurus",
|
||||||
|
"Neriman2003"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Yeni lövhə",
|
"index.newPad": "Yeni lövhə",
|
||||||
|
@ -61,6 +62,8 @@
|
||||||
"pad.modals.connected": "Bağlandı.",
|
"pad.modals.connected": "Bağlandı.",
|
||||||
"pad.modals.reconnecting": "Sizin lövhə yenidən qoşulur..",
|
"pad.modals.reconnecting": "Sizin lövhə yenidən qoşulur..",
|
||||||
"pad.modals.forcereconnect": "Məcbur təkrarən bağlan",
|
"pad.modals.forcereconnect": "Məcbur təkrarən bağlan",
|
||||||
|
"pad.modals.reconnecttimer": "Yenidən qoşulur",
|
||||||
|
"pad.modals.cancel": "Ləğv et",
|
||||||
"pad.modals.userdup": "Başqa pəncərədə artıq açıqdır",
|
"pad.modals.userdup": "Başqa pəncərədə artıq açıqdır",
|
||||||
"pad.modals.userdup.explanation": "Bu lövhə, ola bilsin ki, bu kompüterdəki brauzerin bir neçə pəncərəsində açılmışdır.",
|
"pad.modals.userdup.explanation": "Bu lövhə, ola bilsin ki, bu kompüterdəki brauzerin bir neçə pəncərəsində açılmışdır.",
|
||||||
"pad.modals.userdup.advice": "Bu pəncərəni istifadə etmək üçün yenidən qoşul.",
|
"pad.modals.userdup.advice": "Bu pəncərəni istifadə etmək üçün yenidən qoşul.",
|
||||||
|
|
|
@ -57,10 +57,11 @@
|
||||||
"pad.importExport.exportword": "Microsoft Word",
|
"pad.importExport.exportword": "Microsoft Word",
|
||||||
"pad.importExport.exportpdf": "PDF",
|
"pad.importExport.exportpdf": "PDF",
|
||||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||||
"pad.importExport.abiword.innerHTML": "Teyna duz metini yana html formati şıma şenê azete dê. Dehana vêşi xısusiyetanê azere kerdışi rê grey <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">AbiWord'i bar kerên</a>.",
|
"pad.importExport.abiword.innerHTML": "Şıma şenê tenya metınanê zelalan ya zi formatanê HTML-i biyarê. Seba vêşi xısusiyetanê arezekerdışi ra gırey <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">AbiWord-i bar kerên</a>.",
|
||||||
"pad.modals.connected": "Gırediya.",
|
"pad.modals.connected": "Gırediya.",
|
||||||
"pad.modals.reconnecting": "Bloknot da şıma rê fına irtibat kewê no",
|
"pad.modals.reconnecting": "Bloknot da şıma rê fına irtibat kewê no",
|
||||||
"pad.modals.forcereconnect": "Mecbur anciya gırê de",
|
"pad.modals.forcereconnect": "Mecbur anciya gırê de",
|
||||||
|
"pad.modals.reconnecttimer": "Anciya gırê beno",
|
||||||
"pad.modals.cancel": "Bıtexelne",
|
"pad.modals.cancel": "Bıtexelne",
|
||||||
"pad.modals.userdup": "Zewbina pençere de bi a",
|
"pad.modals.userdup": "Zewbina pençere de bi a",
|
||||||
"pad.modals.userdup.explanation": "Ena bloknot ena komputer de yew ra zeder penceran dı akerde asena",
|
"pad.modals.userdup.explanation": "Ena bloknot ena komputer de yew ra zeder penceran dı akerde asena",
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
"pad.colorpicker.save": "Guardar",
|
"pad.colorpicker.save": "Guardar",
|
||||||
"pad.colorpicker.cancel": "Cancelar",
|
"pad.colorpicker.cancel": "Cancelar",
|
||||||
"pad.loading": "Cargando...",
|
"pad.loading": "Cargando...",
|
||||||
"pad.noCookie": "La cookie no se pudo encontrar. ¡Habilita las cookies en tu navegador!",
|
"pad.noCookie": "No se pudo encontrar la «cookie». Permite la utilización de «cookies» en el navegador.",
|
||||||
"pad.passwordRequired": "Necesitas una contraseña para acceder a este pad",
|
"pad.passwordRequired": "Necesitas una contraseña para acceder a este pad",
|
||||||
"pad.permissionDenied": "No tienes permiso para acceder a este pad",
|
"pad.permissionDenied": "No tienes permiso para acceder a este pad",
|
||||||
"pad.wrongPassword": "La contraseña era incorrecta",
|
"pad.wrongPassword": "La contraseña era incorrecta",
|
||||||
|
|
|
@ -13,7 +13,8 @@
|
||||||
"Macofe",
|
"Macofe",
|
||||||
"MrTapsa",
|
"MrTapsa",
|
||||||
"Silvonen",
|
"Silvonen",
|
||||||
"Espeox"
|
"Espeox",
|
||||||
|
"Pyscowicz"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Uusi muistio",
|
"index.newPad": "Uusi muistio",
|
||||||
|
@ -68,6 +69,8 @@
|
||||||
"pad.modals.connected": "Yhdistetty.",
|
"pad.modals.connected": "Yhdistetty.",
|
||||||
"pad.modals.reconnecting": "Muodostetaan yhteyttä muistioon uudelleen...",
|
"pad.modals.reconnecting": "Muodostetaan yhteyttä muistioon uudelleen...",
|
||||||
"pad.modals.forcereconnect": "Pakota yhdistämään uudelleen",
|
"pad.modals.forcereconnect": "Pakota yhdistämään uudelleen",
|
||||||
|
"pad.modals.reconnecttimer": "Yritetään yhdistää uudelleen",
|
||||||
|
"pad.modals.cancel": "Peruuta",
|
||||||
"pad.modals.userdup": "Avattu toisessa ikkunassa",
|
"pad.modals.userdup": "Avattu toisessa ikkunassa",
|
||||||
"pad.modals.userdup.explanation": "Tämä muistio vaikuttaa olevan avoinna useammassa eri selainikkunassa tällä koneella.",
|
"pad.modals.userdup.explanation": "Tämä muistio vaikuttaa olevan avoinna useammassa eri selainikkunassa tällä koneella.",
|
||||||
"pad.modals.userdup.advice": "Yhdistä uudelleen, jos haluat käyttää tätä ikkunaa.",
|
"pad.modals.userdup.advice": "Yhdistä uudelleen, jos haluat käyttää tätä ikkunaa.",
|
||||||
|
|
43
src/locales/fy.json
Normal file
43
src/locales/fy.json
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"@metadata": {
|
||||||
|
"authors": [
|
||||||
|
"Robin van der Vliet"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pad.toolbar.bold.title": "Fet (Ctrl+B)",
|
||||||
|
"pad.toolbar.italic.title": "Kursyf (Ctrl+I)",
|
||||||
|
"pad.toolbar.underline.title": "Understreekje (Ctrl+U)",
|
||||||
|
"pad.toolbar.settings.title": "Ynstellingen",
|
||||||
|
"pad.colorpicker.save": "Bewarje",
|
||||||
|
"pad.colorpicker.cancel": "Annulearje",
|
||||||
|
"pad.settings.fontType.normal": "Normaal",
|
||||||
|
"pad.settings.fontType.monospaced": "Monospace",
|
||||||
|
"pad.settings.language": "Taal:",
|
||||||
|
"pad.importExport.exporthtml": "HTML",
|
||||||
|
"pad.importExport.exportword": "Microsoft Word",
|
||||||
|
"pad.importExport.exportpdf": "PDF",
|
||||||
|
"pad.modals.connected": "Ferbûn.",
|
||||||
|
"pad.modals.deleted": "Fuortsmiten.",
|
||||||
|
"pad.share.link": "Keppeling",
|
||||||
|
"timeslider.toolbar.authors": "Auteurs:",
|
||||||
|
"timeslider.toolbar.authorsList": "Gjin auteurs",
|
||||||
|
"timeslider.toolbar.exportlink.title": "Eksportearje",
|
||||||
|
"timeslider.version": "Ferzje {{version}}",
|
||||||
|
"timeslider.dateformat": "{{day}}-{{month}}-{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
||||||
|
"timeslider.month.january": "jannewaris",
|
||||||
|
"timeslider.month.february": "febrewaris",
|
||||||
|
"timeslider.month.march": "maart",
|
||||||
|
"timeslider.month.april": "april",
|
||||||
|
"timeslider.month.may": "maaie",
|
||||||
|
"timeslider.month.june": "juny",
|
||||||
|
"timeslider.month.july": "july",
|
||||||
|
"timeslider.month.august": "augustus",
|
||||||
|
"timeslider.month.september": "septimber",
|
||||||
|
"timeslider.month.october": "oktober",
|
||||||
|
"timeslider.month.november": "novimber",
|
||||||
|
"timeslider.month.december": "desimber",
|
||||||
|
"pad.userlist.unnamed": "sûnder namme",
|
||||||
|
"pad.userlist.guest": "Gast",
|
||||||
|
"pad.userlist.deny": "Wegerje",
|
||||||
|
"pad.userlist.approve": "Goedkarre"
|
||||||
|
}
|
39
src/locales/hi.json
Normal file
39
src/locales/hi.json
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"@metadata": {
|
||||||
|
"authors": [
|
||||||
|
"Sfic"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pad.toolbar.bold.title": "गहरा (Ctrl+B)",
|
||||||
|
"pad.toolbar.italic.title": "तिरछा (Ctrl+I)",
|
||||||
|
"pad.toolbar.strikethrough.title": "काटें (Ctrl+5)",
|
||||||
|
"pad.colorpicker.save": "सहेजें",
|
||||||
|
"pad.colorpicker.cancel": "रद्द करें",
|
||||||
|
"pad.loading": "लोड हो रहा है...",
|
||||||
|
"pad.settings.language": "भाषा:",
|
||||||
|
"pad.importExport.import_export": "आयात/निर्यात",
|
||||||
|
"pad.importExport.exportpdf": "पीडीएफ़",
|
||||||
|
"pad.modals.cancel": "रद्द करें",
|
||||||
|
"timeslider.toolbar.authors": "लेखक:",
|
||||||
|
"timeslider.toolbar.exportlink.title": "निर्यात",
|
||||||
|
"timeslider.version": "संस्करण {{version}}",
|
||||||
|
"timeslider.saved": "{{day}} {{month}} {{year}} सहेजा गया",
|
||||||
|
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
||||||
|
"timeslider.month.january": "जनवरी",
|
||||||
|
"timeslider.month.february": "फ़रवरी",
|
||||||
|
"timeslider.month.march": "मार्च",
|
||||||
|
"timeslider.month.april": "अप्रैल",
|
||||||
|
"timeslider.month.may": "मई",
|
||||||
|
"timeslider.month.june": "जून",
|
||||||
|
"timeslider.month.july": "जुलाई",
|
||||||
|
"timeslider.month.august": "अगस्त",
|
||||||
|
"timeslider.month.september": "सितम्बर",
|
||||||
|
"timeslider.month.october": "अक्टूबर",
|
||||||
|
"timeslider.month.november": "नवम्बर",
|
||||||
|
"timeslider.month.december": "दिसम्बर",
|
||||||
|
"pad.userlist.guest": "अतिथि",
|
||||||
|
"pad.impexp.importbutton": "अभी आयात करें",
|
||||||
|
"pad.impexp.importing": "आयात कर रहा...",
|
||||||
|
"pad.impexp.importfailed": "आयात विफल हुआ",
|
||||||
|
"pad.impexp.copypaste": "कृपया कॉपी पेस्ट करें"
|
||||||
|
}
|
|
@ -3,7 +3,8 @@
|
||||||
"authors": [
|
"authors": [
|
||||||
"Shirayuki",
|
"Shirayuki",
|
||||||
"Torinky",
|
"Torinky",
|
||||||
"Omotecho"
|
"Omotecho",
|
||||||
|
"Aefgh39622"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "新規作成",
|
"index.newPad": "新規作成",
|
||||||
|
@ -54,7 +55,7 @@
|
||||||
"pad.importExport.exportword": "Microsoft Word",
|
"pad.importExport.exportword": "Microsoft Word",
|
||||||
"pad.importExport.exportpdf": "PDF",
|
"pad.importExport.exportpdf": "PDF",
|
||||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||||
"pad.importExport.abiword.innerHTML": "プレーンテキストまたは HTML ファイルからのみインポートできます。より高度なインポート機能を使用するには、<a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">abiword をインストール</a>してください。",
|
"pad.importExport.abiword.innerHTML": "プレーンテキストまたは HTML ファイルからのみインポートできます。より高度なインポート機能を使用するには、<a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">AbiWord をインストール</a>してください。",
|
||||||
"pad.modals.connected": "接続されました。",
|
"pad.modals.connected": "接続されました。",
|
||||||
"pad.modals.reconnecting": "パッドに再接続中...",
|
"pad.modals.reconnecting": "パッドに再接続中...",
|
||||||
"pad.modals.forcereconnect": "強制的に再接続",
|
"pad.modals.forcereconnect": "強制的に再接続",
|
||||||
|
|
41
src/locales/krc.json
Normal file
41
src/locales/krc.json
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"@metadata": {
|
||||||
|
"authors": [
|
||||||
|
"Ernác"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pad.toolbar.settings.title": "Джарашдырыула",
|
||||||
|
"pad.colorpicker.save": "Сакъла",
|
||||||
|
"pad.loading": "Джюклениу...",
|
||||||
|
"pad.settings.fontType.normal": "Нормал",
|
||||||
|
"pad.settings.fontType.monospaced": "Монокенгликли",
|
||||||
|
"pad.settings.globalView": "Глобал кёрюнюу",
|
||||||
|
"pad.settings.language": "Тил:",
|
||||||
|
"pad.importExport.import_export": "Импорт/экспорт",
|
||||||
|
"pad.importExport.importSuccessful": "Тыйыншлы!",
|
||||||
|
"pad.importExport.exporthtml": "HTML",
|
||||||
|
"pad.importExport.exportplain": "Тюз текст",
|
||||||
|
"pad.importExport.exportword": "Microsoft Word",
|
||||||
|
"pad.importExport.exportpdf": "PDF",
|
||||||
|
"pad.importExport.exportopen": "ODF (OpenOffice'ни документи)",
|
||||||
|
"pad.chat": "Чат",
|
||||||
|
"timeslider.toolbar.returnbutton": "Документге",
|
||||||
|
"timeslider.toolbar.authors": "Авторла:",
|
||||||
|
"timeslider.toolbar.exportlink.title": "Эспорт эт",
|
||||||
|
"timeslider.version": "{{version}} версия",
|
||||||
|
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}.{{minutes}}.{{seconds}}",
|
||||||
|
"timeslider.month.january": "январь",
|
||||||
|
"timeslider.month.february": "февраль",
|
||||||
|
"timeslider.month.march": "март",
|
||||||
|
"timeslider.month.april": "апрель",
|
||||||
|
"timeslider.month.may": "май",
|
||||||
|
"timeslider.month.june": "июнь",
|
||||||
|
"timeslider.month.july": "июль",
|
||||||
|
"timeslider.month.august": "август",
|
||||||
|
"timeslider.month.september": "сентябрь",
|
||||||
|
"timeslider.month.october": "октябрь",
|
||||||
|
"timeslider.month.november": "ноябрь",
|
||||||
|
"timeslider.month.december": "декабрь",
|
||||||
|
"pad.userlist.guest": "Къонакъ",
|
||||||
|
"pad.impexp.importing": "Импорт этиу…"
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": [
|
||||||
"StefanusRA"
|
"StefanusRA",
|
||||||
|
"Empu"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Pad Anyar",
|
"index.newPad": "Pad Anyar",
|
||||||
|
@ -30,7 +31,7 @@
|
||||||
"pad.permissionDenied": "Rika ora duwe idin kanggo ngakses pad kiye",
|
"pad.permissionDenied": "Rika ora duwe idin kanggo ngakses pad kiye",
|
||||||
"pad.wrongPassword": "Tembung sandhine Rika salah",
|
"pad.wrongPassword": "Tembung sandhine Rika salah",
|
||||||
"pad.settings.padSettings": "Pangaturan Pad",
|
"pad.settings.padSettings": "Pangaturan Pad",
|
||||||
"pad.settings.myView": "Delengane Inyong",
|
"pad.settings.myView": "Delengané Inyong",
|
||||||
"pad.settings.stickychat": "Dopokan mesti nang layar",
|
"pad.settings.stickychat": "Dopokan mesti nang layar",
|
||||||
"pad.settings.colorcheck": "Authorship colors",
|
"pad.settings.colorcheck": "Authorship colors",
|
||||||
"pad.settings.linenocheck": "Nomer baris",
|
"pad.settings.linenocheck": "Nomer baris",
|
||||||
|
|
42
src/locales/nah.json
Normal file
42
src/locales/nah.json
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"@metadata": {
|
||||||
|
"authors": [
|
||||||
|
"Akapochtli",
|
||||||
|
"Taresi"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"index.newPad": "Yancuic Pad",
|
||||||
|
"index.createOpenPad": "auh xicchīhua/xictlapo cē Pad in ītōcā:",
|
||||||
|
"pad.toolbar.bold.title": "Tilāhuac (Ctrl+B)",
|
||||||
|
"pad.toolbar.italic.title": "Coltic (Ctrl+I)",
|
||||||
|
"pad.toolbar.underline.title": "Tlahuahuantli (Ctrl+U)",
|
||||||
|
"pad.toolbar.strikethrough.title": "Tlīlhuahuantli (Ctrl+5)",
|
||||||
|
"pad.toolbar.undo.title": "Xicmācuepa (Ctrl+Z)",
|
||||||
|
"pad.toolbar.redo.title": "Occeppa (Ctrl+Y)",
|
||||||
|
"pad.toolbar.settings.title": "Tlatlālīliztli",
|
||||||
|
"pad.colorpicker.save": "Xicpiya",
|
||||||
|
"pad.colorpicker.cancel": "Xiccāhua",
|
||||||
|
"pad.settings.padSettings": "Pad Ītlatlālīliz",
|
||||||
|
"pad.settings.myView": "Notlachiyaliz",
|
||||||
|
"pad.settings.language": "Tlahtōlli:",
|
||||||
|
"pad.importExport.exportetherpad": "Etherpad",
|
||||||
|
"pad.importExport.exporthtml": "HTML",
|
||||||
|
"pad.importExport.exportword": "Microsoft Word",
|
||||||
|
"pad.importExport.exportpdf": "PDF",
|
||||||
|
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||||
|
"pad.modals.deleted": "Omopohpoloh.",
|
||||||
|
"pad.modals.deleted.explanation": "Ōmopoloh inīn Pad.",
|
||||||
|
"timeslider.version": "Inīc {{version}} Cuepaliztli",
|
||||||
|
"timeslider.month.january": "Īccēmētztli",
|
||||||
|
"timeslider.month.february": "Īcōmemētztli",
|
||||||
|
"timeslider.month.march": "Īcyēyimētztli",
|
||||||
|
"timeslider.month.april": "Īcnāhuimētztli",
|
||||||
|
"timeslider.month.may": "Īcmācuīllimētztli",
|
||||||
|
"timeslider.month.june": "Īcchicuacemmētztli",
|
||||||
|
"timeslider.month.july": "Īcchicōmemētztli",
|
||||||
|
"timeslider.month.august": "Īcchicuēyimētztli",
|
||||||
|
"timeslider.month.september": "Īcchiucnāhuimētztli",
|
||||||
|
"timeslider.month.october": "Īcmahtlactlimētztli",
|
||||||
|
"timeslider.month.november": "Īcmahtlactlioncēmētztli",
|
||||||
|
"timeslider.month.december": "Īcmahtlactliomōmemētztli"
|
||||||
|
}
|
|
@ -56,7 +56,7 @@
|
||||||
"pad.importExport.exportword": "Microsoft Word",
|
"pad.importExport.exportword": "Microsoft Word",
|
||||||
"pad.importExport.exportpdf": "PDF",
|
"pad.importExport.exportpdf": "PDF",
|
||||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||||
"pad.importExport.abiword.innerHTML": "Du kan bare importere fra ren tekst eller HTML-formater. For mer avanserte importfunksjoner, <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">installer abiword</a>.",
|
"pad.importExport.abiword.innerHTML": "Du kan bare importere fra ren tekst eller HTML-formater. For mer avanserte importfunksjoner, <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">installer AbiWord</a>.",
|
||||||
"pad.modals.connected": "Tilkoblet.",
|
"pad.modals.connected": "Tilkoblet.",
|
||||||
"pad.modals.reconnecting": "Kobler til din blokk på nytt...",
|
"pad.modals.reconnecting": "Kobler til din blokk på nytt...",
|
||||||
"pad.modals.forcereconnect": "Tving gjenoppkobling",
|
"pad.modals.forcereconnect": "Tving gjenoppkobling",
|
||||||
|
|
|
@ -8,7 +8,8 @@
|
||||||
"Imperadeiro98",
|
"Imperadeiro98",
|
||||||
"Macofe",
|
"Macofe",
|
||||||
"Ti4goc",
|
"Ti4goc",
|
||||||
"Cainamarques"
|
"Cainamarques",
|
||||||
|
"Athena in Wonderland"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Nova Nota",
|
"index.newPad": "Nova Nota",
|
||||||
|
@ -33,12 +34,14 @@
|
||||||
"pad.colorpicker.save": "Gravar",
|
"pad.colorpicker.save": "Gravar",
|
||||||
"pad.colorpicker.cancel": "Cancelar",
|
"pad.colorpicker.cancel": "Cancelar",
|
||||||
"pad.loading": "A carregar…",
|
"pad.loading": "A carregar…",
|
||||||
|
"pad.noCookie": "O cookie não foi encontrado. Por favor, ative os cookies no seu navegador!",
|
||||||
"pad.passwordRequired": "Precisa de uma senha para aceder a este pad",
|
"pad.passwordRequired": "Precisa de uma senha para aceder a este pad",
|
||||||
"pad.permissionDenied": "Não tem permissão para aceder a este pad.",
|
"pad.permissionDenied": "Não tem permissão para aceder a este pad.",
|
||||||
"pad.wrongPassword": "A palavra-chave está errada",
|
"pad.wrongPassword": "A palavra-chave está errada",
|
||||||
"pad.settings.padSettings": "Configurações da Nota",
|
"pad.settings.padSettings": "Configurações da Nota",
|
||||||
"pad.settings.myView": "Minha vista",
|
"pad.settings.myView": "Minha vista",
|
||||||
"pad.settings.stickychat": "Bate-papo sempre no ecrã",
|
"pad.settings.stickychat": "Bate-papo sempre no ecrã",
|
||||||
|
"pad.settings.chatandusers": "Mostrar a conversação e os utilizadores",
|
||||||
"pad.settings.colorcheck": "Cores de autoria",
|
"pad.settings.colorcheck": "Cores de autoria",
|
||||||
"pad.settings.linenocheck": "Números de linha",
|
"pad.settings.linenocheck": "Números de linha",
|
||||||
"pad.settings.rtlcheck": "Ler o conteúdo da direita para a esquerda?",
|
"pad.settings.rtlcheck": "Ler o conteúdo da direita para a esquerda?",
|
||||||
|
@ -51,21 +54,34 @@
|
||||||
"pad.importExport.import": "Carregar qualquer ficheiro de texto ou documento",
|
"pad.importExport.import": "Carregar qualquer ficheiro de texto ou documento",
|
||||||
"pad.importExport.importSuccessful": "Bem sucedido!",
|
"pad.importExport.importSuccessful": "Bem sucedido!",
|
||||||
"pad.importExport.export": "Exportar a Nota atual como:",
|
"pad.importExport.export": "Exportar a Nota atual como:",
|
||||||
|
"pad.importExport.exportetherpad": "Etherpad",
|
||||||
"pad.importExport.exporthtml": "HTML",
|
"pad.importExport.exporthtml": "HTML",
|
||||||
"pad.importExport.exportplain": "Texto simples",
|
"pad.importExport.exportplain": "Texto simples",
|
||||||
"pad.importExport.exportword": "Microsoft Word",
|
"pad.importExport.exportword": "Microsoft Word",
|
||||||
"pad.importExport.exportpdf": "PDF",
|
"pad.importExport.exportpdf": "PDF",
|
||||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||||
|
"pad.importExport.abiword.innerHTML": "Só é possível importar texto sem formatação ou HTML. Para obter funcionalidades de importação mais avançadas, por favor <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instale o AbiWord</a>.",
|
||||||
"pad.modals.connected": "Ligado.",
|
"pad.modals.connected": "Ligado.",
|
||||||
"pad.modals.reconnecting": "Reconectando-se ao seu bloco…",
|
"pad.modals.reconnecting": "Reconectando-se ao seu bloco…",
|
||||||
"pad.modals.forcereconnect": "Forçar reconexão",
|
"pad.modals.forcereconnect": "Forçar reconexão",
|
||||||
|
"pad.modals.reconnecttimer": "A tentar religar",
|
||||||
|
"pad.modals.cancel": "Cancelar",
|
||||||
"pad.modals.userdup": "Aberto noutra janela",
|
"pad.modals.userdup": "Aberto noutra janela",
|
||||||
"pad.modals.userdup.explanation": "Este pad parece estar aberto em mais do que uma janela do navegador neste computador.",
|
"pad.modals.userdup.explanation": "Este pad parece estar aberto em mais do que uma janela do navegador neste computador.",
|
||||||
|
"pad.modals.userdup.advice": "Religar para utilizar esta janela.",
|
||||||
"pad.modals.unauth": "Não autorizado",
|
"pad.modals.unauth": "Não autorizado",
|
||||||
|
"pad.modals.unauth.explanation": "As suas permissões foram alteradas enquanto revia esta página. Tente religar.",
|
||||||
"pad.modals.looping.explanation": "Existem problemas de comunicação com o servidor de sincronização.",
|
"pad.modals.looping.explanation": "Existem problemas de comunicação com o servidor de sincronização.",
|
||||||
|
"pad.modals.looping.cause": "Talvez tenha ligado por um firewall ou proxy incompatível.",
|
||||||
"pad.modals.initsocketfail": "O servidor está inacessível.",
|
"pad.modals.initsocketfail": "O servidor está inacessível.",
|
||||||
"pad.modals.initsocketfail.explanation": "Não foi possível a conexão ao servidor de sincronização.",
|
"pad.modals.initsocketfail.explanation": "Não foi possível a conexão ao servidor de sincronização.",
|
||||||
|
"pad.modals.initsocketfail.cause": "Isto provavelmente ocorreu por um problema no seu navegador ou na sua ligação de Internet.",
|
||||||
"pad.modals.slowcommit.explanation": "O servidor não está a responder.",
|
"pad.modals.slowcommit.explanation": "O servidor não está a responder.",
|
||||||
|
"pad.modals.slowcommit.cause": "Isto pode ser por problemas com a ligação de rede.",
|
||||||
|
"pad.modals.badChangeset.explanation": "Uma edição que fez foi classificada como ilegal pelo servidor de sincronização.",
|
||||||
|
"pad.modals.badChangeset.cause": "Isto pode ocorrer devido a uma configuração errada do servidor ou algum outro comportamento inesperado. Por favor contacte o administrador, se acredita que é um erro. Tente religar para continuar a editar.",
|
||||||
|
"pad.modals.corruptPad.explanation": "A nota que está a tentar aceder está corrompida.",
|
||||||
|
"pad.modals.corruptPad.cause": "Isto pode ocorrer devido a uma configuração errada do servidor ou algum outro comportamento inesperado. Por favor contacte o administrador.",
|
||||||
"pad.modals.deleted": "Eliminado.",
|
"pad.modals.deleted": "Eliminado.",
|
||||||
"pad.modals.deleted.explanation": "Este pad foi removido.",
|
"pad.modals.deleted.explanation": "Este pad foi removido.",
|
||||||
"pad.modals.disconnected": "Você foi desconectado.",
|
"pad.modals.disconnected": "Você foi desconectado.",
|
||||||
|
@ -74,9 +90,11 @@
|
||||||
"pad.share": "Compartilhar este pad",
|
"pad.share": "Compartilhar este pad",
|
||||||
"pad.share.readonly": "Somente para leitura",
|
"pad.share.readonly": "Somente para leitura",
|
||||||
"pad.share.link": "Ligação",
|
"pad.share.link": "Ligação",
|
||||||
|
"pad.share.emebdcode": "Incorporar o URL",
|
||||||
"pad.chat": "Bate-papo",
|
"pad.chat": "Bate-papo",
|
||||||
"pad.chat.title": "Abrir o bate-papo para este pad.",
|
"pad.chat.title": "Abrir o bate-papo para este pad.",
|
||||||
"pad.chat.loadmessages": "Carregar mais mensagens",
|
"pad.chat.loadmessages": "Carregar mais mensagens",
|
||||||
|
"timeslider.pageTitle": "Linha do tempo de {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "Voltar ao pad",
|
"timeslider.toolbar.returnbutton": "Voltar ao pad",
|
||||||
"timeslider.toolbar.authors": "Autores:",
|
"timeslider.toolbar.authors": "Autores:",
|
||||||
"timeslider.toolbar.authorsList": "Sem Autores",
|
"timeslider.toolbar.authorsList": "Sem Autores",
|
||||||
|
@ -84,6 +102,9 @@
|
||||||
"timeslider.exportCurrent": "Exportar versão atual como:",
|
"timeslider.exportCurrent": "Exportar versão atual como:",
|
||||||
"timeslider.version": "Versão {{version}}",
|
"timeslider.version": "Versão {{version}}",
|
||||||
"timeslider.saved": "Gravado a {{day}} de {{month}} de {{ano}}",
|
"timeslider.saved": "Gravado a {{day}} de {{month}} de {{ano}}",
|
||||||
|
"timeslider.playPause": "Reproduzir / Pausar conteúdo do Pad",
|
||||||
|
"timeslider.backRevision": "Voltar a uma revisão anterior neste Pad",
|
||||||
|
"timeslider.forwardRevision": "Ir a uma revisão posterior neste Pad",
|
||||||
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
||||||
"timeslider.month.january": "Janeiro",
|
"timeslider.month.january": "Janeiro",
|
||||||
"timeslider.month.february": "Fevereiro",
|
"timeslider.month.february": "Fevereiro",
|
||||||
|
@ -97,7 +118,9 @@
|
||||||
"timeslider.month.october": "Outubro",
|
"timeslider.month.october": "Outubro",
|
||||||
"timeslider.month.november": "Novembro",
|
"timeslider.month.november": "Novembro",
|
||||||
"timeslider.month.december": "Dezembro",
|
"timeslider.month.december": "Dezembro",
|
||||||
|
"timeslider.unnamedauthors": "{{num}} {[plural(num) one: autor anónimo, other: autores anónimos ]}",
|
||||||
"pad.savedrevs.marked": "Esta revisão está agora marcada como gravada",
|
"pad.savedrevs.marked": "Esta revisão está agora marcada como gravada",
|
||||||
|
"pad.savedrevs.timeslider": "Pode consultar as revisões gravadas visitando a linha do tempo",
|
||||||
"pad.userlist.entername": "Insira o seu nome",
|
"pad.userlist.entername": "Insira o seu nome",
|
||||||
"pad.userlist.unnamed": "sem nome",
|
"pad.userlist.unnamed": "sem nome",
|
||||||
"pad.userlist.guest": "Convidado",
|
"pad.userlist.guest": "Convidado",
|
||||||
|
@ -107,8 +130,10 @@
|
||||||
"pad.impexp.importbutton": "Importar agora",
|
"pad.impexp.importbutton": "Importar agora",
|
||||||
"pad.impexp.importing": "Importando...",
|
"pad.impexp.importing": "Importando...",
|
||||||
"pad.impexp.confirmimport": "A importação de um ficheiro irá substituir o texto atual do pad. Tem certeza que deseja continuar?",
|
"pad.impexp.confirmimport": "A importação de um ficheiro irá substituir o texto atual do pad. Tem certeza que deseja continuar?",
|
||||||
"pad.impexp.padHasData": "Não fomos capazes de importar este arquivo porque este Pad já tinha alterações, por favor importe para um novo pad",
|
"pad.impexp.convertFailed": "Não foi possível importar este ficheiro. Utilize outro formato ou copie e insira manualmente",
|
||||||
|
"pad.impexp.padHasData": "Não fomos capazes de importar este ficheiro porque este Pad já tinha alterações, por favor importe para um novo pad",
|
||||||
"pad.impexp.uploadFailed": "O upload falhou. Por favor, tente novamente",
|
"pad.impexp.uploadFailed": "O upload falhou. Por favor, tente novamente",
|
||||||
"pad.impexp.importfailed": "A importação falhou",
|
"pad.impexp.importfailed": "A importação falhou",
|
||||||
"pad.impexp.copypaste": "Por favor, copie e cole"
|
"pad.impexp.copypaste": "Por favor, copie e cole",
|
||||||
|
"pad.impexp.exportdisabled": "A exportação no formato {{type}} está desativada. Por favor, contacte o administrador do sistema para mais informações."
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Nov dokument",
|
"index.newPad": "Nov dokument",
|
||||||
"index.createOpenPad": "ali pa odpri dokument z imenom:",
|
"index.createOpenPad": "ali pa ustvari/odpri dokument z imenom:",
|
||||||
"pad.toolbar.bold.title": "Krepko (Ctrl-B)",
|
"pad.toolbar.bold.title": "Krepko (Ctrl-B)",
|
||||||
"pad.toolbar.italic.title": "Ležeče (Ctrl-I)",
|
"pad.toolbar.italic.title": "Ležeče (Ctrl-I)",
|
||||||
"pad.toolbar.underline.title": "Podčrtano (Ctrl-U)",
|
"pad.toolbar.underline.title": "Podčrtano (Ctrl-U)",
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
"pad.toolbar.undo.title": "Razveljavi (Ctrl-Z)",
|
"pad.toolbar.undo.title": "Razveljavi (Ctrl-Z)",
|
||||||
"pad.toolbar.redo.title": "Ponovno uveljavi (Ctrl-Y)",
|
"pad.toolbar.redo.title": "Ponovno uveljavi (Ctrl-Y)",
|
||||||
"pad.toolbar.clearAuthorship.title": "Počisti barve avtorstva (Ctrl+Shift+C)",
|
"pad.toolbar.clearAuthorship.title": "Počisti barve avtorstva (Ctrl+Shift+C)",
|
||||||
"pad.toolbar.import_export.title": "Izvozi/Uvozi različne oblike zapisov",
|
"pad.toolbar.import_export.title": "Uvozi/Izvozi različne oblike zapisov",
|
||||||
"pad.toolbar.timeslider.title": "Časovni trak",
|
"pad.toolbar.timeslider.title": "Časovni trak",
|
||||||
"pad.toolbar.savedRevision.title": "Shrani redakcijo",
|
"pad.toolbar.savedRevision.title": "Shrani redakcijo",
|
||||||
"pad.toolbar.settings.title": "Nastavitve",
|
"pad.toolbar.settings.title": "Nastavitve",
|
||||||
|
|
|
@ -5,21 +5,22 @@
|
||||||
"Milicevic01",
|
"Milicevic01",
|
||||||
"Милан Јелисавчић",
|
"Милан Јелисавчић",
|
||||||
"Srdjan m",
|
"Srdjan m",
|
||||||
"Obsuser"
|
"Obsuser",
|
||||||
|
"Acamicamacaraca"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Нови Пад",
|
"index.newPad": "Нови Пад",
|
||||||
"index.createOpenPad": "или направите/отворите пад следећег назива:",
|
"index.createOpenPad": "или направите/отворите пад следећег назива:",
|
||||||
"pad.toolbar.bold.title": "Подебљано (Ctrl-B)",
|
"pad.toolbar.bold.title": "Подебљано (Ctrl+B)",
|
||||||
"pad.toolbar.italic.title": "Искошено (Ctrl-I)",
|
"pad.toolbar.italic.title": "Искошено (Ctrl+I)",
|
||||||
"pad.toolbar.underline.title": "Подвучено (Ctrl-U)",
|
"pad.toolbar.underline.title": "Подвучено (Ctrl+U)",
|
||||||
"pad.toolbar.strikethrough.title": "Прецртано (Ctrl+5)",
|
"pad.toolbar.strikethrough.title": "Прецртано (Ctrl+5)",
|
||||||
"pad.toolbar.ol.title": "Уређен списак (Ctrl+Shift+N)",
|
"pad.toolbar.ol.title": "Уређен списак (Ctrl+Shift+N)",
|
||||||
"pad.toolbar.ul.title": "Неуређен списак (Ctrl+Shift+L)",
|
"pad.toolbar.ul.title": "Неуређен списак (Ctrl+Shift+L)",
|
||||||
"pad.toolbar.indent.title": "Увлачење (TAB)",
|
"pad.toolbar.indent.title": "Увлачење (TAB)",
|
||||||
"pad.toolbar.unindent.title": "Извлачење (Shift+TAB)",
|
"pad.toolbar.unindent.title": "Извлачење (Shift+TAB)",
|
||||||
"pad.toolbar.undo.title": "Опозови (Ctrl+Z)",
|
"pad.toolbar.undo.title": "Опозови (Ctrl+Z)",
|
||||||
"pad.toolbar.redo.title": "Опозови (Ctrl+Z)",
|
"pad.toolbar.redo.title": "Понови (Ctrl+Z)",
|
||||||
"pad.toolbar.clearAuthorship.title": "Очисти ауторске боје (Ctrl+Shift+C)",
|
"pad.toolbar.clearAuthorship.title": "Очисти ауторске боје (Ctrl+Shift+C)",
|
||||||
"pad.toolbar.import_export.title": "Увези/извези из/на друге датотечне формате",
|
"pad.toolbar.import_export.title": "Увези/извези из/на друге датотечне формате",
|
||||||
"pad.toolbar.timeslider.title": "Временска линија",
|
"pad.toolbar.timeslider.title": "Временска линија",
|
||||||
|
@ -29,9 +30,9 @@
|
||||||
"pad.toolbar.showusers.title": "Прикажи кориснике на овом паду",
|
"pad.toolbar.showusers.title": "Прикажи кориснике на овом паду",
|
||||||
"pad.colorpicker.save": "Сачувај",
|
"pad.colorpicker.save": "Сачувај",
|
||||||
"pad.colorpicker.cancel": "Откажи",
|
"pad.colorpicker.cancel": "Откажи",
|
||||||
"pad.loading": "Учитавање...",
|
"pad.loading": "Учитавам…",
|
||||||
"pad.noCookie": "Колачић није пронађен. Молимо да укључите колачиће у вашем прегледавачу!",
|
"pad.noCookie": "Колачић није пронађен. Молимо да укључите колачиће у вашем прегледавачу!",
|
||||||
"pad.passwordRequired": "Требате лозинку како бисте приступили овом паду",
|
"pad.passwordRequired": "Требате имати лозинку како бисте приступили овом паду",
|
||||||
"pad.permissionDenied": "Немате дозволу да приступите овом паду",
|
"pad.permissionDenied": "Немате дозволу да приступите овом паду",
|
||||||
"pad.wrongPassword": "Ваша лозинка није исправна",
|
"pad.wrongPassword": "Ваша лозинка није исправна",
|
||||||
"pad.settings.padSettings": "Подешавања пада",
|
"pad.settings.padSettings": "Подешавања пада",
|
||||||
|
@ -48,15 +49,15 @@
|
||||||
"pad.settings.language": "Језик:",
|
"pad.settings.language": "Језик:",
|
||||||
"pad.importExport.import_export": "Увоз/извоз",
|
"pad.importExport.import_export": "Увоз/извоз",
|
||||||
"pad.importExport.import": "Отпремите било коју текстуалну датотеку или документ",
|
"pad.importExport.import": "Отпремите било коју текстуалну датотеку или документ",
|
||||||
"pad.importExport.importSuccessful": "Успело!",
|
"pad.importExport.importSuccessful": "Успешно!",
|
||||||
"pad.importExport.export": "Извези тренутни пад као:",
|
"pad.importExport.export": "Извези тренутни пад као:",
|
||||||
"pad.importExport.exportetherpad": "Etherpad",
|
"pad.importExport.exportetherpad": "Etherpad",
|
||||||
"pad.importExport.exporthtml": "HTML",
|
"pad.importExport.exporthtml": "HTML",
|
||||||
"pad.importExport.exportplain": "чист текст",
|
"pad.importExport.exportplain": "Чист текст",
|
||||||
"pad.importExport.exportword": "Microsoft Word",
|
"pad.importExport.exportword": "Microsoft Word",
|
||||||
"pad.importExport.exportpdf": "PDF",
|
"pad.importExport.exportpdf": "PDF",
|
||||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||||
"pad.importExport.abiword.innerHTML": "Једино можете увести са једноставног текстуалног формата или HTML формата. За компликованије функције о увозу, молимо да <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">инсталирате AbiWord</a>.",
|
"pad.importExport.abiword.innerHTML": "Једино можете увести са једноставног текстуалног формата или HTML формата. За компликованије функције о увозу, молимо да <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">инсталирате AbiWord</a>.",
|
||||||
"pad.modals.connected": "Повезано.",
|
"pad.modals.connected": "Повезано.",
|
||||||
"pad.modals.reconnecting": "Поново се повезујем на ваш пад..",
|
"pad.modals.reconnecting": "Поново се повезујем на ваш пад..",
|
||||||
"pad.modals.forcereconnect": "Присилно се поново повежи",
|
"pad.modals.forcereconnect": "Присилно се поново повежи",
|
||||||
|
@ -83,24 +84,24 @@
|
||||||
"pad.modals.disconnected": "Веза је прекинута.",
|
"pad.modals.disconnected": "Веза је прекинута.",
|
||||||
"pad.modals.disconnected.explanation": "Изгубљена је веза са сервером",
|
"pad.modals.disconnected.explanation": "Изгубљена је веза са сервером",
|
||||||
"pad.modals.disconnected.cause": "Сервер није доступан. Обавестите сервисног администратора ако се ово настави дешавати.",
|
"pad.modals.disconnected.cause": "Сервер није доступан. Обавестите сервисног администратора ако се ово настави дешавати.",
|
||||||
"pad.share": "Дели овај пад",
|
"pad.share": "Пофели овај пад",
|
||||||
"pad.share.readonly": "Само за читање",
|
"pad.share.readonly": "Само за читање",
|
||||||
"pad.share.link": "Веза",
|
"pad.share.link": "Веза",
|
||||||
"pad.share.emebdcode": "Угради везу",
|
"pad.share.emebdcode": "Угради везу",
|
||||||
"pad.chat": "Ћаскање",
|
"pad.chat": "Ћаскање",
|
||||||
"pad.chat.title": "Отворите ћаскање за овај пад.",
|
"pad.chat.title": "Отворите ћаскање за овај пад.",
|
||||||
"pad.chat.loadmessages": "Учитајте више порука.",
|
"pad.chat.loadmessages": "Учитај више порука",
|
||||||
"timeslider.pageTitle": "{{appTitle}} временска линија",
|
"timeslider.pageTitle": "{{appTitle}} временска линија",
|
||||||
"timeslider.toolbar.returnbutton": "Врати се на пад",
|
"timeslider.toolbar.returnbutton": "Врати се на пад",
|
||||||
"timeslider.toolbar.authors": "Аутори:",
|
"timeslider.toolbar.authors": "Аутори:",
|
||||||
"timeslider.toolbar.authorsList": "Нема аутора",
|
"timeslider.toolbar.authorsList": "Нема аутора",
|
||||||
"timeslider.toolbar.exportlink.title": "Извези",
|
"timeslider.toolbar.exportlink.title": "Извези",
|
||||||
"timeslider.exportCurrent": "Извези тренутну верзију као:",
|
"timeslider.exportCurrent": "Извези тренутну верзију као:",
|
||||||
"timeslider.version": "Верзија {{version}}",
|
"timeslider.version": "Издање {{version}}",
|
||||||
"timeslider.saved": "Сачувано на {{day}}. {{month}}. {{year}}",
|
"timeslider.saved": "Сачувано на {{day}}. {{month}}. {{year}}",
|
||||||
"timeslider.playPause": "Пусти/паузирај садржај пада",
|
"timeslider.playPause": "Пусти/паузирај садржај пада",
|
||||||
"timeslider.backRevision": "Иди на претходну верзију овог пада",
|
"timeslider.backRevision": "Иди на претходну верзију овог пада",
|
||||||
"timeslider.forwardRevision": "Иди на следећу верзију овог пада",
|
"timeslider.forwardRevision": "Иди на следеће издање пада",
|
||||||
"timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
"timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
||||||
"timeslider.month.january": "јануар",
|
"timeslider.month.january": "јануар",
|
||||||
"timeslider.month.february": "фебруар",
|
"timeslider.month.february": "фебруар",
|
||||||
|
@ -115,21 +116,21 @@
|
||||||
"timeslider.month.november": "новембар",
|
"timeslider.month.november": "новембар",
|
||||||
"timeslider.month.december": "децембар",
|
"timeslider.month.december": "децембар",
|
||||||
"timeslider.unnamedauthors": "{{num}} неименован(и) {[plural(num) one: аутор, other: аутори ]}",
|
"timeslider.unnamedauthors": "{{num}} неименован(и) {[plural(num) one: аутор, other: аутори ]}",
|
||||||
"pad.savedrevs.marked": "Ова верзија је сада означена као сачувана",
|
"pad.savedrevs.marked": "Ова измена је сада означена као сачувана",
|
||||||
"pad.savedrevs.timeslider": "Можете видети сачуване измене користећи се временском линијом",
|
"pad.savedrevs.timeslider": "Можете видети сачуване измене користећи се временском линијом",
|
||||||
"pad.userlist.entername": "Упишите своје име",
|
"pad.userlist.entername": "Упишите своје име",
|
||||||
"pad.userlist.unnamed": "нема имена",
|
"pad.userlist.unnamed": "неименован",
|
||||||
"pad.userlist.guest": "Гост",
|
"pad.userlist.guest": "Гост",
|
||||||
"pad.userlist.deny": "Одбиј",
|
"pad.userlist.deny": "Одбиј",
|
||||||
"pad.userlist.approve": "одобрено",
|
"pad.userlist.approve": "Одобри",
|
||||||
"pad.editbar.clearcolors": "Очисти ауторске боје за цели документ?",
|
"pad.editbar.clearcolors": "Очисти ауторске боје за цели документ?",
|
||||||
"pad.impexp.importbutton": "Увези одмах",
|
"pad.impexp.importbutton": "Увези одмах",
|
||||||
"pad.impexp.importing": "Увожење...",
|
"pad.impexp.importing": "Увозим...",
|
||||||
"pad.impexp.confirmimport": "Увоз датотеке ће преписати тренутни текст пада. Да ли сте сигурни да желите наставити?",
|
"pad.impexp.confirmimport": "Увоз датотеке ће преписати тренутни текст пада. Да ли сте сигурни да желите наставити?",
|
||||||
"pad.impexp.convertFailed": "Не можемо увести ову датотеку. Молимо да користите други формат документа или да документ копирате ручно",
|
"pad.impexp.convertFailed": "Не могу да увезем ову датотеку. Молимо да користите други формат документа или да документ копирате ручно",
|
||||||
"pad.impexp.padHasData": "Не можемо да увеземо ову датотеку зато што је већ било промена на овом паду, молимо да увезете нови пад",
|
"pad.impexp.padHasData": "Не могу да увезем ову датотеку зато што је већ било промена на овом паду, молимо да увезете нови пад",
|
||||||
"pad.impexp.uploadFailed": "Отпремање није успело, молимо да покушате поново",
|
"pad.impexp.uploadFailed": "Нисам успео да отпремим, молимо покушате поново",
|
||||||
"pad.impexp.importfailed": "Увоз неуспешан",
|
"pad.impexp.importfailed": "Нисам успео да увезем",
|
||||||
"pad.impexp.copypaste": "Молимо да ручно копирате",
|
"pad.impexp.copypaste": "Копирајте и залепите",
|
||||||
"pad.impexp.exportdisabled": "Извоз у формату {{type}} није дозвољен. Контактирајте системског администратора за детаље."
|
"pad.impexp.exportdisabled": "Извоз у формату {{type}} није дозвољен. Контактирајте системског администратора за детаље."
|
||||||
}
|
}
|
||||||
|
|
48
src/locales/tcy.json
Normal file
48
src/locales/tcy.json
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
{
|
||||||
|
"@metadata": {
|
||||||
|
"authors": [
|
||||||
|
"BHARATHESHA ALASANDEMAJALU",
|
||||||
|
"VASANTH S.N."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"index.newPad": "ಪೊಸ ಪ್ಯಾಡ್",
|
||||||
|
"index.createOpenPad": "ಅತಂಡ ಈ ಪುದರ್ತ ಪ್ಯಾಡನ್ನು ಉಂಡು ಮನ್ಪು/ತೋಜಾಲ:",
|
||||||
|
"pad.toolbar.bold.title": "ದಪ್ಪೊ(Ctrl+B)",
|
||||||
|
"pad.toolbar.italic.title": "ಓರೆ (Ctrl-I)",
|
||||||
|
"pad.toolbar.underline.title": "ಅಡಿಗೆರೆ(Ctrl-U)",
|
||||||
|
"pad.toolbar.indent.title": "Indent (TAB)",
|
||||||
|
"pad.toolbar.undo.title": "ಪಿರವುತ(Ctrl+Z)",
|
||||||
|
"pad.toolbar.redo.title": "ದುಂಬುತ್ತ(Ctrl+Y)",
|
||||||
|
"pad.toolbar.settings.title": "ಸಂಯೋಜನೆಲು",
|
||||||
|
"pad.toolbar.showusers.title": "ಈ ಪ್ಯಾಡ್ ಟ್ ಗಲಸುನಾಯಾನ್ ತೋಜಾಲೆ",
|
||||||
|
"pad.colorpicker.save": "ಒರಿಪಾಲೆ",
|
||||||
|
"pad.colorpicker.cancel": "ವಜಾ ಮಲ್ಪುಲೆ",
|
||||||
|
"pad.loading": "ದಿಂಜಾವೊಂದುಂಡು......",
|
||||||
|
"pad.wrongPassword": "ಇರೇನಾ ಪಾಸ್ ವರ್ಡ್ ತಪ್ಪತುಂಡ್",
|
||||||
|
"pad.settings.padSettings": "ಪ್ಯಾಡ್ ಸಂಯೋಜನೆ",
|
||||||
|
"pad.settings.language": "ಬಾಸೆ:",
|
||||||
|
"pad.importExport.exportetherpad": "Etherpad",
|
||||||
|
"pad.importExport.exporthtml": "HTML",
|
||||||
|
"pad.importExport.exportpdf": "PDF",
|
||||||
|
"pad.modals.connected": "ನೆಟ್ ವರ್ಕ್ ತಿಕೊಂತುಂಡು.",
|
||||||
|
"pad.modals.cancel": "ವಜಾ ಮಲ್ಪುಲೆ",
|
||||||
|
"pad.modals.deleted": "ಮಾಜಾಯಿನ.",
|
||||||
|
"pad.share.readonly": "ಓದ್ಯರಾ ಮಾತ್ರ",
|
||||||
|
"pad.share.link": "ಕೊಂಡಿಲು",
|
||||||
|
"timeslider.month.january": "ಜನವರಿ",
|
||||||
|
"timeslider.month.february": "ಪೆಬ್ರವರಿ",
|
||||||
|
"timeslider.month.march": "ಮಾರ್ಚಿ",
|
||||||
|
"timeslider.month.april": "ಎಪ್ರಿಲ್",
|
||||||
|
"timeslider.month.may": "ಮೇ",
|
||||||
|
"timeslider.month.june": "ಜೂನ್",
|
||||||
|
"timeslider.month.july": "ಜುಲಾಯಿ",
|
||||||
|
"timeslider.month.august": "ಆಗೋಸ್ಟು",
|
||||||
|
"timeslider.month.september": "ಸಪ್ಟಂಬರೊ",
|
||||||
|
"timeslider.month.october": "ಅಕ್ಟೋಬರ",
|
||||||
|
"timeslider.month.november": "ನವಂಬರೊ",
|
||||||
|
"timeslider.month.december": "ದಸಂಬರೊ",
|
||||||
|
"pad.userlist.entername": "ಈರೆನೆ ಪುದರ್ ಬರೆಲೆ",
|
||||||
|
"pad.userlist.unnamed": "ಪುದರ್ ಇಜ್ಜಂತಿನವು",
|
||||||
|
"pad.userlist.guest": "ಬಿನ್ನೆರ್",
|
||||||
|
"pad.userlist.approve": "ಒಪ್ಪಂದ ಅಂಡ್"
|
||||||
|
}
|
129
src/locales/th.json
Normal file
129
src/locales/th.json
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
{
|
||||||
|
"@metadata": {
|
||||||
|
"authors": [
|
||||||
|
"Aefgh39622"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"index.newPad": "สร้างแผ่นจดบันทึกใหม่",
|
||||||
|
"index.createOpenPad": "หรือสร้าง/เปิดแผ่นจดบันทึกที่มีชื่อ:",
|
||||||
|
"pad.toolbar.bold.title": "ตัวหนา (Ctrl+B)",
|
||||||
|
"pad.toolbar.italic.title": "ตัวเอียง (Ctrl+I)",
|
||||||
|
"pad.toolbar.underline.title": "ขีดเส้นใต้ (Ctrl+U)",
|
||||||
|
"pad.toolbar.strikethrough.title": "ขีดทับ (Ctrl+5)",
|
||||||
|
"pad.toolbar.ol.title": "รายการที่เรียงลำดับ (Ctrl+Shift+N)",
|
||||||
|
"pad.toolbar.ul.title": "รายการที่ไม่เรียงลำดับ (Ctrl+Shift+L)",
|
||||||
|
"pad.toolbar.indent.title": "เยื้องเข้า (TAB)",
|
||||||
|
"pad.toolbar.unindent.title": "เยื้องออก (Shift+TAB)",
|
||||||
|
"pad.toolbar.undo.title": "เลิกทำ (Ctrl+Z)",
|
||||||
|
"pad.toolbar.redo.title": "ทำซ้ำ (Ctrl+Y)",
|
||||||
|
"pad.toolbar.clearAuthorship.title": "ลบสีผู้เขียน (Ctrl+Shift+C)",
|
||||||
|
"pad.toolbar.import_export.title": "นำเข้า/ส่งออกไฟล์จาก/เป็นรูปแบบต่าง ๆ",
|
||||||
|
"pad.toolbar.timeslider.title": "ตัวเลื่อนเวลา",
|
||||||
|
"pad.toolbar.savedRevision.title": "บันทึกรุ่นแก้ไข",
|
||||||
|
"pad.toolbar.settings.title": "การตั้งค่า",
|
||||||
|
"pad.toolbar.embed.title": "แชร์และฝังแผ่นจดบันทึกนี้",
|
||||||
|
"pad.toolbar.showusers.title": "แสดงผู้ใช้บนแผ่นจดบันทึกนี้",
|
||||||
|
"pad.colorpicker.save": "บันทึก",
|
||||||
|
"pad.colorpicker.cancel": "ยกเลิก",
|
||||||
|
"pad.loading": "กำลังโหลด...",
|
||||||
|
"pad.noCookie": "ไม่พบคุกกี้ โปรดเปิดใช้งานคุกกี้ในเบราว์เซอร์ของคุณ!",
|
||||||
|
"pad.passwordRequired": "คุณต้องใช้รหัสผ่านเพื่อเข้าถึงแผ่นจดบันทึกนี้",
|
||||||
|
"pad.permissionDenied": "คุณไม่มีสิทธิ์เข้าถึงแผ่นจดบันทึกนี้",
|
||||||
|
"pad.wrongPassword": "รหัสผ่านของคุณผิด",
|
||||||
|
"pad.settings.padSettings": "การตั้งค่าแผ่นจดบันทึก",
|
||||||
|
"pad.settings.myView": "มุมมองของฉัน",
|
||||||
|
"pad.settings.stickychat": "แสดงการแชทบนหน้าจอเสมอ",
|
||||||
|
"pad.settings.chatandusers": "แสดงการแชทและผู้ใช้",
|
||||||
|
"pad.settings.colorcheck": "สีผู้เขียน",
|
||||||
|
"pad.settings.linenocheck": "เลขบรรทัด",
|
||||||
|
"pad.settings.rtlcheck": "อ่านเนื้อหาจากขวาไปซ้ายหรือไม่?",
|
||||||
|
"pad.settings.fontType": "ชนิดแบบอักษร:",
|
||||||
|
"pad.settings.globalView": "มุมมองสากล",
|
||||||
|
"pad.settings.language": "ภาษา:",
|
||||||
|
"pad.importExport.import_export": "นำเข้า/ส่งออก",
|
||||||
|
"pad.importExport.import": "อัปโหลดไฟล์ข้อความหรือเอกสารใดๆ",
|
||||||
|
"pad.importExport.importSuccessful": "สำเร็จ!",
|
||||||
|
"pad.importExport.export": "ส่งออกแผ่นจดบันทึกปัจจุบันเป็น:",
|
||||||
|
"pad.importExport.exportetherpad": "Etherpad",
|
||||||
|
"pad.importExport.exporthtml": "HTML",
|
||||||
|
"pad.importExport.exportplain": "ข้อความธรรมดา",
|
||||||
|
"pad.importExport.exportword": "Microsoft Word",
|
||||||
|
"pad.importExport.exportpdf": "PDF",
|
||||||
|
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||||
|
"pad.importExport.abiword.innerHTML": "คุณสามารถนำเข้าได้จากรูปแบบ HTML หรือข้อความธรรมดาเท่านั้น สำหรับคุณสมบัติการนำเข้าขั้นสูงเพิ่มเติม โปรด<a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">ติดตั้ง AbiWord</a>",
|
||||||
|
"pad.modals.connected": "เชื่อมต่อแล้ว",
|
||||||
|
"pad.modals.reconnecting": "กำลังเชื่อมต่อกับแผ่นจดบันทึกของคุณใหม่..",
|
||||||
|
"pad.modals.forcereconnect": "บังคับเชื่อมต่อใหม่",
|
||||||
|
"pad.modals.reconnecttimer": "กำลังพยายามเชื่อมต่อใหม่ใน",
|
||||||
|
"pad.modals.cancel": "ยกเลิก",
|
||||||
|
"pad.modals.userdup": "เปิดในหน้าต่างอื่นแล้ว",
|
||||||
|
"pad.modals.userdup.explanation": "แผ่นจดบันทึกนี้ดูเหมือนว่าจะถูกเปิดในหน้าต่างเบราว์เซอร์มากกว่าหนึ่งหน้าต่างบนคอมพิวเตอร์นี้",
|
||||||
|
"pad.modals.userdup.advice": "เชื่อมต่อใหม่เพื่อใช้หน้าต่างนี้แทน",
|
||||||
|
"pad.modals.unauth": "ไม่ได้รับอนุญาต",
|
||||||
|
"pad.modals.unauth.explanation": "สิทธิของคุณถูกเปลี่ยนขณะที่คุณดูหน้านี้อยู่ พยายามเชื่อมต่อใหม่",
|
||||||
|
"pad.modals.looping.explanation": "มีปัญหาการสื่อสารกับเซิร์ฟเวอร์การซิงค์ข้อมูล",
|
||||||
|
"pad.modals.looping.cause": "บางทีอาจเป็นเพราะคุณเชื่อมต่อกับไฟร์วอลล์หรือพร็อกซีที่เข้ากันไม่ได้",
|
||||||
|
"pad.modals.initsocketfail": "เซิร์ฟเวอร์ไม่สามารถเข้าถึงได้",
|
||||||
|
"pad.modals.initsocketfail.explanation": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์การซิงค์ข้อมูล",
|
||||||
|
"pad.modals.initsocketfail.cause": "อาจเป็นเนื่องจากเบราว์เซอร์ของคุณหรือการเชื่อมต่ออินเทอร์เน็ตของคุณมีปัญหา",
|
||||||
|
"pad.modals.slowcommit.explanation": "เซิร์ฟเวอร์ไม่ตอบสนอง",
|
||||||
|
"pad.modals.slowcommit.cause": "อาจเป็นเนื่องจากปัญหาเกี่ยวกับการเชื่อมต่อเครือข่าย",
|
||||||
|
"pad.modals.badChangeset.explanation": "การแก้ไขที่คุณกระทำถูกจัดว่าไม่เหมาะสมโดยเซิร์ฟเวอร์การซิงค์ข้อมูล",
|
||||||
|
"pad.modals.badChangeset.cause": "อาจเป็นเนื่องจากการกำหนดค่าเซิร์ฟเวอร์ไม่ถูกต้องหรือมีลักษณะการทำงานอื่นๆ บางอย่างที่ไม่คาดคิด โปรดติดต่อผู้ดูแลการให้บริการ ถ้าคุณรู้สึกว่านี่คือข้อผิดพลาด โปรดทำการเชื่อมต่อใหม่อีกครั้งเพื่อทำการแก้ไขต่อไป",
|
||||||
|
"pad.modals.corruptPad.explanation": "แผ่นจดบันทึกที่คุณกำลังพยายามเข้าถึงเสียหาย",
|
||||||
|
"pad.modals.corruptPad.cause": "อาจเป็นเนื่องจากการกำหนดค่าเซิร์ฟเวอร์ไม่ถูกต้องหรือมีลักษณะการทำงานอื่นๆ บางอย่างที่ไม่คาดคิด โปรดติดต่อผู้ดูแลการให้บริการ",
|
||||||
|
"pad.modals.deleted": "ลบแล้ว",
|
||||||
|
"pad.modals.deleted.explanation": "แผ่นจดบันทึกนี้ได้ถูกลบออกแล้ว",
|
||||||
|
"pad.modals.disconnected": "คุณได้ตัดการเชื่อมต่อแล้ว",
|
||||||
|
"pad.modals.disconnected.explanation": "การเชื่อมต่อกับเซิร์ฟเวอร์ถูกตัด",
|
||||||
|
"pad.modals.disconnected.cause": "เซิร์ฟเวอร์อาจใช้ไม่ได้ชั่วคราว โปรดแจ้งให้ผู้ดูแลการให้บริการทราบถ้าปัญหานี้ยังคงเกิดขึ้น",
|
||||||
|
"pad.share": "แชร์แผ่นจดบันทึกนี้",
|
||||||
|
"pad.share.readonly": "อ่านเท่านั้น",
|
||||||
|
"pad.share.link": "ลิงก์",
|
||||||
|
"pad.share.emebdcode": "URL แบบฝังตัว",
|
||||||
|
"pad.chat": "แชท",
|
||||||
|
"pad.chat.title": "เปิดการแชทสำหรับแผ่นจดบันทึกนี้",
|
||||||
|
"pad.chat.loadmessages": "โหลดข้อความเพิ่มเติม",
|
||||||
|
"timeslider.pageTitle": "ตัวเลื่อนเวลา {{appTitle}}",
|
||||||
|
"timeslider.toolbar.returnbutton": "กลับไปแผ่นจดบันทึก",
|
||||||
|
"timeslider.toolbar.authors": "ผู้เขียน:",
|
||||||
|
"timeslider.toolbar.authorsList": "ไม่มีผู้เขียน",
|
||||||
|
"timeslider.toolbar.exportlink.title": "ส่งออก",
|
||||||
|
"timeslider.exportCurrent": "ส่งออกรุ่นปัจจุบันเป็น:",
|
||||||
|
"timeslider.version": "รุ่น {{version}}",
|
||||||
|
"timeslider.saved": "บันทึกแล้วเมื่อ {{day}} {{month}} {{year}}",
|
||||||
|
"timeslider.playPause": "เล่น / พักเนื้อหาแผ่นจดบันทึก",
|
||||||
|
"timeslider.backRevision": "กลับไปรุ่นแก้ไขเก่าของแผ่นจดบันทึกนี้",
|
||||||
|
"timeslider.forwardRevision": "ไปยังรุ่นแก้ไขใหม่ของแผ่นจดบันทึกนี้",
|
||||||
|
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
||||||
|
"timeslider.month.january": "มกราคม",
|
||||||
|
"timeslider.month.february": "กุมภาพันธ์",
|
||||||
|
"timeslider.month.march": "มีนาคม",
|
||||||
|
"timeslider.month.april": "เมษายน",
|
||||||
|
"timeslider.month.may": "พฤษภาคม",
|
||||||
|
"timeslider.month.june": "มิถุนายน",
|
||||||
|
"timeslider.month.july": "กรกฎาคม",
|
||||||
|
"timeslider.month.august": "สิงหาคม",
|
||||||
|
"timeslider.month.september": "กันยายน",
|
||||||
|
"timeslider.month.october": "ตุลาคม",
|
||||||
|
"timeslider.month.november": "พฤศจิกายน",
|
||||||
|
"timeslider.month.december": "ธันวาคม",
|
||||||
|
"timeslider.unnamedauthors": "{{num}} ผู้เขียนที่ไม่มีชื่อ",
|
||||||
|
"pad.savedrevs.marked": "รุ่นแก้ไขนี้ถูกทำเครื่องหมายเป็นรุ่นแก้ไขที่บันทึกแล้ว",
|
||||||
|
"pad.savedrevs.timeslider": "คุณสามารถดูรุ่นแก้ไขที่บันทึกแล้วโดยเยี่ยมชมตัวเลื่อนเวลา",
|
||||||
|
"pad.userlist.entername": "กรอกชื่อของคุณ",
|
||||||
|
"pad.userlist.unnamed": "ไม่มีชื่อ",
|
||||||
|
"pad.userlist.guest": "ผู้เยี่ยมชม",
|
||||||
|
"pad.userlist.deny": "ปฏิเสธ",
|
||||||
|
"pad.userlist.approve": "อนุมัติ",
|
||||||
|
"pad.editbar.clearcolors": "ล้างสีผู้เขียนบนทั้งเอกสารหรือไม่?",
|
||||||
|
"pad.impexp.importbutton": "นำเข้าเดี๋ยวนี้",
|
||||||
|
"pad.impexp.importing": "กำลังนำเข้า...",
|
||||||
|
"pad.impexp.confirmimport": "การนำเข้าไฟล์จะเป็นการเขียนทับข้อความปัจจุบันบนแผ่นจดบันทึก คุณแน่ใจหรือว่าคุณต้องการดำเนินการต่อ?",
|
||||||
|
"pad.impexp.convertFailed": "เราไม่สามารถนำเข้าไฟล์นี้ได้ โปรดใช้รูปแบบเอกสารอื่นหรือคัดลอกแล้ววางด้วยตนเอง",
|
||||||
|
"pad.impexp.padHasData": "เราไม่สามารถนำเข้าไฟล์นี้ได้เนื่องจากแผ่นจดบันทึกนี้มีการเปลี่ยนแปลงอยู่แล้ว โปรดนำเข้าไปแผ่นจดบันทึกใหม่แทน",
|
||||||
|
"pad.impexp.uploadFailed": "การอัปโหลดล้มเหลว โปรดลองอีกครั้ง",
|
||||||
|
"pad.impexp.importfailed": "การนำเข้าล้มเหลว",
|
||||||
|
"pad.impexp.copypaste": "โปรดคัดลอกแล้ววาง",
|
||||||
|
"pad.impexp.exportdisabled": "การส่งออกเป็นรูปแบบ {{type}} ถูกปิดใช้งาน โปรดติดต่อผู้ดูแลระบบของคุณสำหรับรายละเอียดเพิ่มเติม"
|
||||||
|
}
|
|
@ -59,7 +59,7 @@
|
||||||
"pad.importExport.exportword": "Microsoft Word",
|
"pad.importExport.exportword": "Microsoft Word",
|
||||||
"pad.importExport.exportpdf": "PDF",
|
"pad.importExport.exportpdf": "PDF",
|
||||||
"pad.importExport.exportopen": "ODF(開放文件格式)",
|
"pad.importExport.exportopen": "ODF(開放文件格式)",
|
||||||
"pad.importExport.abiword.innerHTML": "您只可以純文字或html格式檔匯入。<a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">安裝abiword</a>取得更多進階的匯入功能。",
|
"pad.importExport.abiword.innerHTML": "您只可以純文字或 HTML 格式檔匯入。<a href=\"ttps://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">安裝\n AbiWord </a>取得更多進階的匯入功能。",
|
||||||
"pad.modals.connected": "已連線。",
|
"pad.modals.connected": "已連線。",
|
||||||
"pad.modals.reconnecting": "重新連接到您的記事本...",
|
"pad.modals.reconnecting": "重新連接到您的記事本...",
|
||||||
"pad.modals.forcereconnect": "強制重新連線",
|
"pad.modals.forcereconnect": "強制重新連線",
|
||||||
|
|
|
@ -1216,6 +1216,15 @@ function handleClientReady(client, message)
|
||||||
"parts": plugins.parts,
|
"parts": plugins.parts,
|
||||||
},
|
},
|
||||||
"indentationOnNewLine": settings.indentationOnNewLine,
|
"indentationOnNewLine": settings.indentationOnNewLine,
|
||||||
|
"scrollWhenFocusLineIsOutOfViewport": {
|
||||||
|
"percentage" : {
|
||||||
|
"editionAboveViewport": settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport,
|
||||||
|
"editionBelowViewport": settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport,
|
||||||
|
},
|
||||||
|
"duration": settings.scrollWhenFocusLineIsOutOfViewport.duration,
|
||||||
|
"scrollWhenCaretIsInTheLastLineOfViewport": settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport,
|
||||||
|
"percentageToScrollWhenUserPressesArrowUp": settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp,
|
||||||
|
},
|
||||||
"initialChangesets": [] // FIXME: REMOVE THIS SHIT
|
"initialChangesets": [] // FIXME: REMOVE THIS SHIT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ var apiLogger = log4js.getLogger("API");
|
||||||
var clientLogger = log4js.getLogger("client");
|
var clientLogger = log4js.getLogger("client");
|
||||||
var formidable = require('formidable');
|
var formidable = require('formidable');
|
||||||
var apiHandler = require('../../handler/APIHandler');
|
var apiHandler = require('../../handler/APIHandler');
|
||||||
|
var isVarName = require('is-var-name');
|
||||||
|
|
||||||
//This is for making an api call, collecting all post information and passing it to the apiHandler
|
//This is for making an api call, collecting all post information and passing it to the apiHandler
|
||||||
var apiCaller = function(req, res, fields) {
|
var apiCaller = function(req, res, fields) {
|
||||||
|
@ -18,7 +19,7 @@ var apiCaller = function(req, res, fields) {
|
||||||
apiLogger.info("RESPONSE, " + req.params.func + ", " + response);
|
apiLogger.info("RESPONSE, " + req.params.func + ", " + response);
|
||||||
|
|
||||||
//is this a jsonp call, if yes, add the function call
|
//is this a jsonp call, if yes, add the function call
|
||||||
if(req.query.jsonp)
|
if(req.query.jsonp && isVarName(response))
|
||||||
response = req.query.jsonp + "(" + response + ")";
|
response = req.query.jsonp + "(" + response + ")";
|
||||||
|
|
||||||
res._____send(response);
|
res._____send(response);
|
||||||
|
|
|
@ -49,5 +49,8 @@ exports.expressCreateServer = function (hook_name, args, cb) {
|
||||||
//sigint is so far not working on windows
|
//sigint is so far not working on windows
|
||||||
//https://github.com/joyent/node/issues/1553
|
//https://github.com/joyent/node/issues/1553
|
||||||
process.on('SIGINT', exports.gracefulShutdown);
|
process.on('SIGINT', exports.gracefulShutdown);
|
||||||
|
// when running as PID1 (e.g. in docker container)
|
||||||
|
// allow graceful shutdown on SIGTERM c.f. #3265
|
||||||
|
process.on('SIGTERM', exports.gracefulShutdown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -247,6 +247,33 @@ exports.users = {};
|
||||||
*/
|
*/
|
||||||
exports.showSettingsInAdminPage = true;
|
exports.showSettingsInAdminPage = true;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* By default, when caret is moved out of viewport, it scrolls the minimum height needed to make this
|
||||||
|
* line visible.
|
||||||
|
*/
|
||||||
|
exports.scrollWhenFocusLineIsOutOfViewport = {
|
||||||
|
/*
|
||||||
|
* Percentage of viewport height to be additionally scrolled.
|
||||||
|
*/
|
||||||
|
"percentage": {
|
||||||
|
"editionAboveViewport": 0,
|
||||||
|
"editionBelowViewport": 0
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
* Time (in milliseconds) used to animate the scroll transition. Set to 0 to disable animation
|
||||||
|
*/
|
||||||
|
"duration": 0,
|
||||||
|
/*
|
||||||
|
* Flag to control if it should scroll when user places the caret in the last line of the viewport
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
* Percentage of viewport height to be additionally scrolled when user presses arrow up
|
||||||
|
* in the line of the top of the viewport.
|
||||||
|
*/
|
||||||
|
"percentageToScrollWhenUserPressesArrowUp": 0,
|
||||||
|
"scrollWhenCaretIsInTheLastLineOfViewport": false
|
||||||
|
};
|
||||||
|
|
||||||
//checks if abiword is avaiable
|
//checks if abiword is avaiable
|
||||||
exports.abiwordAvailable = function()
|
exports.abiwordAvailable = function()
|
||||||
{
|
{
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
"cheerio" : "0.20.0",
|
"cheerio" : "0.20.0",
|
||||||
"async-stacktrace" : "0.0.2",
|
"async-stacktrace" : "0.0.2",
|
||||||
"npm" : "4.0.2",
|
"npm" : "4.0.2",
|
||||||
"ejs" : "2.4.1",
|
"ejs" : "2.5.7",
|
||||||
"graceful-fs" : "4.1.3",
|
"graceful-fs" : "4.1.3",
|
||||||
"slide" : "1.1.6",
|
"slide" : "1.1.6",
|
||||||
"semver" : "5.1.0",
|
"semver" : "5.1.0",
|
||||||
|
@ -43,7 +43,8 @@
|
||||||
"jsonminify" : "0.4.1",
|
"jsonminify" : "0.4.1",
|
||||||
"measured" : "1.1.0",
|
"measured" : "1.1.0",
|
||||||
"mocha" : "2.4.5",
|
"mocha" : "2.4.5",
|
||||||
"supertest" : "1.2.0"
|
"supertest" : "1.2.0",
|
||||||
|
"is-var-name" : "1.0.0"
|
||||||
},
|
},
|
||||||
"bin": { "etherpad-lite": "./node/server.js" },
|
"bin": { "etherpad-lite": "./node/server.js" },
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -55,6 +56,6 @@
|
||||||
"repository" : { "type" : "git",
|
"repository" : { "type" : "git",
|
||||||
"url" : "http://github.com/ether/etherpad-lite.git"
|
"url" : "http://github.com/ether/etherpad-lite.git"
|
||||||
},
|
},
|
||||||
"version" : "1.6.2",
|
"version" : "1.6.3",
|
||||||
"license" : "Apache-2.0"
|
"license" : "Apache-2.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -400,7 +400,19 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
||||||
this.removeAttributeOnLine(lineNum, attributeName) :
|
this.removeAttributeOnLine(lineNum, attributeName) :
|
||||||
this.setAttributeOnLine(lineNum, attributeName, attributeValue);
|
this.setAttributeOnLine(lineNum, attributeName, attributeValue);
|
||||||
|
|
||||||
}
|
},
|
||||||
|
|
||||||
|
hasAttributeOnSelectionOrCaretPosition: function(attributeName) {
|
||||||
|
var hasSelection = ((this.rep.selStart[0] !== this.rep.selEnd[0]) || (this.rep.selEnd[1] !== this.rep.selStart[1]));
|
||||||
|
var hasAttrib;
|
||||||
|
if (hasSelection) {
|
||||||
|
hasAttrib = this.getAttributeOnSelection(attributeName);
|
||||||
|
}else {
|
||||||
|
var attributesOnCaretPosition = this.getAttributesOnCaret();
|
||||||
|
hasAttrib = _.contains(_.flatten(attributesOnCaretPosition), attributeName);
|
||||||
|
}
|
||||||
|
return hasAttrib;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = AttributeManager;
|
module.exports = AttributeManager;
|
||||||
|
|
|
@ -20,7 +20,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
var _, $, jQuery, plugins, Ace2Common;
|
var _, $, jQuery, plugins, Ace2Common;
|
||||||
|
|
||||||
var browser = require('./browser');
|
var browser = require('./browser');
|
||||||
if(browser.msie){
|
if(browser.msie){
|
||||||
// Honestly fuck IE royally.
|
// Honestly fuck IE royally.
|
||||||
|
@ -61,6 +60,7 @@ function Ace2Inner(){
|
||||||
var SkipList = require('./skiplist');
|
var SkipList = require('./skiplist');
|
||||||
var undoModule = require('./undomodule').undoModule;
|
var undoModule = require('./undomodule').undoModule;
|
||||||
var AttributeManager = require('./AttributeManager');
|
var AttributeManager = require('./AttributeManager');
|
||||||
|
var Scroll = require('./scroll');
|
||||||
|
|
||||||
var DEBUG = false; //$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;"
|
var DEBUG = false; //$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;"
|
||||||
// changed to false
|
// changed to false
|
||||||
|
@ -75,6 +75,9 @@ function Ace2Inner(){
|
||||||
var EDIT_BODY_PADDING_TOP = 8;
|
var EDIT_BODY_PADDING_TOP = 8;
|
||||||
var EDIT_BODY_PADDING_LEFT = 8;
|
var EDIT_BODY_PADDING_LEFT = 8;
|
||||||
|
|
||||||
|
var FORMATTING_STYLES = ['bold', 'italic', 'underline', 'strikethrough'];
|
||||||
|
var SELECT_BUTTON_CLASS = 'selected';
|
||||||
|
|
||||||
var caughtErrors = [];
|
var caughtErrors = [];
|
||||||
|
|
||||||
var thisAuthor = '';
|
var thisAuthor = '';
|
||||||
|
@ -82,6 +85,7 @@ function Ace2Inner(){
|
||||||
var disposed = false;
|
var disposed = false;
|
||||||
var editorInfo = parent.editorInfo;
|
var editorInfo = parent.editorInfo;
|
||||||
|
|
||||||
|
|
||||||
var iframe = window.frameElement;
|
var iframe = window.frameElement;
|
||||||
var outerWin = iframe.ace_outerWin;
|
var outerWin = iframe.ace_outerWin;
|
||||||
iframe.ace_outerWin = null; // prevent IE 6 memory leak
|
iframe.ace_outerWin = null; // prevent IE 6 memory leak
|
||||||
|
@ -89,6 +93,8 @@ function Ace2Inner(){
|
||||||
var lineMetricsDiv = sideDiv.nextSibling;
|
var lineMetricsDiv = sideDiv.nextSibling;
|
||||||
initLineNumbers();
|
initLineNumbers();
|
||||||
|
|
||||||
|
var scroll = Scroll.init(outerWin);
|
||||||
|
|
||||||
var outsideKeyDown = noop;
|
var outsideKeyDown = noop;
|
||||||
|
|
||||||
var outsideKeyPress = function(){return true;};
|
var outsideKeyPress = function(){return true;};
|
||||||
|
@ -424,7 +430,7 @@ function Ace2Inner(){
|
||||||
var undoWorked = false;
|
var undoWorked = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (evt.eventType == "setup" || evt.eventType == "importText" || evt.eventType == "setBaseText")
|
if (isPadLoading(evt.eventType))
|
||||||
{
|
{
|
||||||
undoModule.clearHistory();
|
undoModule.clearHistory();
|
||||||
}
|
}
|
||||||
|
@ -1208,7 +1214,7 @@ function Ace2Inner(){
|
||||||
updateLineNumbers(); // update line numbers if any time left
|
updateLineNumbers(); // update line numbers if any time left
|
||||||
if (isTimeUp()) return;
|
if (isTimeUp()) return;
|
||||||
|
|
||||||
var visibleRange = getVisibleCharRange();
|
var visibleRange = scroll.getVisibleCharRange(rep);
|
||||||
var docRange = [0, rep.lines.totalWidth()];
|
var docRange = [0, rep.lines.totalWidth()];
|
||||||
//console.log("%o %o", docRange, visibleRange);
|
//console.log("%o %o", docRange, visibleRange);
|
||||||
finishedImportantWork = true;
|
finishedImportantWork = true;
|
||||||
|
@ -1670,7 +1676,7 @@ function Ace2Inner(){
|
||||||
});
|
});
|
||||||
|
|
||||||
//p.mark("relex");
|
//p.mark("relex");
|
||||||
//rep.lexer.lexCharRange(getVisibleCharRange(), function() { return false; });
|
//rep.lexer.lexCharRange(scroll.getVisibleCharRange(rep), function() { return false; });
|
||||||
//var isTimeUp = newTimeLimit(100);
|
//var isTimeUp = newTimeLimit(100);
|
||||||
// do DOM inserts
|
// do DOM inserts
|
||||||
p.mark("insert");
|
p.mark("insert");
|
||||||
|
@ -2469,17 +2475,11 @@ function Ace2Inner(){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectionAllHasIt)
|
|
||||||
{
|
var attributeValue = selectionAllHasIt ? '' : 'true';
|
||||||
documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [
|
documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [[attributeName, attributeValue]]);
|
||||||
[attributeName, '']
|
if (attribIsFormattingStyle(attributeName)) {
|
||||||
]);
|
updateStyleButtonState(attributeName, !selectionAllHasIt); // italic, bold, ...
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [
|
|
||||||
[attributeName, 'true']
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
editorInfo.ace_toggleAttributeOnSelection = toggleAttributeOnSelection;
|
editorInfo.ace_toggleAttributeOnSelection = toggleAttributeOnSelection;
|
||||||
|
@ -2908,12 +2908,24 @@ function Ace2Inner(){
|
||||||
rep.selFocusAtStart = newSelFocusAtStart;
|
rep.selFocusAtStart = newSelFocusAtStart;
|
||||||
currentCallStack.repChanged = true;
|
currentCallStack.repChanged = true;
|
||||||
|
|
||||||
|
// select the formatting buttons when there is the style applied on selection
|
||||||
|
selectFormattingButtonIfLineHasStyleApplied(rep);
|
||||||
|
|
||||||
hooks.callAll('aceSelectionChanged', {
|
hooks.callAll('aceSelectionChanged', {
|
||||||
rep: rep,
|
rep: rep,
|
||||||
callstack: currentCallStack,
|
callstack: currentCallStack,
|
||||||
documentAttributeManager: documentAttributeManager,
|
documentAttributeManager: documentAttributeManager,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// we scroll when user places the caret at the last line of the pad
|
||||||
|
// when this settings is enabled
|
||||||
|
var docTextChanged = currentCallStack.docTextChanged;
|
||||||
|
if(!docTextChanged){
|
||||||
|
var isScrollableEvent = !isPadLoading(currentCallStack.type) && isScrollableEditEvent(currentCallStack.type);
|
||||||
|
var innerHeight = getInnerHeight();
|
||||||
|
scroll.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep, isScrollableEvent, innerHeight);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
//console.log("selStart: %o, selEnd: %o, focusAtStart: %s", rep.selStart, rep.selEnd,
|
//console.log("selStart: %o, selEnd: %o, focusAtStart: %s", rep.selStart, rep.selEnd,
|
||||||
//String(!!rep.selFocusAtStart));
|
//String(!!rep.selFocusAtStart));
|
||||||
|
@ -2922,6 +2934,27 @@ function Ace2Inner(){
|
||||||
//console.log("%o %o %s", rep.selStart, rep.selEnd, rep.selFocusAtStart);
|
//console.log("%o %o %s", rep.selStart, rep.selEnd, rep.selFocusAtStart);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPadLoading(eventType)
|
||||||
|
{
|
||||||
|
return (eventType === 'setup') || (eventType === 'setBaseText') || (eventType === 'importText');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStyleButtonState(attribName, hasStyleOnRepSelection) {
|
||||||
|
var $formattingButton = parent.parent.$('[data-key="' + attribName + '"]').find('a');
|
||||||
|
$formattingButton.toggleClass(SELECT_BUTTON_CLASS, hasStyleOnRepSelection);
|
||||||
|
}
|
||||||
|
|
||||||
|
function attribIsFormattingStyle(attributeName) {
|
||||||
|
return _.contains(FORMATTING_STYLES, attributeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectFormattingButtonIfLineHasStyleApplied (rep) {
|
||||||
|
_.each(FORMATTING_STYLES, function (style) {
|
||||||
|
var hasStyleOnRepSelection = documentAttributeManager.hasAttributeOnSelectionOrCaretPosition(style);
|
||||||
|
updateStyleButtonState(style, hasStyleOnRepSelection);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function doCreateDomLine(nonEmpty)
|
function doCreateDomLine(nonEmpty)
|
||||||
{
|
{
|
||||||
if (browser.msie && (!nonEmpty))
|
if (browser.msie && (!nonEmpty))
|
||||||
|
@ -3277,50 +3310,36 @@ function Ace2Inner(){
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLineEntryTopBottom(entry, destObj)
|
|
||||||
{
|
|
||||||
var dom = entry.lineNode;
|
|
||||||
var top = dom.offsetTop;
|
|
||||||
var height = dom.offsetHeight;
|
|
||||||
var obj = (destObj || {});
|
|
||||||
obj.top = top;
|
|
||||||
obj.bottom = (top + height);
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getViewPortTopBottom()
|
function getViewPortTopBottom()
|
||||||
{
|
{
|
||||||
var theTop = getScrollY();
|
var theTop = scroll.getScrollY();
|
||||||
var doc = outerWin.document;
|
var doc = outerWin.document;
|
||||||
var height = doc.documentElement.clientHeight;
|
var height = doc.documentElement.clientHeight; // includes padding
|
||||||
|
|
||||||
|
// we have to get the exactly height of the viewport. So it has to subtract all the values which changes
|
||||||
|
// the viewport height (E.g. padding, position top)
|
||||||
|
var viewportExtraSpacesAndPosition = getEditorPositionTop() + getPaddingTopAddedWhenPageViewIsEnable();
|
||||||
return {
|
return {
|
||||||
top: theTop,
|
top: theTop,
|
||||||
bottom: (theTop + height)
|
bottom: (theTop + height - viewportExtraSpacesAndPosition)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVisibleLineRange()
|
|
||||||
|
function getEditorPositionTop()
|
||||||
{
|
{
|
||||||
var viewport = getViewPortTopBottom();
|
var editor = parent.document.getElementsByTagName('iframe');
|
||||||
//console.log("viewport top/bottom: %o", viewport);
|
var editorPositionTop = editor[0].offsetTop;
|
||||||
var obj = {};
|
return editorPositionTop;
|
||||||
var start = rep.lines.search(function(e)
|
|
||||||
{
|
|
||||||
return getLineEntryTopBottom(e, obj).bottom > viewport.top;
|
|
||||||
});
|
|
||||||
var end = rep.lines.search(function(e)
|
|
||||||
{
|
|
||||||
return getLineEntryTopBottom(e, obj).top >= viewport.bottom;
|
|
||||||
});
|
|
||||||
if (end < start) end = start; // unlikely
|
|
||||||
//console.log(start+","+end);
|
|
||||||
return [start, end];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVisibleCharRange()
|
// ep_page_view adds padding-top, which makes the viewport smaller
|
||||||
|
function getPaddingTopAddedWhenPageViewIsEnable()
|
||||||
{
|
{
|
||||||
var lineRange = getVisibleLineRange();
|
var rootDocument = parent.parent.document;
|
||||||
return [rep.lines.offsetOfIndex(lineRange[0]), rep.lines.offsetOfIndex(lineRange[1])];
|
var aceOuter = rootDocument.getElementsByName("ace_outer");
|
||||||
|
var aceOuterPaddingTop = parseInt($(aceOuter).css("padding-top"));
|
||||||
|
return aceOuterPaddingTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCut(evt)
|
function handleCut(evt)
|
||||||
|
@ -3966,12 +3985,12 @@ function Ace2Inner(){
|
||||||
doDeleteKey();
|
doDeleteKey();
|
||||||
specialHandled = true;
|
specialHandled = true;
|
||||||
}
|
}
|
||||||
if((evt.which == 36 && evt.ctrlKey == true) && padShortcutEnabled.ctrlHome){ setScrollY(0); } // Control Home send to Y = 0
|
if((evt.which == 36 && evt.ctrlKey == true) && padShortcutEnabled.ctrlHome){ scroll.setScrollY(0); } // Control Home send to Y = 0
|
||||||
if((evt.which == 33 || evt.which == 34) && type == 'keydown' && !evt.ctrlKey){
|
if((evt.which == 33 || evt.which == 34) && type == 'keydown' && !evt.ctrlKey){
|
||||||
|
|
||||||
evt.preventDefault(); // This is required, browsers will try to do normal default behavior on page up / down and the default behavior SUCKS
|
evt.preventDefault(); // This is required, browsers will try to do normal default behavior on page up / down and the default behavior SUCKS
|
||||||
|
|
||||||
var oldVisibleLineRange = getVisibleLineRange();
|
var oldVisibleLineRange = scroll.getVisibleLineRange(rep);
|
||||||
var topOffset = rep.selStart[0] - oldVisibleLineRange[0];
|
var topOffset = rep.selStart[0] - oldVisibleLineRange[0];
|
||||||
if(topOffset < 0 ){
|
if(topOffset < 0 ){
|
||||||
topOffset = 0;
|
topOffset = 0;
|
||||||
|
@ -3981,7 +4000,7 @@ function Ace2Inner(){
|
||||||
var isPageUp = evt.which === 33;
|
var isPageUp = evt.which === 33;
|
||||||
|
|
||||||
scheduler.setTimeout(function(){
|
scheduler.setTimeout(function(){
|
||||||
var newVisibleLineRange = getVisibleLineRange(); // the visible lines IE 1,10
|
var newVisibleLineRange = scroll.getVisibleLineRange(rep); // the visible lines IE 1,10
|
||||||
var linesCount = rep.lines.length(); // total count of lines in pad IE 10
|
var linesCount = rep.lines.length(); // total count of lines in pad IE 10
|
||||||
var numberOfLinesInViewport = newVisibleLineRange[1] - newVisibleLineRange[0]; // How many lines are in the viewport right now?
|
var numberOfLinesInViewport = newVisibleLineRange[1] - newVisibleLineRange[0]; // How many lines are in the viewport right now?
|
||||||
|
|
||||||
|
@ -4014,56 +4033,26 @@ function Ace2Inner(){
|
||||||
// sometimes the first selection is -1 which causes problems (Especially with ep_page_view)
|
// sometimes the first selection is -1 which causes problems (Especially with ep_page_view)
|
||||||
// so use focusNode.offsetTop value.
|
// so use focusNode.offsetTop value.
|
||||||
if(caretOffsetTop === -1) caretOffsetTop = myselection.focusNode.offsetTop;
|
if(caretOffsetTop === -1) caretOffsetTop = myselection.focusNode.offsetTop;
|
||||||
setScrollY(caretOffsetTop); // set the scrollY offset of the viewport on the document
|
scroll.setScrollY(caretOffsetTop); // set the scrollY offset of the viewport on the document
|
||||||
|
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
/* Attempt to apply some sanity to cursor handling in Chrome after a copy / paste event
|
|
||||||
We have to do this the way we do because rep. doesn't hold the value for keyheld events IE if the user
|
|
||||||
presses and holds the arrow key .. Sorry if this is ugly, blame Chrome's weird handling of viewports after new content is added*/
|
|
||||||
if((evt.which == 37 || evt.which == 38 || evt.which == 39 || evt.which == 40) && browser.chrome){
|
|
||||||
var viewport = getViewPortTopBottom();
|
|
||||||
var myselection = document.getSelection(); // get the current caret selection, can't use rep. here because that only gives us the start position not the current
|
|
||||||
var caretOffsetTop = myselection.focusNode.parentNode.offsetTop || myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214
|
|
||||||
var lineHeight = $(myselection.focusNode.parentNode).parent("div").height(); // get the line height of the caret line
|
|
||||||
// top.console.log("offsetTop", myselection.focusNode.parentNode.parentNode.offsetTop);
|
|
||||||
try {
|
|
||||||
lineHeight = $(myselection.focusNode).height() // needed for how chrome handles line heights of null objects
|
|
||||||
// console.log("lineHeight now", lineHeight);
|
|
||||||
}catch(e){}
|
|
||||||
var caretOffsetTopBottom = caretOffsetTop + lineHeight;
|
|
||||||
var visibleLineRange = getVisibleLineRange(); // the visible lines IE 1,10
|
|
||||||
|
|
||||||
if(caretOffsetTop){ // sometimes caretOffsetTop bugs out and returns 0, not sure why, possible Chrome bug? Either way if it does we don't wanna mess with it
|
// scroll to viewport when user presses arrow keys and caret is out of the viewport
|
||||||
// top.console.log(caretOffsetTop, viewport.top, caretOffsetTopBottom, viewport.bottom);
|
if((evt.which == 37 || evt.which == 38 || evt.which == 39 || evt.which == 40)){
|
||||||
var caretIsNotVisible = (caretOffsetTop < viewport.top || caretOffsetTopBottom >= viewport.bottom); // Is the Caret Visible to the user?
|
// we use arrowKeyWasReleased to avoid triggering the animation when a key is continuously pressed
|
||||||
// Expect some weird behavior caretOffsetTopBottom is greater than viewport.bottom on a keypress down
|
// this makes the scroll smooth
|
||||||
var offsetTopSamePlace = caretOffsetTop == viewport.top; // sometimes moving key left & up leaves the caret at the same point as the viewport.top, technically the caret is visible but it's not fully visible so we should move to it
|
if(!continuouslyPressingArrowKey(type)){
|
||||||
if(offsetTopSamePlace && (evt.which == 37 || evt.which == 38)){
|
// We use getSelection() instead of rep to get the caret position. This avoids errors like when
|
||||||
var newY = caretOffsetTop;
|
// the caret position is not synchronized with the rep. For example, when an user presses arrow
|
||||||
setScrollY(newY);
|
// down to scroll the pad without releasing the key. When the key is released the rep is not
|
||||||
}
|
// synchronized, so we don't get the right node where caret is.
|
||||||
|
var selection = getSelection();
|
||||||
|
|
||||||
if(caretIsNotVisible){ // is the cursor no longer visible to the user?
|
if(selection){
|
||||||
// top.console.log("Caret is NOT visible to the user");
|
var arrowUp = evt.which === 38;
|
||||||
// top.console.log(caretOffsetTop,viewport.top,caretOffsetTopBottom,viewport.bottom);
|
var innerHeight = getInnerHeight();
|
||||||
// Oh boy the caret is out of the visible area, I need to scroll the browser window to lineNum.
|
scroll.scrollWhenPressArrowKeys(arrowUp, rep, innerHeight);
|
||||||
if(evt.which == 37 || evt.which == 38){ // If left or up arrow
|
|
||||||
var newY = caretOffsetTop; // That was easy!
|
|
||||||
}
|
|
||||||
if(evt.which == 39 || evt.which == 40){ // if down or right arrow
|
|
||||||
// only move the viewport if we're at the bottom of the viewport, if we hit down any other time the viewport shouldn't change
|
|
||||||
// NOTE: This behavior only fires if Chrome decides to break the page layout after a paste, it's annoying but nothing I can do
|
|
||||||
var selection = getSelection();
|
|
||||||
// top.console.log("line #", rep.selStart[0]); // the line our caret is on
|
|
||||||
// top.console.log("firstvisible", visibleLineRange[0]); // the first visiblel ine
|
|
||||||
// top.console.log("lastVisible", visibleLineRange[1]); // the last visible line
|
|
||||||
// top.console.log(rep.selStart[0], visibleLineRange[1], rep.selStart[0], visibleLineRange[0]);
|
|
||||||
var newY = viewport.top + lineHeight;
|
|
||||||
}
|
|
||||||
if(newY){
|
|
||||||
setScrollY(newY); // set the scrollY offset of the viewport on the document
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4121,6 +4110,19 @@ function Ace2Inner(){
|
||||||
|
|
||||||
var thisKeyDoesntTriggerNormalize = false;
|
var thisKeyDoesntTriggerNormalize = false;
|
||||||
|
|
||||||
|
var arrowKeyWasReleased = true;
|
||||||
|
function continuouslyPressingArrowKey(type) {
|
||||||
|
var firstTimeKeyIsContinuouslyPressed = false;
|
||||||
|
|
||||||
|
if (type == 'keyup') arrowKeyWasReleased = true;
|
||||||
|
else if (type == 'keydown' && arrowKeyWasReleased) {
|
||||||
|
firstTimeKeyIsContinuouslyPressed = true;
|
||||||
|
arrowKeyWasReleased = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !firstTimeKeyIsContinuouslyPressed;
|
||||||
|
}
|
||||||
|
|
||||||
function doUndoRedo(which)
|
function doUndoRedo(which)
|
||||||
{
|
{
|
||||||
// precond: normalized DOM
|
// precond: normalized DOM
|
||||||
|
@ -4837,9 +4839,6 @@ function Ace2Inner(){
|
||||||
setIfNecessary(root.style, "height", "");
|
setIfNecessary(root.style, "height", "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if near edge, scroll to edge
|
|
||||||
var scrollX = getScrollX();
|
|
||||||
var scrollY = getScrollY();
|
|
||||||
var win = outerWin;
|
var win = outerWin;
|
||||||
var r = 20;
|
var r = 20;
|
||||||
|
|
||||||
|
@ -4848,52 +4847,6 @@ function Ace2Inner(){
|
||||||
$(sideDiv).addClass('sidedivdelayed');
|
$(sideDiv).addClass('sidedivdelayed');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getScrollXY()
|
|
||||||
{
|
|
||||||
var win = outerWin;
|
|
||||||
var odoc = outerWin.document;
|
|
||||||
if (typeof(win.pageYOffset) == "number")
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
x: win.pageXOffset,
|
|
||||||
y: win.pageYOffset
|
|
||||||
};
|
|
||||||
}
|
|
||||||
var docel = odoc.documentElement;
|
|
||||||
if (docel && typeof(docel.scrollTop) == "number")
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
x: docel.scrollLeft,
|
|
||||||
y: docel.scrollTop
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getScrollX()
|
|
||||||
{
|
|
||||||
return getScrollXY().x;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getScrollY()
|
|
||||||
{
|
|
||||||
return getScrollXY().y;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setScrollX(x)
|
|
||||||
{
|
|
||||||
outerWin.scrollTo(x, getScrollY());
|
|
||||||
}
|
|
||||||
|
|
||||||
function setScrollY(y)
|
|
||||||
{
|
|
||||||
outerWin.scrollTo(getScrollX(), y);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setScrollXY(x, y)
|
|
||||||
{
|
|
||||||
outerWin.scrollTo(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
var _teardownActions = [];
|
var _teardownActions = [];
|
||||||
|
|
||||||
function teardown()
|
function teardown()
|
||||||
|
@ -5214,26 +5167,6 @@ function Ace2Inner(){
|
||||||
return odoc.documentElement.clientWidth;
|
return odoc.documentElement.clientWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollNodeVerticallyIntoView(node)
|
|
||||||
{
|
|
||||||
// requires element (non-text) node;
|
|
||||||
// if node extends above top of viewport or below bottom of viewport (or top of scrollbar),
|
|
||||||
// scroll it the minimum distance needed to be completely in view.
|
|
||||||
var win = outerWin;
|
|
||||||
var odoc = outerWin.document;
|
|
||||||
var distBelowTop = node.offsetTop + iframePadTop - win.scrollY;
|
|
||||||
var distAboveBottom = win.scrollY + getInnerHeight() - (node.offsetTop + iframePadTop + node.offsetHeight);
|
|
||||||
|
|
||||||
if (distBelowTop < 0)
|
|
||||||
{
|
|
||||||
win.scrollBy(0, distBelowTop);
|
|
||||||
}
|
|
||||||
else if (distAboveBottom < 0)
|
|
||||||
{
|
|
||||||
win.scrollBy(0, -distAboveBottom);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollXHorizontallyIntoView(pixelX)
|
function scrollXHorizontallyIntoView(pixelX)
|
||||||
{
|
{
|
||||||
var win = outerWin;
|
var win = outerWin;
|
||||||
|
@ -5255,8 +5188,8 @@ function Ace2Inner(){
|
||||||
{
|
{
|
||||||
if (!rep.selStart) return;
|
if (!rep.selStart) return;
|
||||||
fixView();
|
fixView();
|
||||||
var focusLine = (rep.selFocusAtStart ? rep.selStart[0] : rep.selEnd[0]);
|
var innerHeight = getInnerHeight();
|
||||||
scrollNodeVerticallyIntoView(rep.lines.atIndex(focusLine).lineNode);
|
scroll.scrollNodeVerticallyIntoView(rep, innerHeight);
|
||||||
if (!doesWrap)
|
if (!doesWrap)
|
||||||
{
|
{
|
||||||
var browserSelection = getSelection();
|
var browserSelection = getSelection();
|
||||||
|
|
241
src/static/js/caretPosition.js
Normal file
241
src/static/js/caretPosition.js
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
// One rep.line(div) can be broken in more than one line in the browser.
|
||||||
|
// This function is useful to get the caret position of the line as
|
||||||
|
// is represented by the browser
|
||||||
|
exports.getPosition = function ()
|
||||||
|
{
|
||||||
|
var rect, line;
|
||||||
|
var editor = $('#innerdocbody')[0];
|
||||||
|
var range = getSelectionRange();
|
||||||
|
var isSelectionInsideTheEditor = range && $(range.endContainer).closest('body')[0].id === 'innerdocbody';
|
||||||
|
|
||||||
|
if(isSelectionInsideTheEditor){
|
||||||
|
// when we have the caret in an empty line, e.g. a line with only a <br>,
|
||||||
|
// getBoundingClientRect() returns all dimensions value as 0
|
||||||
|
var selectionIsInTheBeginningOfLine = range.endOffset > 0;
|
||||||
|
if (selectionIsInTheBeginningOfLine) {
|
||||||
|
var clonedRange = createSelectionRange(range);
|
||||||
|
line = getPositionOfElementOrSelection(clonedRange);
|
||||||
|
clonedRange.detach()
|
||||||
|
}
|
||||||
|
|
||||||
|
// when there's a <br> or any element that has no height, we can't get
|
||||||
|
// the dimension of the element where the caret is
|
||||||
|
if(!rect || rect.height === 0){
|
||||||
|
var clonedRange = createSelectionRange(range);
|
||||||
|
|
||||||
|
// as we can't get the element height, we create a text node to get the dimensions
|
||||||
|
// on the position
|
||||||
|
var shadowCaret = $(document.createTextNode("|"));
|
||||||
|
clonedRange.insertNode(shadowCaret[0]);
|
||||||
|
clonedRange.selectNode(shadowCaret[0]);
|
||||||
|
|
||||||
|
line = getPositionOfElementOrSelection(clonedRange);
|
||||||
|
clonedRange.detach()
|
||||||
|
shadowCaret.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
var createSelectionRange = function (range) {
|
||||||
|
clonedRange = range.cloneRange();
|
||||||
|
|
||||||
|
// we set the selection start and end to avoid error when user selects a text bigger than
|
||||||
|
// the viewport height and uses the arrow keys to expand the selection. In this particular
|
||||||
|
// case is necessary to know where the selections ends because both edges of the selection
|
||||||
|
// is out of the viewport but we only use the end of it to calculate if it needs to scroll
|
||||||
|
clonedRange.setStart(range.endContainer, range.endOffset);
|
||||||
|
clonedRange.setEnd(range.endContainer, range.endOffset);
|
||||||
|
return clonedRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
var getPositionOfRepLineAtOffset = function (node, offset) {
|
||||||
|
// it is not a text node, so we cannot make a selection
|
||||||
|
if (node.tagName === 'BR' || node.tagName === 'EMPTY') {
|
||||||
|
return getPositionOfElementOrSelection(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (node.length === 0 && node.nextSibling) {
|
||||||
|
node = node.nextSibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newRange = new Range();
|
||||||
|
newRange.setStart(node, offset);
|
||||||
|
newRange.setEnd(node, offset);
|
||||||
|
var linePosition = getPositionOfElementOrSelection(newRange);
|
||||||
|
newRange.detach(); // performance sake
|
||||||
|
return linePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPositionOfElementOrSelection(element) {
|
||||||
|
var rect = element.getBoundingClientRect();
|
||||||
|
var linePosition = {
|
||||||
|
bottom: rect.bottom,
|
||||||
|
height: rect.height,
|
||||||
|
top: rect.top
|
||||||
|
}
|
||||||
|
return linePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
// here we have two possibilities:
|
||||||
|
// [1] the line before the caret line has the same type, so both of them has the same margin, padding
|
||||||
|
// height, etc. So, we can use the caret line to make calculation necessary to know where is the top
|
||||||
|
// of the previous line
|
||||||
|
// [2] the line before is part of another rep line. It's possible this line has different margins
|
||||||
|
// height. So we have to get the exactly position of the line
|
||||||
|
exports.getPositionTopOfPreviousBrowserLine = function(caretLinePosition, rep) {
|
||||||
|
var previousLineTop = caretLinePosition.top - caretLinePosition.height; // [1]
|
||||||
|
var isCaretLineFirstBrowserLine = caretLineIsFirstBrowserLine(caretLinePosition.top, rep);
|
||||||
|
|
||||||
|
// the caret is in the beginning of a rep line, so the previous browser line
|
||||||
|
// is the last line browser line of the a rep line
|
||||||
|
if (isCaretLineFirstBrowserLine) { //[2]
|
||||||
|
var lineBeforeCaretLine = rep.selStart[0] - 1;
|
||||||
|
var firstLineVisibleBeforeCaretLine = getPreviousVisibleLine(lineBeforeCaretLine, rep);
|
||||||
|
var linePosition = getDimensionOfLastBrowserLineOfRepLine(firstLineVisibleBeforeCaretLine, rep);
|
||||||
|
previousLineTop = linePosition.top;
|
||||||
|
}
|
||||||
|
return previousLineTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
function caretLineIsFirstBrowserLine(caretLineTop, rep)
|
||||||
|
{
|
||||||
|
var caretRepLine = rep.selStart[0];
|
||||||
|
var lineNode = rep.lines.atIndex(caretRepLine).lineNode;
|
||||||
|
var firstRootNode = getFirstRootChildNode(lineNode);
|
||||||
|
|
||||||
|
// to get the position of the node we get the position of the first char
|
||||||
|
var positionOfFirstRootNode = getPositionOfRepLineAtOffset(firstRootNode, 1);
|
||||||
|
return positionOfFirstRootNode.top === caretLineTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the first root node, usually it is a text node
|
||||||
|
function getFirstRootChildNode(node)
|
||||||
|
{
|
||||||
|
if(!node.firstChild){
|
||||||
|
return node;
|
||||||
|
}else{
|
||||||
|
return getFirstRootChildNode(node.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPreviousVisibleLine(line, rep)
|
||||||
|
{
|
||||||
|
if (line < 0) {
|
||||||
|
return 0;
|
||||||
|
}else if (isLineVisible(line, rep)) {
|
||||||
|
return line;
|
||||||
|
}else{
|
||||||
|
return getPreviousVisibleLine(line - 1, rep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDimensionOfLastBrowserLineOfRepLine(line, rep)
|
||||||
|
{
|
||||||
|
var lineNode = rep.lines.atIndex(line).lineNode;
|
||||||
|
var lastRootChildNode = getLastRootChildNode(lineNode);
|
||||||
|
|
||||||
|
// we get the position of the line in the last char of it
|
||||||
|
var lastRootChildNodePosition = getPositionOfRepLineAtOffset(lastRootChildNode.node, lastRootChildNode.length);
|
||||||
|
return lastRootChildNodePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastRootChildNode(node)
|
||||||
|
{
|
||||||
|
if(!node.lastChild){
|
||||||
|
return {
|
||||||
|
node: node,
|
||||||
|
length: node.length
|
||||||
|
};
|
||||||
|
}else{
|
||||||
|
return getLastRootChildNode(node.lastChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// here we have two possibilities:
|
||||||
|
// [1] The next line is part of the same rep line of the caret line, so we have the same dimensions.
|
||||||
|
// So, we can use the caret line to calculate the bottom of the line.
|
||||||
|
// [2] the next line is part of another rep line. It's possible this line has different dimensions, so we
|
||||||
|
// have to get the exactly dimension of it
|
||||||
|
exports.getBottomOfNextBrowserLine = function(caretLinePosition, rep)
|
||||||
|
{
|
||||||
|
var nextLineBottom = caretLinePosition.bottom + caretLinePosition.height; //[1]
|
||||||
|
var isCaretLineLastBrowserLine = caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep);
|
||||||
|
|
||||||
|
// the caret is at the end of a rep line, so we can get the next browser line dimension
|
||||||
|
// using the position of the first char of the next rep line
|
||||||
|
if(isCaretLineLastBrowserLine){ //[2]
|
||||||
|
var nextLineAfterCaretLine = rep.selStart[0] + 1;
|
||||||
|
var firstNextLineVisibleAfterCaretLine = getNextVisibleLine(nextLineAfterCaretLine, rep);
|
||||||
|
var linePosition = getDimensionOfFirstBrowserLineOfRepLine(firstNextLineVisibleAfterCaretLine, rep);
|
||||||
|
nextLineBottom = linePosition.bottom;
|
||||||
|
}
|
||||||
|
return nextLineBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
function caretLineIsLastBrowserLineOfRepLine(caretLineTop, rep)
|
||||||
|
{
|
||||||
|
var caretRepLine = rep.selStart[0];
|
||||||
|
var lineNode = rep.lines.atIndex(caretRepLine).lineNode;
|
||||||
|
var lastRootChildNode = getLastRootChildNode(lineNode);
|
||||||
|
|
||||||
|
// we take a rep line and get the position of the last char of it
|
||||||
|
var lastRootChildNodePosition = getPositionOfRepLineAtOffset(lastRootChildNode.node, lastRootChildNode.length);
|
||||||
|
return lastRootChildNodePosition.top === caretLineTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPreviousVisibleLine(line, rep)
|
||||||
|
{
|
||||||
|
var firstLineOfPad = 0;
|
||||||
|
if (line <= firstLineOfPad) {
|
||||||
|
return firstLineOfPad;
|
||||||
|
}else if (isLineVisible(line,rep)) {
|
||||||
|
return line;
|
||||||
|
}else{
|
||||||
|
return getPreviousVisibleLine(line - 1, rep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.getPreviousVisibleLine = getPreviousVisibleLine;
|
||||||
|
|
||||||
|
function getNextVisibleLine(line, rep)
|
||||||
|
{
|
||||||
|
var lastLineOfThePad = rep.lines.length() - 1;
|
||||||
|
if (line >= lastLineOfThePad) {
|
||||||
|
return lastLineOfThePad;
|
||||||
|
}else if (isLineVisible(line,rep)) {
|
||||||
|
return line;
|
||||||
|
}else{
|
||||||
|
return getNextVisibleLine(line + 1, rep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.getNextVisibleLine = getNextVisibleLine;
|
||||||
|
|
||||||
|
function isLineVisible(line, rep)
|
||||||
|
{
|
||||||
|
return rep.lines.atIndex(line).lineNode.offsetHeight > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDimensionOfFirstBrowserLineOfRepLine(line, rep)
|
||||||
|
{
|
||||||
|
var lineNode = rep.lines.atIndex(line).lineNode;
|
||||||
|
var firstRootChildNode = getFirstRootChildNode(lineNode);
|
||||||
|
|
||||||
|
// we can get the position of the line, getting the position of the first char of the rep line
|
||||||
|
var firstRootChildNodePosition = getPositionOfRepLineAtOffset(firstRootChildNode, 1);
|
||||||
|
return firstRootChildNodePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectionRange()
|
||||||
|
{
|
||||||
|
var selection;
|
||||||
|
if (!window.getSelection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selection = window.getSelection();
|
||||||
|
if (selection.rangeCount > 0) {
|
||||||
|
return selection.getRangeAt(0);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -524,7 +524,7 @@ function setupGlobalExceptionHandler() {
|
||||||
$("#editorloadingbox").css("padding", "10px");
|
$("#editorloadingbox").css("padding", "10px");
|
||||||
$("#editorloadingbox").css("padding-top", "45px");
|
$("#editorloadingbox").css("padding-top", "45px");
|
||||||
$("#editorloadingbox").html("<div style='text-align:left;color:red;font-size:16px;'><b>An error occurred</b><br>The error was reported with the following id: '" + errorId + "'<br><br><span style='color:black;font-weight:bold;font-size:16px'>Please press and hold Ctrl and press F5 to reload this page, if the problem persists please send this error message to your webmaster: </span><div style='color:black;font-size:14px'>'"
|
$("#editorloadingbox").html("<div style='text-align:left;color:red;font-size:16px;'><b>An error occurred</b><br>The error was reported with the following id: '" + errorId + "'<br><br><span style='color:black;font-weight:bold;font-size:16px'>Please press and hold Ctrl and press F5 to reload this page, if the problem persists please send this error message to your webmaster: </span><div style='color:black;font-size:14px'>'"
|
||||||
+ "ErrorId: " + errorId + "<br>URL: " + window.location.href + "<br>UserAgent: " + userAgent + "<br>" + msg + " in " + url + " at line " + linenumber + "'</div></div>");
|
+ "ErrorId: " + errorId + "<br>URL: " + padutils.escapeHtml(window.location.href) + "<br>UserAgent: " + userAgent + "<br>" + msg + " in " + url + " at line " + linenumber + "'</div></div>");
|
||||||
}
|
}
|
||||||
|
|
||||||
//send javascript errors to the server
|
//send javascript errors to the server
|
||||||
|
|
366
src/static/js/scroll.js
Normal file
366
src/static/js/scroll.js
Normal file
|
@ -0,0 +1,366 @@
|
||||||
|
/*
|
||||||
|
This file handles scroll on edition or when user presses arrow keys.
|
||||||
|
In this file we have two representations of line (browser and rep line).
|
||||||
|
Rep Line = a line in the way is represented by Etherpad(rep) (each <div> is a line)
|
||||||
|
Browser Line = each vertical line. A <div> can be break into more than one
|
||||||
|
browser line.
|
||||||
|
*/
|
||||||
|
var caretPosition = require('/caretPosition');
|
||||||
|
|
||||||
|
function Scroll(outerWin) {
|
||||||
|
// scroll settings
|
||||||
|
this.scrollSettings = parent.parent.clientVars.scrollWhenFocusLineIsOutOfViewport;
|
||||||
|
|
||||||
|
// DOM reference
|
||||||
|
this.outerWin = outerWin;
|
||||||
|
this.doc = this.outerWin.document;
|
||||||
|
this.rootDocument = parent.parent.document;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary = function (rep, isScrollableEvent, innerHeight)
|
||||||
|
{
|
||||||
|
// are we placing the caret on the line at the bottom of viewport?
|
||||||
|
// And if so, do we need to scroll the editor, as defined on the settings.json?
|
||||||
|
var shouldScrollWhenCaretIsAtBottomOfViewport = this.scrollSettings.scrollWhenCaretIsInTheLastLineOfViewport;
|
||||||
|
if (shouldScrollWhenCaretIsAtBottomOfViewport) {
|
||||||
|
// avoid scrolling when selection includes multiple lines -- user can potentially be selecting more lines
|
||||||
|
// than it fits on viewport
|
||||||
|
var multipleLinesSelected = rep.selStart[0] !== rep.selEnd[0];
|
||||||
|
|
||||||
|
// avoid scrolling when pad loads
|
||||||
|
if (isScrollableEvent && !multipleLinesSelected && this._isCaretAtTheBottomOfViewport(rep)) {
|
||||||
|
// when scrollWhenFocusLineIsOutOfViewport.percentage is 0, pixelsToScroll is 0
|
||||||
|
var pixelsToScroll = this._getPixelsRelativeToPercentageOfViewport(innerHeight);
|
||||||
|
this._scrollYPage(pixelsToScroll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype.scrollWhenPressArrowKeys = function(arrowUp, rep, innerHeight)
|
||||||
|
{
|
||||||
|
// if percentageScrollArrowUp is 0, let the scroll to be handled as default, put the previous
|
||||||
|
// rep line on the top of the viewport
|
||||||
|
if(this._arrowUpWasPressedInTheFirstLineOfTheViewport(arrowUp, rep)){
|
||||||
|
var pixelsToScroll = this._getPixelsToScrollWhenUserPressesArrowUp(innerHeight);
|
||||||
|
|
||||||
|
// by default, the browser scrolls to the middle of the viewport. To avoid the twist made
|
||||||
|
// when we apply a second scroll, we made it immediately (without animation)
|
||||||
|
this._scrollYPageWithoutAnimation(-pixelsToScroll);
|
||||||
|
}else{
|
||||||
|
this.scrollNodeVerticallyIntoView(rep, innerHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some plugins might set a minimum height to the editor (ex: ep_page_view), so checking
|
||||||
|
// if (caretLine() === rep.lines.length() - 1) is not enough. We need to check if there are
|
||||||
|
// other lines after caretLine(), and all of them are out of viewport.
|
||||||
|
Scroll.prototype._isCaretAtTheBottomOfViewport = function(rep)
|
||||||
|
{
|
||||||
|
// computing a line position using getBoundingClientRect() is expensive.
|
||||||
|
// (obs: getBoundingClientRect() is called on caretPosition.getPosition())
|
||||||
|
// To avoid that, we only call this function when it is possible that the
|
||||||
|
// caret is in the bottom of viewport
|
||||||
|
var caretLine = rep.selStart[0];
|
||||||
|
var lineAfterCaretLine = caretLine + 1;
|
||||||
|
var firstLineVisibleAfterCaretLine = caretPosition.getNextVisibleLine(lineAfterCaretLine, rep);
|
||||||
|
var caretLineIsPartiallyVisibleOnViewport = this._isLinePartiallyVisibleOnViewport(caretLine, rep);
|
||||||
|
var lineAfterCaretLineIsPartiallyVisibleOnViewport = this._isLinePartiallyVisibleOnViewport(firstLineVisibleAfterCaretLine, rep);
|
||||||
|
if (caretLineIsPartiallyVisibleOnViewport || lineAfterCaretLineIsPartiallyVisibleOnViewport) {
|
||||||
|
// check if the caret is in the bottom of the viewport
|
||||||
|
var caretLinePosition = caretPosition.getPosition();
|
||||||
|
var viewportBottom = this._getViewPortTopBottom().bottom;
|
||||||
|
var nextLineBottom = caretPosition.getBottomOfNextBrowserLine(caretLinePosition, rep);
|
||||||
|
var nextLineIsBelowViewportBottom = nextLineBottom > viewportBottom;
|
||||||
|
return nextLineIsBelowViewportBottom;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype._isLinePartiallyVisibleOnViewport = function(lineNumber, rep)
|
||||||
|
{
|
||||||
|
var lineNode = rep.lines.atIndex(lineNumber);
|
||||||
|
var linePosition = this._getLineEntryTopBottom(lineNode);
|
||||||
|
var lineTop = linePosition.top;
|
||||||
|
var lineBottom = linePosition.bottom;
|
||||||
|
var viewport = this._getViewPortTopBottom();
|
||||||
|
var viewportBottom = viewport.bottom;
|
||||||
|
var viewportTop = viewport.top;
|
||||||
|
|
||||||
|
var topOfLineIsAboveOfViewportBottom = lineTop < viewportBottom;
|
||||||
|
var bottomOfLineIsOnOrBelowOfViewportBottom = lineBottom >= viewportBottom;
|
||||||
|
var topOfLineIsBelowViewportTop = lineTop >= viewportTop;
|
||||||
|
var topOfLineIsAboveViewportBottom = lineTop <= viewportBottom;
|
||||||
|
var bottomOfLineIsAboveViewportBottom = lineBottom <= viewportBottom;
|
||||||
|
var bottomOfLineIsBelowViewportTop = lineBottom >= viewportTop;
|
||||||
|
|
||||||
|
return (topOfLineIsAboveOfViewportBottom && bottomOfLineIsOnOrBelowOfViewportBottom) ||
|
||||||
|
(topOfLineIsBelowViewportTop && topOfLineIsAboveViewportBottom) ||
|
||||||
|
(bottomOfLineIsAboveViewportBottom && bottomOfLineIsBelowViewportTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype._getViewPortTopBottom = function()
|
||||||
|
{
|
||||||
|
var theTop = this.getScrollY();
|
||||||
|
var doc = this.doc;
|
||||||
|
var height = doc.documentElement.clientHeight; // includes padding
|
||||||
|
|
||||||
|
// we have to get the exactly height of the viewport. So it has to subtract all the values which changes
|
||||||
|
// the viewport height (E.g. padding, position top)
|
||||||
|
var viewportExtraSpacesAndPosition = this._getEditorPositionTop() + this._getPaddingTopAddedWhenPageViewIsEnable();
|
||||||
|
return {
|
||||||
|
top: theTop,
|
||||||
|
bottom: (theTop + height - viewportExtraSpacesAndPosition)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype._getEditorPositionTop = function()
|
||||||
|
{
|
||||||
|
var editor = parent.document.getElementsByTagName('iframe');
|
||||||
|
var editorPositionTop = editor[0].offsetTop;
|
||||||
|
return editorPositionTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ep_page_view adds padding-top, which makes the viewport smaller
|
||||||
|
Scroll.prototype._getPaddingTopAddedWhenPageViewIsEnable = function()
|
||||||
|
{
|
||||||
|
var aceOuter = this.rootDocument.getElementsByName("ace_outer");
|
||||||
|
var aceOuterPaddingTop = parseInt($(aceOuter).css("padding-top"));
|
||||||
|
return aceOuterPaddingTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype._getScrollXY = function()
|
||||||
|
{
|
||||||
|
var win = this.outerWin;
|
||||||
|
var odoc = this.doc;
|
||||||
|
if (typeof(win.pageYOffset) == "number")
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
x: win.pageXOffset,
|
||||||
|
y: win.pageYOffset
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var docel = odoc.documentElement;
|
||||||
|
if (docel && typeof(docel.scrollTop) == "number")
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
x: docel.scrollLeft,
|
||||||
|
y: docel.scrollTop
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype.getScrollX = function()
|
||||||
|
{
|
||||||
|
return this._getScrollXY().x;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype.getScrollY = function()
|
||||||
|
{
|
||||||
|
return this._getScrollXY().y;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype.setScrollX = function(x)
|
||||||
|
{
|
||||||
|
this.outerWin.scrollTo(x, this.getScrollY());
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype.setScrollY = function(y)
|
||||||
|
{
|
||||||
|
this.outerWin.scrollTo(this.getScrollX(), y);
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype.setScrollXY = function(x, y)
|
||||||
|
{
|
||||||
|
this.outerWin.scrollTo(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype._isCaretAtTheTopOfViewport = function(rep)
|
||||||
|
{
|
||||||
|
var caretLine = rep.selStart[0];
|
||||||
|
var linePrevCaretLine = caretLine - 1;
|
||||||
|
var firstLineVisibleBeforeCaretLine = caretPosition.getPreviousVisibleLine(linePrevCaretLine, rep);
|
||||||
|
var caretLineIsPartiallyVisibleOnViewport = this._isLinePartiallyVisibleOnViewport(caretLine, rep);
|
||||||
|
var lineBeforeCaretLineIsPartiallyVisibleOnViewport = this._isLinePartiallyVisibleOnViewport(firstLineVisibleBeforeCaretLine, rep);
|
||||||
|
if (caretLineIsPartiallyVisibleOnViewport || lineBeforeCaretLineIsPartiallyVisibleOnViewport) {
|
||||||
|
var caretLinePosition = caretPosition.getPosition(); // get the position of the browser line
|
||||||
|
var viewportPosition = this._getViewPortTopBottom();
|
||||||
|
var viewportTop = viewportPosition.top;
|
||||||
|
var viewportBottom = viewportPosition.bottom;
|
||||||
|
var caretLineIsBelowViewportTop = caretLinePosition.bottom >= viewportTop;
|
||||||
|
var caretLineIsAboveViewportBottom = caretLinePosition.top < viewportBottom;
|
||||||
|
var caretLineIsInsideOfViewport = caretLineIsBelowViewportTop && caretLineIsAboveViewportBottom;
|
||||||
|
if (caretLineIsInsideOfViewport) {
|
||||||
|
var prevLineTop = caretPosition.getPositionTopOfPreviousBrowserLine(caretLinePosition, rep);
|
||||||
|
var previousLineIsAboveViewportTop = prevLineTop < viewportTop;
|
||||||
|
return previousLineIsAboveViewportTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// By default, when user makes an edition in a line out of viewport, this line goes
|
||||||
|
// to the edge of viewport. This function gets the extra pixels necessary to get the
|
||||||
|
// caret line in a position X relative to Y% viewport.
|
||||||
|
Scroll.prototype._getPixelsRelativeToPercentageOfViewport = function(innerHeight, aboveOfViewport)
|
||||||
|
{
|
||||||
|
var pixels = 0;
|
||||||
|
var scrollPercentageRelativeToViewport = this._getPercentageToScroll(aboveOfViewport);
|
||||||
|
if(scrollPercentageRelativeToViewport > 0 && scrollPercentageRelativeToViewport <= 1){
|
||||||
|
pixels = parseInt(innerHeight * scrollPercentageRelativeToViewport);
|
||||||
|
}
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we use different percentages when change selection. It depends on if it is
|
||||||
|
// either above the top or below the bottom of the page
|
||||||
|
Scroll.prototype._getPercentageToScroll = function(aboveOfViewport)
|
||||||
|
{
|
||||||
|
var percentageToScroll = this.scrollSettings.percentage.editionBelowViewport;
|
||||||
|
if(aboveOfViewport){
|
||||||
|
percentageToScroll = this.scrollSettings.percentage.editionAboveViewport;
|
||||||
|
}
|
||||||
|
return percentageToScroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype._getPixelsToScrollWhenUserPressesArrowUp = function(innerHeight)
|
||||||
|
{
|
||||||
|
var pixels = 0;
|
||||||
|
var percentageToScrollUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp;
|
||||||
|
if(percentageToScrollUp > 0 && percentageToScrollUp <= 1){
|
||||||
|
pixels = parseInt(innerHeight * percentageToScrollUp);
|
||||||
|
}
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype._scrollYPage = function(pixelsToScroll)
|
||||||
|
{
|
||||||
|
var durationOfAnimationToShowFocusline = this.scrollSettings.duration;
|
||||||
|
if(durationOfAnimationToShowFocusline){
|
||||||
|
this._scrollYPageWithAnimation(pixelsToScroll, durationOfAnimationToShowFocusline);
|
||||||
|
}else{
|
||||||
|
this._scrollYPageWithoutAnimation(pixelsToScroll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype._scrollYPageWithoutAnimation = function(pixelsToScroll)
|
||||||
|
{
|
||||||
|
this.outerWin.scrollBy(0, pixelsToScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype._scrollYPageWithAnimation = function(pixelsToScroll, durationOfAnimationToShowFocusline)
|
||||||
|
{
|
||||||
|
var outerDocBody = this.doc.getElementById("outerdocbody");
|
||||||
|
|
||||||
|
// it works on later versions of Chrome
|
||||||
|
var $outerDocBody = $(outerDocBody);
|
||||||
|
this._triggerScrollWithAnimation($outerDocBody, pixelsToScroll, durationOfAnimationToShowFocusline);
|
||||||
|
|
||||||
|
// it works on Firefox and earlier versions of Chrome
|
||||||
|
var $outerDocBodyParent = $outerDocBody.parent();
|
||||||
|
this._triggerScrollWithAnimation($outerDocBodyParent, pixelsToScroll, durationOfAnimationToShowFocusline);
|
||||||
|
}
|
||||||
|
|
||||||
|
// using a custom queue and clearing it, we avoid creating a queue of scroll animations. So if this function
|
||||||
|
// is called twice quickly, only the last one runs.
|
||||||
|
Scroll.prototype._triggerScrollWithAnimation = function($elem, pixelsToScroll, durationOfAnimationToShowFocusline)
|
||||||
|
{
|
||||||
|
// clear the queue of animation
|
||||||
|
$elem.stop("scrollanimation");
|
||||||
|
$elem.animate({
|
||||||
|
scrollTop: '+=' + pixelsToScroll
|
||||||
|
}, {
|
||||||
|
duration: durationOfAnimationToShowFocusline,
|
||||||
|
queue: "scrollanimation"
|
||||||
|
}).dequeue("scrollanimation");
|
||||||
|
}
|
||||||
|
|
||||||
|
// scrollAmountWhenFocusLineIsOutOfViewport is set to 0 (default), scroll it the minimum distance
|
||||||
|
// needed to be completely in view. If the value is greater than 0 and less than or equal to 1,
|
||||||
|
// besides of scrolling the minimum needed to be visible, it scrolls additionally
|
||||||
|
// (viewport height * scrollAmountWhenFocusLineIsOutOfViewport) pixels
|
||||||
|
Scroll.prototype.scrollNodeVerticallyIntoView = function(rep, innerHeight)
|
||||||
|
{
|
||||||
|
var viewport = this._getViewPortTopBottom();
|
||||||
|
var isPartOfRepLineOutOfViewport = this._partOfRepLineIsOutOfViewport(viewport, rep);
|
||||||
|
|
||||||
|
// when the selection changes outside of the viewport the browser automatically scrolls the line
|
||||||
|
// to inside of the viewport. Tested on IE, Firefox, Chrome in releases from 2015 until now
|
||||||
|
// So, when the line scrolled gets outside of the viewport we let the browser handle it.
|
||||||
|
var linePosition = caretPosition.getPosition();
|
||||||
|
if(linePosition){
|
||||||
|
var distanceOfTopOfViewport = linePosition.top - viewport.top;
|
||||||
|
var distanceOfBottomOfViewport = viewport.bottom - linePosition.bottom;
|
||||||
|
var caretIsAboveOfViewport = distanceOfTopOfViewport < 0;
|
||||||
|
var caretIsBelowOfViewport = distanceOfBottomOfViewport < 0;
|
||||||
|
if(caretIsAboveOfViewport){
|
||||||
|
var pixelsToScroll = distanceOfTopOfViewport - this._getPixelsRelativeToPercentageOfViewport(innerHeight, true);
|
||||||
|
this._scrollYPage(pixelsToScroll);
|
||||||
|
}else if(caretIsBelowOfViewport){
|
||||||
|
var pixelsToScroll = -distanceOfBottomOfViewport + this._getPixelsRelativeToPercentageOfViewport(innerHeight);
|
||||||
|
this._scrollYPage(pixelsToScroll);
|
||||||
|
}else{
|
||||||
|
this.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep, true, innerHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype._partOfRepLineIsOutOfViewport = function(viewportPosition, rep)
|
||||||
|
{
|
||||||
|
var focusLine = (rep.selFocusAtStart ? rep.selStart[0] : rep.selEnd[0]);
|
||||||
|
var line = rep.lines.atIndex(focusLine);
|
||||||
|
var linePosition = this._getLineEntryTopBottom(line);
|
||||||
|
var lineIsAboveOfViewport = linePosition.top < viewportPosition.top;
|
||||||
|
var lineIsBelowOfViewport = linePosition.bottom > viewportPosition.bottom;
|
||||||
|
|
||||||
|
return lineIsBelowOfViewport || lineIsAboveOfViewport;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype._getLineEntryTopBottom = function(entry, destObj)
|
||||||
|
{
|
||||||
|
var dom = entry.lineNode;
|
||||||
|
var top = dom.offsetTop;
|
||||||
|
var height = dom.offsetHeight;
|
||||||
|
var obj = (destObj || {});
|
||||||
|
obj.top = top;
|
||||||
|
obj.bottom = (top + height);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype._arrowUpWasPressedInTheFirstLineOfTheViewport = function(arrowUp, rep)
|
||||||
|
{
|
||||||
|
var percentageScrollArrowUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp;
|
||||||
|
return percentageScrollArrowUp && arrowUp && this._isCaretAtTheTopOfViewport(rep);
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype.getVisibleLineRange = function(rep)
|
||||||
|
{
|
||||||
|
var viewport = this._getViewPortTopBottom();
|
||||||
|
//console.log("viewport top/bottom: %o", viewport);
|
||||||
|
var obj = {};
|
||||||
|
var self = this;
|
||||||
|
var start = rep.lines.search(function(e)
|
||||||
|
{
|
||||||
|
return self._getLineEntryTopBottom(e, obj).bottom > viewport.top;
|
||||||
|
});
|
||||||
|
var end = rep.lines.search(function(e)
|
||||||
|
{
|
||||||
|
// return the first line that the top position is greater or equal than
|
||||||
|
// the viewport. That is the first line that is below the viewport bottom.
|
||||||
|
// So the line that is in the bottom of the viewport is the very previous one.
|
||||||
|
return self._getLineEntryTopBottom(e, obj).top >= viewport.bottom;
|
||||||
|
});
|
||||||
|
if (end < start) end = start; // unlikely
|
||||||
|
// top.console.log(start+","+(end -1));
|
||||||
|
return [start, end - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.prototype.getVisibleCharRange = function(rep)
|
||||||
|
{
|
||||||
|
var lineRange = this.getVisibleLineRange(rep);
|
||||||
|
return [rep.lines.offsetOfIndex(lineRange[0]), rep.lines.offsetOfIndex(lineRange[1])];
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.init = function(outerWin)
|
||||||
|
{
|
||||||
|
return new Scroll(outerWin);
|
||||||
|
}
|
649
tests/frontend/specs/scroll.js
Normal file
649
tests/frontend/specs/scroll.js
Normal file
|
@ -0,0 +1,649 @@
|
||||||
|
describe('scroll when focus line is out of viewport', function () {
|
||||||
|
before(function (done) {
|
||||||
|
helper.newPad(function(){
|
||||||
|
cleanPad(function(){
|
||||||
|
forceUseMonospacedFont();
|
||||||
|
scrollWhenPlaceCaretInTheLastLineOfViewport();
|
||||||
|
createPadWithSeveralLines(function(){
|
||||||
|
resizeEditorHeight();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.timeout(20000);
|
||||||
|
});
|
||||||
|
|
||||||
|
context('when user presses any arrow keys on a line above the viewport', function(){
|
||||||
|
context('and scroll percentage config is set to 0.2 on settings.json', function(){
|
||||||
|
var lineCloseOfTopOfPad = 10;
|
||||||
|
before(function (done) {
|
||||||
|
setScrollPercentageWhenFocusLineIsOutOfViewport(0.2, true);
|
||||||
|
scrollEditorToBottomOfPad();
|
||||||
|
|
||||||
|
placeCaretInTheBeginningOfLine(lineCloseOfTopOfPad, function(){ // place caret in the 10th line
|
||||||
|
// warning: even pressing right arrow, the caret does not change of position
|
||||||
|
// the column where the caret is, it has not importance, only the line
|
||||||
|
pressAndReleaseRightArrow();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the focus line scrolled 20% from the top of the viewport', function (done) {
|
||||||
|
// default behavior is to put the line in the top of viewport, but as
|
||||||
|
// scrollPercentageWhenFocusLineIsOutOfViewport is set to 0.2, we have an extra 20% of lines scrolled
|
||||||
|
// (2 lines, which are the 20% of the 10 that are visible on viewport)
|
||||||
|
var firstLineOfViewport = getFirstLineVisibileOfViewport();
|
||||||
|
expect(lineCloseOfTopOfPad).to.be(firstLineOfViewport + 2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('when user presses any arrow keys on a line below the viewport', function(){
|
||||||
|
context('and scroll percentage config is set to 0.7 on settings.json', function(){
|
||||||
|
var lineCloseToBottomOfPad = 50;
|
||||||
|
before(function (done) {
|
||||||
|
setScrollPercentageWhenFocusLineIsOutOfViewport(0.7);
|
||||||
|
|
||||||
|
// firstly, scroll to make the lineCloseToBottomOfPad visible. After that, scroll to make it out of viewport
|
||||||
|
scrollEditorToTopOfPad();
|
||||||
|
placeCaretAtTheEndOfLine(lineCloseToBottomOfPad); // place caret in the 50th line
|
||||||
|
setTimeout(function() {
|
||||||
|
// warning: even pressing right arrow, the caret does not change of position
|
||||||
|
pressAndReleaseLeftArrow();
|
||||||
|
done();
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the focus line scrolled 70% from the bottom of the viewport', function (done) {
|
||||||
|
// default behavior is to put the line in the top of viewport, but as
|
||||||
|
// scrollPercentageWhenFocusLineIsOutOfViewport is set to 0.7, we have an extra 70% of lines scrolled
|
||||||
|
// (7 lines, which are the 70% of the 10 that are visible on viewport)
|
||||||
|
var lastLineOfViewport = getLastLineVisibleOfViewport();
|
||||||
|
expect(lineCloseToBottomOfPad).to.be(lastLineOfViewport - 7);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('when user presses arrow up on the first line of the viewport', function(){
|
||||||
|
context('and percentageToScrollWhenUserPressesArrowUp is set to 0.3', function () {
|
||||||
|
var lineOnTopOfViewportWhenThePadIsScrolledDown;
|
||||||
|
before(function (done) {
|
||||||
|
setPercentageToScrollWhenUserPressesArrowUp(0.3);
|
||||||
|
|
||||||
|
// we need some room to make the scroll up
|
||||||
|
scrollEditorToBottomOfPad();
|
||||||
|
lineOnTopOfViewportWhenThePadIsScrolledDown = 91;
|
||||||
|
placeCaretAtTheEndOfLine(lineOnTopOfViewportWhenThePadIsScrolledDown);
|
||||||
|
setTimeout(function() {
|
||||||
|
// warning: even pressing up arrow, the caret does not change of position
|
||||||
|
pressAndReleaseUpArrow();
|
||||||
|
done();
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the focus line scrolled 30% of the top of the viewport', function (done) {
|
||||||
|
// default behavior is to put the line in the top of viewport, but as
|
||||||
|
// PercentageToScrollWhenUserPressesArrowUp is set to 0.3, we have an extra 30% of lines scrolled
|
||||||
|
// (3 lines, which are the 30% of the 10 that are visible on viewport)
|
||||||
|
var firstLineOfViewport = getFirstLineVisibileOfViewport();
|
||||||
|
expect(firstLineOfViewport).to.be(lineOnTopOfViewportWhenThePadIsScrolledDown - 3);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('when user edits the last line of viewport', function(){
|
||||||
|
context('and scroll percentage config is set to 0 on settings.json', function(){
|
||||||
|
var lastLineOfViewportBeforeEnter = 10;
|
||||||
|
before(function () {
|
||||||
|
// the default value
|
||||||
|
resetScrollPercentageWhenFocusLineIsOutOfViewport();
|
||||||
|
|
||||||
|
// make sure the last line on viewport is the 10th one
|
||||||
|
scrollEditorToTopOfPad();
|
||||||
|
placeCaretAtTheEndOfLine(lastLineOfViewportBeforeEnter);
|
||||||
|
pressEnter();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the focus line on the bottom of the viewport', function (done) {
|
||||||
|
var lastLineOfViewportAfterEnter = getLastLineVisibleOfViewport();
|
||||||
|
expect(lastLineOfViewportAfterEnter).to.be(lastLineOfViewportBeforeEnter + 1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('and scrollPercentageWhenFocusLineIsOutOfViewport is set to 0.3', function(){ // this value is arbitrary
|
||||||
|
var lastLineOfViewportBeforeEnter = 9;
|
||||||
|
before(function () {
|
||||||
|
setScrollPercentageWhenFocusLineIsOutOfViewport(0.3);
|
||||||
|
|
||||||
|
// make sure the last line on viewport is the 10th one
|
||||||
|
scrollEditorToTopOfPad();
|
||||||
|
placeCaretAtTheEndOfLine(lastLineOfViewportBeforeEnter);
|
||||||
|
pressBackspace();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scrolls 30% of viewport up', function (done) {
|
||||||
|
var lastLineOfViewportAfterEnter = getLastLineVisibleOfViewport();
|
||||||
|
// default behavior is to scroll one line at the bottom of viewport, but as
|
||||||
|
// scrollPercentageWhenFocusLineIsOutOfViewport is set to 0.3, we have an extra 30% of lines scrolled
|
||||||
|
// (3 lines, which are the 30% of the 10 that are visible on viewport)
|
||||||
|
expect(lastLineOfViewportAfterEnter).to.be(lastLineOfViewportBeforeEnter + 3);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('and it is set to a value that overflow the interval [0, 1]', function(){
|
||||||
|
var lastLineOfViewportBeforeEnter = 10;
|
||||||
|
before(function(){
|
||||||
|
var scrollPercentageWhenFocusLineIsOutOfViewport = 1.5;
|
||||||
|
scrollEditorToTopOfPad();
|
||||||
|
placeCaretAtTheEndOfLine(lastLineOfViewportBeforeEnter);
|
||||||
|
setScrollPercentageWhenFocusLineIsOutOfViewport(scrollPercentageWhenFocusLineIsOutOfViewport);
|
||||||
|
pressEnter();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the default behavior of moving the focus line on the bottom of the viewport', function (done) {
|
||||||
|
var lastLineOfViewportAfterEnter = getLastLineVisibleOfViewport();
|
||||||
|
expect(lastLineOfViewportAfterEnter).to.be(lastLineOfViewportBeforeEnter + 1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('when user edits a line above the viewport', function(){
|
||||||
|
context('and scroll percentage config is set to 0 on settings.json', function(){
|
||||||
|
var lineCloseOfTopOfPad = 10;
|
||||||
|
before(function () {
|
||||||
|
// the default value
|
||||||
|
setScrollPercentageWhenFocusLineIsOutOfViewport(0);
|
||||||
|
|
||||||
|
// firstly, scroll to make the lineCloseOfTopOfPad visible. After that, scroll to make it out of viewport
|
||||||
|
scrollEditorToTopOfPad();
|
||||||
|
placeCaretAtTheEndOfLine(lineCloseOfTopOfPad); // place caret in the 10th line
|
||||||
|
scrollEditorToBottomOfPad();
|
||||||
|
pressBackspace(); // edit the line where the caret is, which is above the viewport
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the focus line on the top of the viewport', function (done) {
|
||||||
|
var firstLineOfViewportAfterEnter = getFirstLineVisibileOfViewport();
|
||||||
|
expect(firstLineOfViewportAfterEnter).to.be(lineCloseOfTopOfPad);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('and scrollPercentageWhenFocusLineIsOutOfViewport is set to 0.2', function(){ // this value is arbitrary
|
||||||
|
var lineCloseToBottomOfPad = 50;
|
||||||
|
before(function () {
|
||||||
|
// we force the line edited to be above the top of the viewport
|
||||||
|
setScrollPercentageWhenFocusLineIsOutOfViewport(0.2, true); // set scroll jump to 20%
|
||||||
|
scrollEditorToTopOfPad();
|
||||||
|
placeCaretAtTheEndOfLine(lineCloseToBottomOfPad);
|
||||||
|
scrollEditorToBottomOfPad();
|
||||||
|
pressBackspace(); // edit line
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scrolls 20% of viewport down', function (done) {
|
||||||
|
// default behavior is to scroll one line at the top of viewport, but as
|
||||||
|
// scrollPercentageWhenFocusLineIsOutOfViewport is set to 0.2, we have an extra 20% of lines scrolled
|
||||||
|
// (2 lines, which are the 20% of the 10 that are visible on viewport)
|
||||||
|
var firstLineVisibileOfViewport = getFirstLineVisibileOfViewport();
|
||||||
|
expect(lineCloseToBottomOfPad).to.be(firstLineVisibileOfViewport + 2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('when user places the caret at the last line visible of viewport', function(){
|
||||||
|
var lastLineVisible;
|
||||||
|
context('and scroll percentage config is set to 0 on settings.json', function(){
|
||||||
|
before(function (done) {
|
||||||
|
// reset to the default value
|
||||||
|
resetScrollPercentageWhenFocusLineIsOutOfViewport();
|
||||||
|
|
||||||
|
placeCaretInTheBeginningOfLine(0, function(){ // reset caret position
|
||||||
|
scrollEditorToTopOfPad();
|
||||||
|
lastLineVisible = getLastLineVisibleOfViewport();
|
||||||
|
placeCaretInTheBeginningOfLine(lastLineVisible, done); // place caret in the 9th line
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not scroll', function(done){
|
||||||
|
setTimeout(function() {
|
||||||
|
var lastLineOfViewport = getLastLineVisibleOfViewport();
|
||||||
|
var lineDoesNotScroll = lastLineOfViewport === lastLineVisible;
|
||||||
|
expect(lineDoesNotScroll).to.be(true);
|
||||||
|
done();
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
context('and scroll percentage config is set to 0.5 on settings.json', function(){
|
||||||
|
before(function (done) {
|
||||||
|
setScrollPercentageWhenFocusLineIsOutOfViewport(0.5);
|
||||||
|
scrollEditorToTopOfPad();
|
||||||
|
placeCaretInTheBeginningOfLine(0, function(){ // reset caret position
|
||||||
|
// this timeout inside a callback is ugly but it necessary to give time to aceSelectionChange
|
||||||
|
// realizes that the selection has been changed
|
||||||
|
setTimeout(function() {
|
||||||
|
lastLineVisible = getLastLineVisibleOfViewport();
|
||||||
|
placeCaretInTheBeginningOfLine(lastLineVisible, done); // place caret in the 9th line
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scrolls line to 50% of the viewport', function(done){
|
||||||
|
helper.waitFor(function(){
|
||||||
|
var lastLineOfViewport = getLastLineVisibleOfViewport();
|
||||||
|
var lastLinesScrolledFiveLinesUp = lastLineOfViewport - 5 === lastLineVisible;
|
||||||
|
return lastLinesScrolledFiveLinesUp;
|
||||||
|
}).done(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is a special case. When user is selecting a text with arrow down or arrow left we have
|
||||||
|
// to keep the last line selected on focus
|
||||||
|
context('when the first line selected is out of the viewport and user presses shift arrow down', function(){
|
||||||
|
var lastLineOfPad = 99;
|
||||||
|
before(function (done) {
|
||||||
|
scrollEditorToTopOfPad();
|
||||||
|
|
||||||
|
// make a selection bigger than the viewport height
|
||||||
|
var $firstLineOfSelection = getLine(0);
|
||||||
|
var $lastLineOfSelection = getLine(lastLineOfPad);
|
||||||
|
var lengthOfLastLine = $lastLineOfSelection.text().length;
|
||||||
|
helper.selectLines($firstLineOfSelection, $lastLineOfSelection, 0, lengthOfLastLine);
|
||||||
|
|
||||||
|
// place the last line selected on the viewport
|
||||||
|
scrollEditorToBottomOfPad();
|
||||||
|
|
||||||
|
// press a key to make the selection goes down
|
||||||
|
// although we can't simulate the extending of selection. It's possible to send a key event
|
||||||
|
// which is captured on ace2_inner scroll function.
|
||||||
|
pressAndReleaseLeftArrow(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the last line selected on focus', function (done) {
|
||||||
|
var lastLineOfSelectionIsVisible = isLineOnViewport(lastLineOfPad);
|
||||||
|
expect(lastLineOfSelectionIsVisible).to.be(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// In this scenario we avoid the bouncing scroll. E.g Let's suppose we have a big line that is
|
||||||
|
// the size of the viewport, and its top is above the viewport. When user presses '<-', this line
|
||||||
|
// will scroll down because the top is out of the viewport. When it scrolls down, the bottom of
|
||||||
|
// line gets below the viewport so when user presses '<-' again it scrolls up to make the bottom
|
||||||
|
// of line visible. If user presses arrow keys more than one time, the editor will keep scrolling up and down
|
||||||
|
context('when the line height is bigger than the scroll amount percentage * viewport height', function(){
|
||||||
|
var scrollOfEditorBeforePressKey;
|
||||||
|
var BIG_LINE_NUMBER = 0;
|
||||||
|
var MIDDLE_OF_BIG_LINE = 51;
|
||||||
|
before(function (done) {
|
||||||
|
createPadWithALineHigherThanViewportHeight(this, BIG_LINE_NUMBER, function(){
|
||||||
|
setScrollPercentageWhenFocusLineIsOutOfViewport(0.5); // set any value to force scroll to outside to viewport
|
||||||
|
var $bigLine = getLine(BIG_LINE_NUMBER);
|
||||||
|
|
||||||
|
// each line has about 5 chars, we place the caret in the middle of the line
|
||||||
|
helper.selectLines($bigLine, $bigLine, MIDDLE_OF_BIG_LINE, MIDDLE_OF_BIG_LINE);
|
||||||
|
|
||||||
|
scrollEditorToLeaveTopAndBottomOfBigLineOutOfViewport($bigLine);
|
||||||
|
scrollOfEditorBeforePressKey = getEditorScroll();
|
||||||
|
|
||||||
|
// press a key to force to scroll
|
||||||
|
pressAndReleaseRightArrow();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// reset pad to the original text
|
||||||
|
after(function (done) {
|
||||||
|
this.timeout(5000);
|
||||||
|
cleanPad(function(){
|
||||||
|
createPadWithSeveralLines(function(){
|
||||||
|
resetEditorWidth();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// as the editor.line is inside of the viewport, it should not scroll
|
||||||
|
it('should not scroll', function (done) {
|
||||||
|
var scrollOfEditorAfterPressKey = getEditorScroll();
|
||||||
|
expect(scrollOfEditorAfterPressKey).to.be(scrollOfEditorBeforePressKey);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Some plugins, for example the ep_page_view, change the editor dimensions. This plugin, for example,
|
||||||
|
// adds padding-top to the ace_outer, which changes the viewport height
|
||||||
|
describe('integration with plugins which changes the margin of editor', function(){
|
||||||
|
context('when editor dimensions changes', function(){
|
||||||
|
before(function () {
|
||||||
|
// reset the size of editor. Now we show more than 10 lines as in the other tests
|
||||||
|
resetResizeOfEditorHeight();
|
||||||
|
scrollEditorToTopOfPad();
|
||||||
|
|
||||||
|
// height of the editor viewport
|
||||||
|
var editorHeight = getEditorHeight();
|
||||||
|
|
||||||
|
// add a big padding-top, 50% of the viewport
|
||||||
|
var paddingTopOfAceOuter = editorHeight/2;
|
||||||
|
var chrome$ = helper.padChrome$;
|
||||||
|
var $outerIframe = chrome$('iframe');
|
||||||
|
$outerIframe.css('padding-top', paddingTopOfAceOuter);
|
||||||
|
|
||||||
|
// we set a big value to check if the scroll is made
|
||||||
|
setScrollPercentageWhenFocusLineIsOutOfViewport(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
context('and user places the caret in the last line visible of the pad', function(){
|
||||||
|
var lastLineVisible;
|
||||||
|
beforeEach(function (done) {
|
||||||
|
lastLineVisible = getLastLineVisibleOfViewport();
|
||||||
|
placeCaretInTheBeginningOfLine(lastLineVisible, done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scrolls the line where caret is', function(done){
|
||||||
|
helper.waitFor(function(){
|
||||||
|
var firstLineVisibileOfViewport = getFirstLineVisibileOfViewport();
|
||||||
|
var linesScrolled = firstLineVisibileOfViewport !== 0;
|
||||||
|
return linesScrolled;
|
||||||
|
}).done(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ********************* Helper functions/constants ********************* */
|
||||||
|
var TOP_OF_PAGE = 0;
|
||||||
|
var BOTTOM_OF_PAGE = 5000; // we use a big value to force the page to be scrolled all the way down
|
||||||
|
var LINES_OF_PAD = 100;
|
||||||
|
var ENTER = 13;
|
||||||
|
var BACKSPACE = 8;
|
||||||
|
var LEFT_ARROW = 37;
|
||||||
|
var UP_ARROW = 38;
|
||||||
|
var RIGHT_ARROW = 39;
|
||||||
|
var LINES_ON_VIEWPORT = 10;
|
||||||
|
var WIDTH_OF_EDITOR_RESIZED = 100;
|
||||||
|
var LONG_TEXT_CHARS = 100;
|
||||||
|
|
||||||
|
var cleanPad = function(callback) {
|
||||||
|
var inner$ = helper.padInner$;
|
||||||
|
var $padContent = inner$('#innerdocbody');
|
||||||
|
$padContent.html('');
|
||||||
|
|
||||||
|
// wait for Etherpad to re-create first line
|
||||||
|
helper.waitFor(function(){
|
||||||
|
var lineNumber = inner$('div').length;
|
||||||
|
return lineNumber === 1;
|
||||||
|
}, 2000).done(callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
var createPadWithSeveralLines = function(done) {
|
||||||
|
var line = '<span>a</span><br>';
|
||||||
|
var $firstLine = helper.padInner$('div').first();
|
||||||
|
var lines = line.repeat(LINES_OF_PAD); //arbitrary number, we need to create lines that is over the viewport
|
||||||
|
$firstLine.html(lines);
|
||||||
|
|
||||||
|
helper.waitFor(function(){
|
||||||
|
var linesCreated = helper.padInner$('div').length;
|
||||||
|
return linesCreated === LINES_OF_PAD;
|
||||||
|
}, 4000).done(done);
|
||||||
|
};
|
||||||
|
|
||||||
|
var createPadWithALineHigherThanViewportHeight = function(test, line, done) {
|
||||||
|
var viewportHeight = 160; //10 lines * 16px (height of line)
|
||||||
|
test.timeout(5000);
|
||||||
|
cleanPad(function(){
|
||||||
|
// make the editor smaller to make test easier
|
||||||
|
// with that width the each line has about 5 chars
|
||||||
|
resizeEditorWidth();
|
||||||
|
|
||||||
|
// we create a line with 100 chars, which makes about 20 lines
|
||||||
|
setLongTextOnLine(line);
|
||||||
|
helper.waitFor(function () {
|
||||||
|
var $firstLine = getLine(line);
|
||||||
|
|
||||||
|
var heightOfLine = $firstLine.get(0).getBoundingClientRect().height;
|
||||||
|
return heightOfLine >= viewportHeight;
|
||||||
|
}, 4000).done(done);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var setLongTextOnLine = function(line) {
|
||||||
|
var $line = getLine(line);
|
||||||
|
var longText = 'a'.repeat(LONG_TEXT_CHARS);
|
||||||
|
$line.html(longText);
|
||||||
|
};
|
||||||
|
|
||||||
|
// resize the editor to make the tests easier
|
||||||
|
var resizeEditorHeight = function() {
|
||||||
|
var chrome$ = helper.padChrome$;
|
||||||
|
chrome$('#editorcontainer').css('height', getSizeOfViewport());
|
||||||
|
};
|
||||||
|
|
||||||
|
// this makes about 5 chars per line
|
||||||
|
var resizeEditorWidth = function() {
|
||||||
|
var chrome$ = helper.padChrome$;
|
||||||
|
chrome$('#editorcontainer').css('width', WIDTH_OF_EDITOR_RESIZED);
|
||||||
|
};
|
||||||
|
|
||||||
|
var resetResizeOfEditorHeight = function() {
|
||||||
|
var chrome$ = helper.padChrome$;
|
||||||
|
chrome$('#editorcontainer').css('height', '');
|
||||||
|
};
|
||||||
|
|
||||||
|
var resetEditorWidth = function () {
|
||||||
|
var chrome$ = helper.padChrome$;
|
||||||
|
chrome$('#editorcontainer').css('width', '');
|
||||||
|
};
|
||||||
|
|
||||||
|
var getEditorHeight = function() {
|
||||||
|
var chrome$ = helper.padChrome$;
|
||||||
|
var $editor = chrome$('#editorcontainer');
|
||||||
|
var editorHeight = $editor.get(0).clientHeight;
|
||||||
|
return editorHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
var getSizeOfViewport = function() {
|
||||||
|
return getLinePositionOnViewport(LINES_ON_VIEWPORT) - getLinePositionOnViewport(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
var scrollPageTo = function(value) {
|
||||||
|
var outer$ = helper.padOuter$;
|
||||||
|
var $ace_outer = outer$('#outerdocbody').parent();
|
||||||
|
$ace_outer.parent().scrollTop(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
var scrollEditorToTopOfPad = function() {
|
||||||
|
scrollPageTo(TOP_OF_PAGE);
|
||||||
|
};
|
||||||
|
|
||||||
|
var scrollEditorToBottomOfPad = function() {
|
||||||
|
scrollPageTo(BOTTOM_OF_PAGE);
|
||||||
|
};
|
||||||
|
|
||||||
|
var scrollEditorToLeaveTopAndBottomOfBigLineOutOfViewport = function ($bigLine) {
|
||||||
|
var lineHeight = $bigLine.get(0).getBoundingClientRect().height;
|
||||||
|
var middleOfLine = lineHeight/2;
|
||||||
|
scrollPageTo(middleOfLine);
|
||||||
|
};
|
||||||
|
|
||||||
|
var getLine = function(lineNum) {
|
||||||
|
var inner$ = helper.padInner$;
|
||||||
|
var $line = inner$('div').eq(lineNum);
|
||||||
|
return $line;
|
||||||
|
};
|
||||||
|
|
||||||
|
var placeCaretAtTheEndOfLine = function(lineNum) {
|
||||||
|
var $targetLine = getLine(lineNum);
|
||||||
|
var lineLength = $targetLine.text().length;
|
||||||
|
helper.selectLines($targetLine, $targetLine, lineLength, lineLength);
|
||||||
|
};
|
||||||
|
|
||||||
|
var placeCaretInTheBeginningOfLine = function(lineNum, cb) {
|
||||||
|
var $targetLine = getLine(lineNum);
|
||||||
|
helper.selectLines($targetLine, $targetLine, 0, 0);
|
||||||
|
helper.waitFor(function() {
|
||||||
|
var $lineWhereCaretIs = getLineWhereCaretIs();
|
||||||
|
return $targetLine.get(0) === $lineWhereCaretIs.get(0);
|
||||||
|
}).done(cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
var getLineWhereCaretIs = function() {
|
||||||
|
var inner$ = helper.padInner$;
|
||||||
|
var nodeWhereCaretIs = inner$.document.getSelection().anchorNode;
|
||||||
|
var $lineWhereCaretIs = $(nodeWhereCaretIs).closest('div');
|
||||||
|
return $lineWhereCaretIs;
|
||||||
|
};
|
||||||
|
|
||||||
|
var getFirstLineVisibileOfViewport = function() {
|
||||||
|
return _.find(_.range(0, LINES_OF_PAD - 1), isLineOnViewport);
|
||||||
|
};
|
||||||
|
|
||||||
|
var getLastLineVisibleOfViewport = function() {
|
||||||
|
return _.find(_.range(LINES_OF_PAD - 1, 0, -1), isLineOnViewport);
|
||||||
|
};
|
||||||
|
|
||||||
|
var pressKey = function(keyCode, shiftIsPressed){
|
||||||
|
var inner$ = helper.padInner$;
|
||||||
|
var evtType;
|
||||||
|
if(inner$(window)[0].bowser.firefox || inner$(window)[0].bowser.modernIE){ // if it's a mozilla or IE
|
||||||
|
evtType = 'keypress';
|
||||||
|
}else{
|
||||||
|
evtType = 'keydown';
|
||||||
|
}
|
||||||
|
var e = inner$.Event(evtType);
|
||||||
|
e.shiftKey = shiftIsPressed;
|
||||||
|
e.keyCode = keyCode;
|
||||||
|
e.which = keyCode; // etherpad listens to 'which'
|
||||||
|
inner$('#innerdocbody').trigger(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
var releaseKey = function(keyCode){
|
||||||
|
var inner$ = helper.padInner$;
|
||||||
|
var evtType = 'keyup';
|
||||||
|
var e = inner$.Event(evtType);
|
||||||
|
e.keyCode = keyCode;
|
||||||
|
e.which = keyCode; // etherpad listens to 'which'
|
||||||
|
inner$('#innerdocbody').trigger(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
var pressEnter = function() {
|
||||||
|
pressKey(ENTER);
|
||||||
|
};
|
||||||
|
|
||||||
|
var pressBackspace = function() {
|
||||||
|
pressKey(BACKSPACE);
|
||||||
|
};
|
||||||
|
|
||||||
|
var pressAndReleaseUpArrow = function() {
|
||||||
|
pressKey(UP_ARROW);
|
||||||
|
releaseKey(UP_ARROW);
|
||||||
|
};
|
||||||
|
|
||||||
|
var pressAndReleaseRightArrow = function() {
|
||||||
|
pressKey(RIGHT_ARROW);
|
||||||
|
releaseKey(RIGHT_ARROW);
|
||||||
|
};
|
||||||
|
|
||||||
|
var pressAndReleaseLeftArrow = function(shiftIsPressed) {
|
||||||
|
pressKey(LEFT_ARROW, shiftIsPressed);
|
||||||
|
releaseKey(LEFT_ARROW);
|
||||||
|
};
|
||||||
|
|
||||||
|
var isLineOnViewport = function(lineNumber) {
|
||||||
|
// in the function scrollNodeVerticallyIntoView from ace2_inner.js, iframePadTop is used to calculate
|
||||||
|
// how much scroll is needed. Although the name refers to padding-top, this value is not set on the
|
||||||
|
// padding-top.
|
||||||
|
var iframePadTop = 8;
|
||||||
|
var $line = getLine(lineNumber);
|
||||||
|
var linePosition = $line.get(0).getBoundingClientRect();
|
||||||
|
|
||||||
|
// position relative to the current viewport
|
||||||
|
var linePositionTopOnViewport = linePosition.top - getEditorScroll() + iframePadTop;
|
||||||
|
var linePositionBottomOnViewport = linePosition.bottom - getEditorScroll();
|
||||||
|
|
||||||
|
var lineBellowTop = linePositionBottomOnViewport > 0;
|
||||||
|
var lineAboveBottom = linePositionTopOnViewport < getClientHeightVisible();
|
||||||
|
var isVisible = lineBellowTop && lineAboveBottom;
|
||||||
|
|
||||||
|
return isVisible;
|
||||||
|
};
|
||||||
|
|
||||||
|
var getEditorScroll = function () {
|
||||||
|
var outer$ = helper.padOuter$;
|
||||||
|
var scrollTopFirefox = outer$('#outerdocbody').parent().scrollTop(); // works only on firefox
|
||||||
|
var scrollTop = outer$('#outerdocbody').scrollTop() || scrollTopFirefox;
|
||||||
|
return scrollTop;
|
||||||
|
};
|
||||||
|
|
||||||
|
// clientHeight includes padding, so we have to subtract it and consider only the visible viewport
|
||||||
|
var getClientHeightVisible = function () {
|
||||||
|
var outer$ = helper.padOuter$;
|
||||||
|
var $ace_outer = outer$('#outerdocbody').parent();
|
||||||
|
var ace_outerHeight = $ace_outer.get(0).clientHeight;
|
||||||
|
var ace_outerPaddingTop = getIntValueOfCSSProperty($ace_outer, 'padding-top');
|
||||||
|
var paddingAddedWhenPageViewIsEnable = getPaddingAddedWhenPageViewIsEnable();
|
||||||
|
var clientHeight = ace_outerHeight - ( ace_outerPaddingTop + paddingAddedWhenPageViewIsEnable);
|
||||||
|
|
||||||
|
return clientHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ep_page_view changes the dimensions of the editor. We have to guarantee
|
||||||
|
// the viewport height is calculated right
|
||||||
|
var getPaddingAddedWhenPageViewIsEnable = function () {
|
||||||
|
var chrome$ = helper.padChrome$;
|
||||||
|
var $outerIframe = chrome$('iframe');
|
||||||
|
var paddingAddedWhenPageViewIsEnable = parseInt($outerIframe.css('padding-top'));
|
||||||
|
return paddingAddedWhenPageViewIsEnable;
|
||||||
|
};
|
||||||
|
|
||||||
|
var getIntValueOfCSSProperty = function($element, property){
|
||||||
|
var valueString = $element.css(property);
|
||||||
|
return parseInt(valueString) || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
var forceUseMonospacedFont = function () {
|
||||||
|
helper.padChrome$.window.clientVars.padOptions.useMonospaceFont = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
var setScrollPercentageWhenFocusLineIsOutOfViewport = function(value, editionAboveViewport) {
|
||||||
|
var scrollSettings = helper.padChrome$.window.clientVars.scrollWhenFocusLineIsOutOfViewport;
|
||||||
|
if (editionAboveViewport) {
|
||||||
|
scrollSettings.percentage.editionAboveViewport = value;
|
||||||
|
}else{
|
||||||
|
scrollSettings.percentage.editionBelowViewport = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var resetScrollPercentageWhenFocusLineIsOutOfViewport = function() {
|
||||||
|
var scrollSettings = helper.padChrome$.window.clientVars.scrollWhenFocusLineIsOutOfViewport;
|
||||||
|
scrollSettings.percentage.editionAboveViewport = 0;
|
||||||
|
scrollSettings.percentage.editionBelowViewport = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
var setPercentageToScrollWhenUserPressesArrowUp = function (value) {
|
||||||
|
var scrollSettings = helper.padChrome$.window.clientVars.scrollWhenFocusLineIsOutOfViewport;
|
||||||
|
scrollSettings.percentageToScrollWhenUserPressesArrowUp = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
var scrollWhenPlaceCaretInTheLastLineOfViewport = function() {
|
||||||
|
var scrollSettings = helper.padChrome$.window.clientVars.scrollWhenFocusLineIsOutOfViewport;
|
||||||
|
scrollSettings.scrollWhenCaretIsInTheLastLineOfViewport = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
var getLinePositionOnViewport = function(lineNumber) {
|
||||||
|
var $line = getLine(lineNumber);
|
||||||
|
var linePosition = $line.get(0).getBoundingClientRect();
|
||||||
|
|
||||||
|
// position relative to the current viewport
|
||||||
|
return linePosition.top - getEditorScroll();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
166
tests/frontend/specs/select_formatting_buttons.js
Normal file
166
tests/frontend/specs/select_formatting_buttons.js
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
describe("select formatting buttons when selection has style applied", function(){
|
||||||
|
var STYLES = ['italic', 'bold', 'underline', 'strikethrough'];
|
||||||
|
var SHORTCUT_KEYS = ['I', 'B', 'U', '5']; // italic, bold, underline, strikethrough
|
||||||
|
var FIRST_LINE = 0;
|
||||||
|
|
||||||
|
before(function(cb){
|
||||||
|
helper.newPad(cb);
|
||||||
|
this.timeout(60000);
|
||||||
|
});
|
||||||
|
|
||||||
|
var applyStyleOnLine = function(style, line) {
|
||||||
|
var chrome$ = helper.padChrome$;
|
||||||
|
selectLine(line);
|
||||||
|
var $formattingButton = chrome$('.buttonicon-' + style);
|
||||||
|
$formattingButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
var isButtonSelected = function(style) {
|
||||||
|
var chrome$ = helper.padChrome$;
|
||||||
|
var $formattingButton = chrome$('.buttonicon-' + style);
|
||||||
|
return $formattingButton.parent().hasClass('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectLine = function(lineNumber, offsetStart, offsetEnd) {
|
||||||
|
var inner$ = helper.padInner$;
|
||||||
|
var $line = inner$("div").eq(lineNumber);
|
||||||
|
helper.selectLines($line, $line, offsetStart, offsetEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
var placeCaretOnLine = function(lineNumber) {
|
||||||
|
var inner$ = helper.padInner$;
|
||||||
|
var $line = inner$("div").eq(lineNumber);
|
||||||
|
$line.sendkeys('{leftarrow}');
|
||||||
|
}
|
||||||
|
|
||||||
|
var undo = function() {
|
||||||
|
var $undoButton = helper.padChrome$(".buttonicon-undo");
|
||||||
|
$undoButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
var testIfFormattingButtonIsDeselected = function(style) {
|
||||||
|
it('deselects the ' + style + ' button', function(done) {
|
||||||
|
helper.waitFor(function(){
|
||||||
|
return isButtonSelected(style) === false;
|
||||||
|
}).done(done)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var testIfFormattingButtonIsSelected = function(style) {
|
||||||
|
it('selects the ' + style + ' button', function(done) {
|
||||||
|
helper.waitFor(function(){
|
||||||
|
return isButtonSelected(style);
|
||||||
|
}).done(done)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var applyStyleOnLineAndSelectIt = function(line, style, cb) {
|
||||||
|
applyStyleOnLineOnFullLineAndRemoveSelection(line, style, selectLine, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
var applyStyleOnLineAndPlaceCaretOnit = function(line, style, cb) {
|
||||||
|
applyStyleOnLineOnFullLineAndRemoveSelection(line, style, placeCaretOnLine, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
var applyStyleOnLineOnFullLineAndRemoveSelection = function(line, style, selectTarget, cb) {
|
||||||
|
applyStyleOnLine(style, line);
|
||||||
|
|
||||||
|
// we have to give some time to Etherpad detects the selection changed
|
||||||
|
setTimeout(function() {
|
||||||
|
// remove selection from previous line
|
||||||
|
selectLine(line + 1);
|
||||||
|
setTimeout(function() {
|
||||||
|
// select the text or place the caret on a position that
|
||||||
|
// has the formatting text applied previously
|
||||||
|
selectTarget(line);
|
||||||
|
cb();
|
||||||
|
}, 1000);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
var pressFormattingShortcutOnSelection = function(key) {
|
||||||
|
var inner$ = helper.padInner$;
|
||||||
|
var chrome$ = helper.padChrome$;
|
||||||
|
|
||||||
|
//get the first text element out of the inner iframe
|
||||||
|
var $firstTextElement = inner$("div").first();
|
||||||
|
|
||||||
|
//select this text element
|
||||||
|
$firstTextElement.sendkeys('{selectall}');
|
||||||
|
|
||||||
|
if(inner$(window)[0].bowser.firefox || inner$(window)[0].bowser.modernIE){ // if it's a mozilla or IE
|
||||||
|
var evtType = "keypress";
|
||||||
|
}else{
|
||||||
|
var evtType = "keydown";
|
||||||
|
}
|
||||||
|
|
||||||
|
var e = inner$.Event(evtType);
|
||||||
|
e.ctrlKey = true; // Control key
|
||||||
|
e.which = key.charCodeAt(0); // I, U, B, 5
|
||||||
|
inner$("#innerdocbody").trigger(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
STYLES.forEach(function(style){
|
||||||
|
context('when selection is in a text with ' + style + ' applied', function(){
|
||||||
|
before(function (done) {
|
||||||
|
this.timeout(4000);
|
||||||
|
applyStyleOnLineAndSelectIt(FIRST_LINE, style, done);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function () {
|
||||||
|
undo();
|
||||||
|
});
|
||||||
|
|
||||||
|
testIfFormattingButtonIsSelected(style);
|
||||||
|
});
|
||||||
|
|
||||||
|
context('when caret is in a position with ' + style + ' applied', function(){
|
||||||
|
before(function (done) {
|
||||||
|
this.timeout(4000);
|
||||||
|
applyStyleOnLineAndPlaceCaretOnit(FIRST_LINE, style, done);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function () {
|
||||||
|
undo();
|
||||||
|
});
|
||||||
|
|
||||||
|
testIfFormattingButtonIsSelected(style)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('when user applies a style and the selection does not change', function() {
|
||||||
|
var style = STYLES[0]; // italic
|
||||||
|
before(function () {
|
||||||
|
applyStyleOnLine(style, FIRST_LINE);
|
||||||
|
});
|
||||||
|
|
||||||
|
// clean the style applied
|
||||||
|
after(function () {
|
||||||
|
applyStyleOnLine(style, FIRST_LINE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects the style button', function (done) {
|
||||||
|
expect(isButtonSelected(style)).to.be(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
SHORTCUT_KEYS.forEach(function(key, index){
|
||||||
|
var styleOfTheShortcut = STYLES[index]; // italic, bold, ...
|
||||||
|
context('when user presses CMD + ' + key, function() {
|
||||||
|
before(function () {
|
||||||
|
pressFormattingShortcutOnSelection(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
testIfFormattingButtonIsSelected(styleOfTheShortcut);
|
||||||
|
|
||||||
|
context('and user presses CMD + ' + key + ' again', function() {
|
||||||
|
before(function () {
|
||||||
|
pressFormattingShortcutOnSelection(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
testIfFormattingButtonIsDeselected(styleOfTheShortcut);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue