summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md8
-rw-r--r--bin/doc/package.json2
-rw-r--r--settings.json.template28
-rw-r--r--src/locales/az.json5
-rw-r--r--src/locales/diq.json3
-rw-r--r--src/locales/es.json2
-rw-r--r--src/locales/fi.json5
-rw-r--r--src/locales/fy.json43
-rw-r--r--src/locales/hi.json39
-rw-r--r--src/locales/ja.json5
-rw-r--r--src/locales/krc.json41
-rw-r--r--src/locales/map-bms.json5
-rw-r--r--src/locales/nah.json42
-rw-r--r--src/locales/nb.json2
-rw-r--r--src/locales/pt.json31
-rw-r--r--src/locales/sl.json4
-rw-r--r--src/locales/sr-ec.json47
-rw-r--r--src/locales/tcy.json48
-rw-r--r--src/locales/th.json129
-rw-r--r--src/locales/zh-hant.json2
-rw-r--r--src/node/handler/PadMessageHandler.js9
-rw-r--r--src/node/hooks/express/apicalls.js3
-rw-r--r--src/node/hooks/express/errorhandling.js3
-rw-r--r--src/node/utils/Settings.js27
-rw-r--r--src/package.json7
-rw-r--r--src/static/js/AttributeManager.js14
-rw-r--r--src/static/js/ace2_inner.js267
-rw-r--r--src/static/js/caretPosition.js241
-rw-r--r--src/static/js/pad_utils.js2
-rw-r--r--src/static/js/scroll.js366
-rw-r--r--tests/frontend/specs/scroll.js649
-rw-r--r--tests/frontend/specs/select_formatting_buttons.js166
32 files changed, 2033 insertions, 212 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a96b9b3c..74d06f45 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
* NEW: Added pad shortcut disabling feature
* NEW: Create option to automatically reconnect after a few seconds
diff --git a/bin/doc/package.json b/bin/doc/package.json
index d87c9345..5aba79e0 100644
--- a/bin/doc/package.json
+++ b/bin/doc/package.json
@@ -7,7 +7,7 @@
"node": ">=0.6.10"
},
"dependencies": {
- "marked": "~0.1.9"
+ "marked": ">=0.3.6"
},
"devDependencies": {},
"optionalDependencies": {},
diff --git a/settings.json.template b/settings.json.template
index 0cb10d50..699880bd 100644
--- a/settings.json.template
+++ b/settings.json.template
@@ -150,6 +150,34 @@
/* Time (in seconds) to automatically reconnect pad when a "Force reconnect"
message is shown to user. Set to 0 to disable automatic reconnection */
"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.
If you do not uncomment this, /admin will not be available! */
diff --git a/src/locales/az.json b/src/locales/az.json
index 800c22e5..97270649 100644
--- a/src/locales/az.json
+++ b/src/locales/az.json
@@ -6,7 +6,8 @@
"Mushviq Abdulla",
"Wertuose",
"Mastizada",
- "Archaeodontosaurus"
+ "Archaeodontosaurus",
+ "Neriman2003"
]
},
"index.newPad": "Yeni lövhə",
@@ -61,6 +62,8 @@
"pad.modals.connected": "Bağlandı.",
"pad.modals.reconnecting": "Sizin lövhə yenidən qoşulur..",
"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.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.",
diff --git a/src/locales/diq.json b/src/locales/diq.json
index c57284ac..86f44314 100644
--- a/src/locales/diq.json
+++ b/src/locales/diq.json
@@ -57,10 +57,11 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"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.reconnecting": "Bloknot da şıma rê fına irtibat kewê no",
"pad.modals.forcereconnect": "Mecbur anciya gırê de",
+ "pad.modals.reconnecttimer": "Anciya gırê beno",
"pad.modals.cancel": "Bıtexelne",
"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",
diff --git a/src/locales/es.json b/src/locales/es.json
index cc673e09..d271b61e 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -38,7 +38,7 @@
"pad.colorpicker.save": "Guardar",
"pad.colorpicker.cancel": "Cancelar",
"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.permissionDenied": "No tienes permiso para acceder a este pad",
"pad.wrongPassword": "La contraseña era incorrecta",
diff --git a/src/locales/fi.json b/src/locales/fi.json
index 8f4f583e..e42847ee 100644
--- a/src/locales/fi.json
+++ b/src/locales/fi.json
@@ -13,7 +13,8 @@
"Macofe",
"MrTapsa",
"Silvonen",
- "Espeox"
+ "Espeox",
+ "Pyscowicz"
]
},
"index.newPad": "Uusi muistio",
@@ -68,6 +69,8 @@
"pad.modals.connected": "Yhdistetty.",
"pad.modals.reconnecting": "Muodostetaan yhteyttä muistioon 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.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.",
diff --git a/src/locales/fy.json b/src/locales/fy.json
new file mode 100644
index 00000000..540061d6
--- /dev/null
+++ b/src/locales/fy.json
@@ -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"
+}
diff --git a/src/locales/hi.json b/src/locales/hi.json
new file mode 100644
index 00000000..c78d74d0
--- /dev/null
+++ b/src/locales/hi.json
@@ -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": "कृपया कॉपी पेस्ट करें"
+}
diff --git a/src/locales/ja.json b/src/locales/ja.json
index aeb6ba7d..fe200fc2 100644
--- a/src/locales/ja.json
+++ b/src/locales/ja.json
@@ -3,7 +3,8 @@
"authors": [
"Shirayuki",
"Torinky",
- "Omotecho"
+ "Omotecho",
+ "Aefgh39622"
]
},
"index.newPad": "新規作成",
@@ -54,7 +55,7 @@
"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-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.reconnecting": "パッドに再接続中...",
"pad.modals.forcereconnect": "強制的に再接続",
diff --git a/src/locales/krc.json b/src/locales/krc.json
new file mode 100644
index 00000000..df04e942
--- /dev/null
+++ b/src/locales/krc.json
@@ -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": "Импорт этиу…"
+}
diff --git a/src/locales/map-bms.json b/src/locales/map-bms.json
index 1b87bac8..8ebf70c7 100644
--- a/src/locales/map-bms.json
+++ b/src/locales/map-bms.json
@@ -1,7 +1,8 @@
{
"@metadata": {
"authors": [
- "StefanusRA"
+ "StefanusRA",
+ "Empu"
]
},
"index.newPad": "Pad Anyar",
@@ -30,7 +31,7 @@
"pad.permissionDenied": "Rika ora duwe idin kanggo ngakses pad kiye",
"pad.wrongPassword": "Tembung sandhine Rika salah",
"pad.settings.padSettings": "Pangaturan Pad",
- "pad.settings.myView": "Delengane Inyong",
+ "pad.settings.myView": "Delengané Inyong",
"pad.settings.stickychat": "Dopokan mesti nang layar",
"pad.settings.colorcheck": "Authorship colors",
"pad.settings.linenocheck": "Nomer baris",
diff --git a/src/locales/nah.json b/src/locales/nah.json
new file mode 100644
index 00000000..262766b3
--- /dev/null
+++ b/src/locales/nah.json
@@ -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"
+}
diff --git a/src/locales/nb.json b/src/locales/nb.json
index 06293aa8..bd39b18a 100644
--- a/src/locales/nb.json
+++ b/src/locales/nb.json
@@ -56,7 +56,7 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"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.reconnecting": "Kobler til din blokk på nytt...",
"pad.modals.forcereconnect": "Tving gjenoppkobling",
diff --git a/src/locales/pt.json b/src/locales/pt.json
index b5cbe1d8..a65730a0 100644
--- a/src/locales/pt.json
+++ b/src/locales/pt.json
@@ -8,7 +8,8 @@
"Imperadeiro98",
"Macofe",
"Ti4goc",
- "Cainamarques"
+ "Cainamarques",
+ "Athena in Wonderland"
]
},
"index.newPad": "Nova Nota",
@@ -33,12 +34,14 @@
"pad.colorpicker.save": "Gravar",
"pad.colorpicker.cancel": "Cancelar",
"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.permissionDenied": "Não tem permissão para aceder a este pad.",
"pad.wrongPassword": "A palavra-chave está errada",
"pad.settings.padSettings": "Configurações da Nota",
"pad.settings.myView": "Minha vista",
"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.linenocheck": "Números de linha",
"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.importSuccessful": "Bem sucedido!",
"pad.importExport.export": "Exportar a Nota atual como:",
+ "pad.importExport.exportetherpad": "Etherpad",
"pad.importExport.exporthtml": "HTML",
"pad.importExport.exportplain": "Texto simples",
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"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.reconnecting": "Reconectando-se ao seu bloco…",
"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.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.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.cause": "Talvez tenha ligado por um firewall ou proxy incompatí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.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.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.explanation": "Este pad foi removido.",
"pad.modals.disconnected": "Você foi desconectado.",
@@ -74,9 +90,11 @@
"pad.share": "Compartilhar este pad",
"pad.share.readonly": "Somente para leitura",
"pad.share.link": "Ligação",
+ "pad.share.emebdcode": "Incorporar o URL",
"pad.chat": "Bate-papo",
"pad.chat.title": "Abrir o bate-papo para este pad.",
"pad.chat.loadmessages": "Carregar mais mensagens",
+ "timeslider.pageTitle": "Linha do tempo de {{appTitle}}",
"timeslider.toolbar.returnbutton": "Voltar ao pad",
"timeslider.toolbar.authors": "Autores:",
"timeslider.toolbar.authorsList": "Sem Autores",
@@ -84,6 +102,9 @@
"timeslider.exportCurrent": "Exportar versão atual como:",
"timeslider.version": "Versão {{version}}",
"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.month.january": "Janeiro",
"timeslider.month.february": "Fevereiro",
@@ -97,7 +118,9 @@
"timeslider.month.october": "Outubro",
"timeslider.month.november": "Novembro",
"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.timeslider": "Pode consultar as revisões gravadas visitando a linha do tempo",
"pad.userlist.entername": "Insira o seu nome",
"pad.userlist.unnamed": "sem nome",
"pad.userlist.guest": "Convidado",
@@ -107,8 +130,10 @@
"pad.impexp.importbutton": "Importar agora",
"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.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.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."
}
diff --git a/src/locales/sl.json b/src/locales/sl.json
index 50333132..94ccfa73 100644
--- a/src/locales/sl.json
+++ b/src/locales/sl.json
@@ -8,7 +8,7 @@
]
},
"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.italic.title": "Ležeče (Ctrl-I)",
"pad.toolbar.underline.title": "Podčrtano (Ctrl-U)",
@@ -20,7 +20,7 @@
"pad.toolbar.undo.title": "Razveljavi (Ctrl-Z)",
"pad.toolbar.redo.title": "Ponovno uveljavi (Ctrl-Y)",
"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.savedRevision.title": "Shrani redakcijo",
"pad.toolbar.settings.title": "Nastavitve",
diff --git a/src/locales/sr-ec.json b/src/locales/sr-ec.json
index de413722..59f77ef2 100644
--- a/src/locales/sr-ec.json
+++ b/src/locales/sr-ec.json
@@ -5,21 +5,22 @@
"Milicevic01",
"Милан Јелисавчић",
"Srdjan m",
- "Obsuser"
+ "Obsuser",
+ "Acamicamacaraca"
]
},
"index.newPad": "Нови Пад",
"index.createOpenPad": "или направите/отворите пад следећег назива:",
- "pad.toolbar.bold.title": "Подебљано (Ctrl-B)",
- "pad.toolbar.italic.title": "Искошено (Ctrl-I)",
- "pad.toolbar.underline.title": "Подвучено (Ctrl-U)",
+ "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+Z)",
+ "pad.toolbar.redo.title": "Понови (Ctrl+Z)",
"pad.toolbar.clearAuthorship.title": "Очисти ауторске боје (Ctrl+Shift+C)",
"pad.toolbar.import_export.title": "Увези/извези из/на друге датотечне формате",
"pad.toolbar.timeslider.title": "Временска линија",
@@ -29,9 +30,9 @@
"pad.toolbar.showusers.title": "Прикажи кориснике на овом паду",
"pad.colorpicker.save": "Сачувај",
"pad.colorpicker.cancel": "Откажи",
- "pad.loading": "Учитавање...",
+ "pad.loading": "Учитавам…",
"pad.noCookie": "Колачић није пронађен. Молимо да укључите колачиће у вашем прегледавачу!",
- "pad.passwordRequired": "Требате лозинку како бисте приступили овом паду",
+ "pad.passwordRequired": "Требате имати лозинку како бисте приступили овом паду",
"pad.permissionDenied": "Немате дозволу да приступите овом паду",
"pad.wrongPassword": "Ваша лозинка није исправна",
"pad.settings.padSettings": "Подешавања пада",
@@ -48,15 +49,15 @@
"pad.settings.language": "Језик:",
"pad.importExport.import_export": "Увоз/извоз",
"pad.importExport.import": "Отпремите било коју текстуалну датотеку или документ",
- "pad.importExport.importSuccessful": "Успело!",
+ "pad.importExport.importSuccessful": "Успешно!",
"pad.importExport.export": "Извези тренутни пад као:",
"pad.importExport.exportetherpad": "Etherpad",
"pad.importExport.exporthtml": "HTML",
- "pad.importExport.exportplain": "чист текст",
+ "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-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.reconnecting": "Поново се повезујем на ваш пад..",
"pad.modals.forcereconnect": "Присилно се поново повежи",
@@ -83,24 +84,24 @@
"pad.modals.disconnected": "Веза је прекинута.",
"pad.modals.disconnected.explanation": "Изгубљена је веза са сервером",
"pad.modals.disconnected.cause": "Сервер није доступан. Обавестите сервисног администратора ако се ово настави дешавати.",
- "pad.share": "Дели овај пад",
+ "pad.share": "Пофели овај пад",
"pad.share.readonly": "Само за читање",
"pad.share.link": "Веза",
"pad.share.emebdcode": "Угради везу",
"pad.chat": "Ћаскање",
"pad.chat.title": "Отворите ћаскање за овај пад.",
- "pad.chat.loadmessages": "Учитајте више порука.",
+ "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.version": "Издање {{version}}",
"timeslider.saved": "Сачувано на {{day}}. {{month}}. {{year}}",
"timeslider.playPause": "Пусти/паузирај садржај пада",
"timeslider.backRevision": "Иди на претходну верзију овог пада",
- "timeslider.forwardRevision": "Иди на следећу верзију овог пада",
+ "timeslider.forwardRevision": "Иди на следеће издање пада",
"timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
"timeslider.month.january": "јануар",
"timeslider.month.february": "фебруар",
@@ -115,21 +116,21 @@
"timeslider.month.november": "новембар",
"timeslider.month.december": "децембар",
"timeslider.unnamedauthors": "{{num}} неименован(и) {[plural(num) one: аутор, other: аутори ]}",
- "pad.savedrevs.marked": "Ова верзија је сада означена као сачувана",
+ "pad.savedrevs.marked": "Ова измена је сада означена као сачувана",
"pad.savedrevs.timeslider": "Можете видети сачуване измене користећи се временском линијом",
"pad.userlist.entername": "Упишите своје име",
- "pad.userlist.unnamed": "нема имена",
+ "pad.userlist.unnamed": "неименован",
"pad.userlist.guest": "Гост",
"pad.userlist.deny": "Одбиј",
- "pad.userlist.approve": "одобрено",
+ "pad.userlist.approve": "Одобри",
"pad.editbar.clearcolors": "Очисти ауторске боје за цели документ?",
"pad.impexp.importbutton": "Увези одмах",
- "pad.impexp.importing": "Увожење...",
+ "pad.impexp.importing": "Увозим...",
"pad.impexp.confirmimport": "Увоз датотеке ће преписати тренутни текст пада. Да ли сте сигурни да желите наставити?",
- "pad.impexp.convertFailed": "Не можемо увести ову датотеку. Молимо да користите други формат документа или да документ копирате ручно",
- "pad.impexp.padHasData": "Не можемо да увеземо ову датотеку зато што је већ било промена на овом паду, молимо да увезете нови пад",
- "pad.impexp.uploadFailed": "Отпремање није успело, молимо да покушате поново",
- "pad.impexp.importfailed": "Увоз неуспешан",
- "pad.impexp.copypaste": "Молимо да ручно копирате",
+ "pad.impexp.convertFailed": "Не могу да увезем ову датотеку. Молимо да користите други формат документа или да документ копирате ручно",
+ "pad.impexp.padHasData": "Не могу да увезем ову датотеку зато што је већ било промена на овом паду, молимо да увезете нови пад",
+ "pad.impexp.uploadFailed": "Нисам успео да отпремим, молимо покушате поново",
+ "pad.impexp.importfailed": "Нисам успео да увезем",
+ "pad.impexp.copypaste": "Копирајте и залепите",
"pad.impexp.exportdisabled": "Извоз у формату {{type}} није дозвољен. Контактирајте системског администратора за детаље."
}
diff --git a/src/locales/tcy.json b/src/locales/tcy.json
new file mode 100644
index 00000000..3cd815b6
--- /dev/null
+++ b/src/locales/tcy.json
@@ -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": "ಒಪ್ಪಂದ ಅಂಡ್"
+}
diff --git a/src/locales/th.json b/src/locales/th.json
new file mode 100644
index 00000000..d5c24374
--- /dev/null
+++ b/src/locales/th.json
@@ -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}} ถูกปิดใช้งาน โปรดติดต่อผู้ดูแลระบบของคุณสำหรับรายละเอียดเพิ่มเติม"
+}
diff --git a/src/locales/zh-hant.json b/src/locales/zh-hant.json
index e9cac9d4..17591884 100644
--- a/src/locales/zh-hant.json
+++ b/src/locales/zh-hant.json
@@ -59,7 +59,7 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"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.reconnecting": "重新連接到您的記事本...",
"pad.modals.forcereconnect": "強制重新連線",
diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js
index b7ec7cb2..060bca7b 100644
--- a/src/node/handler/PadMessageHandler.js
+++ b/src/node/handler/PadMessageHandler.js
@@ -1216,6 +1216,15 @@ function handleClientReady(client, message)
"parts": plugins.parts,
},
"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
}
diff --git a/src/node/hooks/express/apicalls.js b/src/node/hooks/express/apicalls.js
index db0fc81f..4482fd84 100644
--- a/src/node/hooks/express/apicalls.js
+++ b/src/node/hooks/express/apicalls.js
@@ -3,6 +3,7 @@ var apiLogger = log4js.getLogger("API");
var clientLogger = log4js.getLogger("client");
var formidable = require('formidable');
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
var apiCaller = function(req, res, fields) {
@@ -18,7 +19,7 @@ var apiCaller = function(req, res, fields) {
apiLogger.info("RESPONSE, " + req.params.func + ", " + response);
//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 + ")";
res._____send(response);
diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.js
index 7afe80ae..c36595bd 100644
--- a/src/node/hooks/express/errorhandling.js
+++ b/src/node/hooks/express/errorhandling.js
@@ -49,5 +49,8 @@ exports.expressCreateServer = function (hook_name, args, cb) {
//sigint is so far not working on windows
//https://github.com/joyent/node/issues/1553
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);
}
}
diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js
index 660b7afb..cf7fea80 100644
--- a/src/node/utils/Settings.js
+++ b/src/node/utils/Settings.js
@@ -247,6 +247,33 @@ exports.users = {};
*/
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
exports.abiwordAvailable = function()
{
diff --git a/src/package.json b/src/package.json
index a182fd97..a29c06e5 100644
--- a/src/package.json
+++ b/src/package.json
@@ -29,7 +29,7 @@
"cheerio" : "0.20.0",
"async-stacktrace" : "0.0.2",
"npm" : "4.0.2",
- "ejs" : "2.4.1",
+ "ejs" : "2.5.7",
"graceful-fs" : "4.1.3",
"slide" : "1.1.6",
"semver" : "5.1.0",
@@ -43,7 +43,8 @@
"jsonminify" : "0.4.1",
"measured" : "1.1.0",
"mocha" : "2.4.5",
- "supertest" : "1.2.0"
+ "supertest" : "1.2.0",
+ "is-var-name" : "1.0.0"
},
"bin": { "etherpad-lite": "./node/server.js" },
"devDependencies": {
@@ -55,6 +56,6 @@
"repository" : { "type" : "git",
"url" : "http://github.com/ether/etherpad-lite.git"
},
- "version" : "1.6.2",
+ "version" : "1.6.3",
"license" : "Apache-2.0"
}
diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.js
index 0342408c..53b233e0 100644
--- a/src/static/js/AttributeManager.js
+++ b/src/static/js/AttributeManager.js
@@ -400,7 +400,19 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
this.removeAttributeOnLine(lineNum, attributeName) :
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;
diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js
index 424bacf5..df9c9642 100644
--- a/src/static/js/ace2_inner.js
+++ b/src/static/js/ace2_inner.js
@@ -20,7 +20,6 @@
* limitations under the License.
*/
var _, $, jQuery, plugins, Ace2Common;
-
var browser = require('./browser');
if(browser.msie){
// Honestly fuck IE royally.
@@ -61,6 +60,7 @@ function Ace2Inner(){
var SkipList = require('./skiplist');
var undoModule = require('./undomodule').undoModule;
var AttributeManager = require('./AttributeManager');
+ var Scroll = require('./scroll');
var DEBUG = false; //$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;"
// changed to false
@@ -75,6 +75,9 @@ function Ace2Inner(){
var EDIT_BODY_PADDING_TOP = 8;
var EDIT_BODY_PADDING_LEFT = 8;
+ var FORMATTING_STYLES = ['bold', 'italic', 'underline', 'strikethrough'];
+ var SELECT_BUTTON_CLASS = 'selected';
+
var caughtErrors = [];
var thisAuthor = '';
@@ -82,6 +85,7 @@ function Ace2Inner(){
var disposed = false;
var editorInfo = parent.editorInfo;
+
var iframe = window.frameElement;
var outerWin = iframe.ace_outerWin;
iframe.ace_outerWin = null; // prevent IE 6 memory leak
@@ -89,6 +93,8 @@ function Ace2Inner(){
var lineMetricsDiv = sideDiv.nextSibling;
initLineNumbers();
+ var scroll = Scroll.init(outerWin);
+
var outsideKeyDown = noop;
var outsideKeyPress = function(){return true;};
@@ -424,7 +430,7 @@ function Ace2Inner(){
var undoWorked = false;
try
{
- if (evt.eventType == "setup" || evt.eventType == "importText" || evt.eventType == "setBaseText")
+ if (isPadLoading(evt.eventType))
{
undoModule.clearHistory();
}
@@ -1208,7 +1214,7 @@ function Ace2Inner(){
updateLineNumbers(); // update line numbers if any time left
if (isTimeUp()) return;
- var visibleRange = getVisibleCharRange();
+ var visibleRange = scroll.getVisibleCharRange(rep);
var docRange = [0, rep.lines.totalWidth()];
//console.log("%o %o", docRange, visibleRange);
finishedImportantWork = true;
@@ -1670,7 +1676,7 @@ function Ace2Inner(){
});
//p.mark("relex");
- //rep.lexer.lexCharRange(getVisibleCharRange(), function() { return false; });
+ //rep.lexer.lexCharRange(scroll.getVisibleCharRange(rep), function() { return false; });
//var isTimeUp = newTimeLimit(100);
// do DOM inserts
p.mark("insert");
@@ -2469,17 +2475,11 @@ function Ace2Inner(){
}
}
- if (selectionAllHasIt)
- {
- documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [
- [attributeName, '']
- ]);
- }
- else
- {
- documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [
- [attributeName, 'true']
- ]);
+
+ var attributeValue = selectionAllHasIt ? '' : 'true';
+ documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [[attributeName, attributeValue]]);
+ if (attribIsFormattingStyle(attributeName)) {
+ updateStyleButtonState(attributeName, !selectionAllHasIt); // italic, bold, ...
}
}
editorInfo.ace_toggleAttributeOnSelection = toggleAttributeOnSelection;
@@ -2908,12 +2908,24 @@ function Ace2Inner(){
rep.selFocusAtStart = newSelFocusAtStart;
currentCallStack.repChanged = true;
+ // select the formatting buttons when there is the style applied on selection
+ selectFormattingButtonIfLineHasStyleApplied(rep);
+
hooks.callAll('aceSelectionChanged', {
rep: rep,
callstack: currentCallStack,
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;
//console.log("selStart: %o, selEnd: %o, focusAtStart: %s", rep.selStart, rep.selEnd,
//String(!!rep.selFocusAtStart));
@@ -2922,6 +2934,27 @@ function Ace2Inner(){
//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)
{
if (browser.msie && (!nonEmpty))
@@ -3277,50 +3310,36 @@ function Ace2Inner(){
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()
{
- var theTop = getScrollY();
+ var theTop = scroll.getScrollY();
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 {
top: theTop,
- bottom: (theTop + height)
+ bottom: (theTop + height - viewportExtraSpacesAndPosition)
};
}
- function getVisibleLineRange()
+
+ function getEditorPositionTop()
{
- var viewport = getViewPortTopBottom();
- //console.log("viewport top/bottom: %o", viewport);
- var obj = {};
- 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];
+ var editor = parent.document.getElementsByTagName('iframe');
+ var editorPositionTop = editor[0].offsetTop;
+ return editorPositionTop;
}
- function getVisibleCharRange()
+ // ep_page_view adds padding-top, which makes the viewport smaller
+ function getPaddingTopAddedWhenPageViewIsEnable()
{
- var lineRange = getVisibleLineRange();
- return [rep.lines.offsetOfIndex(lineRange[0]), rep.lines.offsetOfIndex(lineRange[1])];
+ var rootDocument = parent.parent.document;
+ var aceOuter = rootDocument.getElementsByName("ace_outer");
+ var aceOuterPaddingTop = parseInt($(aceOuter).css("padding-top"));
+ return aceOuterPaddingTop;
}
function handleCut(evt)
@@ -3966,12 +3985,12 @@ function Ace2Inner(){
doDeleteKey();
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){
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];
if(topOffset < 0 ){
topOffset = 0;
@@ -3981,7 +4000,7 @@ function Ace2Inner(){
var isPageUp = evt.which === 33;
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 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)
// so use focusNode.offsetTop value.
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);
}
- /* 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
- // top.console.log(caretOffsetTop, viewport.top, caretOffsetTopBottom, viewport.bottom);
- var caretIsNotVisible = (caretOffsetTop < viewport.top || caretOffsetTopBottom >= viewport.bottom); // Is the Caret Visible to the user?
- // Expect some weird behavior caretOffsetTopBottom is greater than viewport.bottom on a keypress down
- 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(offsetTopSamePlace && (evt.which == 37 || evt.which == 38)){
- var newY = caretOffsetTop;
- setScrollY(newY);
- }
- if(caretIsNotVisible){ // is the cursor no longer visible to the user?
- // top.console.log("Caret is NOT visible to the user");
- // top.console.log(caretOffsetTop,viewport.top,caretOffsetTopBottom,viewport.bottom);
- // Oh boy the caret is out of the visible area, I need to scroll the browser window to lineNum.
- 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
- }
+ // scroll to viewport when user presses arrow keys and caret is out of the viewport
+ if((evt.which == 37 || evt.which == 38 || evt.which == 39 || evt.which == 40)){
+ // we use arrowKeyWasReleased to avoid triggering the animation when a key is continuously pressed
+ // this makes the scroll smooth
+ if(!continuouslyPressingArrowKey(type)){
+ // We use getSelection() instead of rep to get the caret position. This avoids errors like when
+ // the caret position is not synchronized with the rep. For example, when an user presses arrow
+ // 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(selection){
+ var arrowUp = evt.which === 38;
+ var innerHeight = getInnerHeight();
+ scroll.scrollWhenPressArrowKeys(arrowUp, rep, innerHeight);
}
}
}
@@ -4121,6 +4110,19 @@ function Ace2Inner(){
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)
{
// precond: normalized DOM
@@ -4837,9 +4839,6 @@ function Ace2Inner(){
setIfNecessary(root.style, "height", "");
}
}
- // if near edge, scroll to edge
- var scrollX = getScrollX();
- var scrollY = getScrollY();
var win = outerWin;
var r = 20;
@@ -4848,52 +4847,6 @@ function Ace2Inner(){
$(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 = [];
function teardown()
@@ -5214,26 +5167,6 @@ function Ace2Inner(){
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)
{
var win = outerWin;
@@ -5255,8 +5188,8 @@ function Ace2Inner(){
{
if (!rep.selStart) return;
fixView();
- var focusLine = (rep.selFocusAtStart ? rep.selStart[0] : rep.selEnd[0]);
- scrollNodeVerticallyIntoView(rep.lines.atIndex(focusLine).lineNode);
+ var innerHeight = getInnerHeight();
+ scroll.scrollNodeVerticallyIntoView(rep, innerHeight);
if (!doesWrap)
{
var browserSelection = getSelection();
diff --git a/src/static/js/caretPosition.js b/src/static/js/caretPosition.js
new file mode 100644
index 00000000..bc3fd007
--- /dev/null
+++ b/src/static/js/caretPosition.js
@@ -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;
+ }
+}
diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js
index b83f21cf..9c1277a0 100644
--- a/src/static/js/pad_utils.js
+++ b/src/static/js/pad_utils.js
@@ -524,7 +524,7 @@ function setupGlobalExceptionHandler() {
$("#editorloadingbox").css("padding", "10px");
$("#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'>'"
- + "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
diff --git a/src/static/js/scroll.js b/src/static/js/scroll.js
new file mode 100644
index 00000000..a53dc38c
--- /dev/null
+++ b/src/static/js/scroll.js
@@ -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);
+}
diff --git a/tests/frontend/specs/scroll.js b/tests/frontend/specs/scroll.js
new file mode 100644
index 00000000..096b06b6
--- /dev/null
+++ b/tests/frontend/specs/scroll.js
@@ -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();
+ };
+});
+
diff --git a/tests/frontend/specs/select_formatting_buttons.js b/tests/frontend/specs/select_formatting_buttons.js
new file mode 100644
index 00000000..5fb97600
--- /dev/null
+++ b/tests/frontend/specs/select_formatting_buttons.js
@@ -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);
+ });
+ });
+ });
+});