From 2677db2ce3add99360ab4d0f15b6d1965b09a034 Mon Sep 17 00:00:00 2001 From: Trevor Parscal Date: Mon, 14 Jul 2014 09:49:52 -0700 Subject: [PATCH] Update OOjs UI to v0.1.0-pre (d2451ac748) New changes: 0e94342 GridLayout: Hide panels with zero width or zero height ef77c68 Localisation updates from https://translatewiki.net. d3f26e6 cleanup: Use local var instead of bind() for inline functions 2c6958f Mobile-friendly styling for demos a785a70 Add OO.ui.Error b490fb6 [BREAKING CHANGE] Change how delay works for OO.ui.Process c34ab93 [BREAKING CHANGE] Split part of OptionWidget into DecoratedOptionWidget 17f297e Add flag event to OO.ui.FlaggableElement c4d3694 Add AccessKey API to OO.ui.ButtonedElement f1fd828 Add API for tabIndex to OO.ui.ButtonedElement bd3a0cf Add support for using arguments with OO.ui.deferMsg 38b8001 Add OO.ui.FormLayout ac01705 [BREAKING CHANGE] Separate setup from setOutlineItem in OO.ui.PageLayout 1d46771 Make OO.ui.FieldLayout labels display inline-block when aligned top a7e3798 Change color of text on frameless buttons 49f3438 Add href and target API to OO.ui.ButtonWidget 667d951 OptionWidget: Fix double icons/indicators a8e7ede Add blur method to OO.ui.InputWidget 3ba36b9 [BREAKING CHANGE] The great and terrible dialog refactor 43f1541 Localisation updates from https://translatewiki.net. f245c8d Localisation updates from https://translatewiki.net. aa8a74b demos: Omit value for disabled attribute, use attr() instead of prop() 12d43c1 OptionWidget: Simplify code by using toggleClass instead of if/else 9690115 SelectWidget: Minor coding style clean up 912cbb7 Split dialog demos by theme d2451ac ProcessDialog: "Other" action buttons should be framed. Change-Id: I0f5cd74a5299dd97addc15737faceca36caf87b4 --- resources/lib/oojs-ui/i18n/ca.json | 7 +- resources/lib/oojs-ui/i18n/de.json | 10 +- resources/lib/oojs-ui/i18n/en.json | 10 +- resources/lib/oojs-ui/i18n/es.json | 8 +- resources/lib/oojs-ui/i18n/fa.json | 10 +- resources/lib/oojs-ui/i18n/fi.json | 10 +- resources/lib/oojs-ui/i18n/fr.json | 13 +- resources/lib/oojs-ui/i18n/gd.json | 13 + resources/lib/oojs-ui/i18n/gl.json | 10 +- resources/lib/oojs-ui/i18n/he.json | 10 +- resources/lib/oojs-ui/i18n/hu.json | 9 +- resources/lib/oojs-ui/i18n/ia.json | 10 +- resources/lib/oojs-ui/i18n/lb.json | 10 +- resources/lib/oojs-ui/i18n/mk.json | 10 +- resources/lib/oojs-ui/i18n/pl.json | 9 +- resources/lib/oojs-ui/i18n/pt.json | 10 +- resources/lib/oojs-ui/i18n/qqq.json | 13 +- resources/lib/oojs-ui/i18n/ro.json | 10 +- resources/lib/oojs-ui/i18n/ru.json | 10 +- resources/lib/oojs-ui/i18n/sq.json | 12 +- resources/lib/oojs-ui/i18n/uk.json | 6 +- resources/lib/oojs-ui/i18n/yi.json | 9 +- resources/lib/oojs-ui/i18n/zh-hans.json | 10 +- .../oojs-ui/images/{tail.svg => anchor.svg} | 2 +- resources/lib/oojs-ui/oojs-ui-agora.css | 4 +- resources/lib/oojs-ui/oojs-ui-apex.css | 607 +- resources/lib/oojs-ui/oojs-ui.js | 11203 +++++++++------- resources/lib/oojs-ui/oojs-ui.svg.css | 153 +- 28 files changed, 7158 insertions(+), 5040 deletions(-) create mode 100644 resources/lib/oojs-ui/i18n/gd.json rename resources/lib/oojs-ui/images/{tail.svg => anchor.svg} (97%) diff --git a/resources/lib/oojs-ui/i18n/ca.json b/resources/lib/oojs-ui/i18n/ca.json index 3ff9763a2e..4555c112eb 100644 --- a/resources/lib/oojs-ui/i18n/ca.json +++ b/resources/lib/oojs-ui/i18n/ca.json @@ -7,11 +7,12 @@ "Pginer", "QuimGil", "SMP", - "Vriullop" + "Vriullop", + "Toniher" ] }, - "ooui-dialog-action-close": "Tanca", "ooui-outline-control-move-down": "Baixa element", "ooui-outline-control-move-up": "Puja element", - "ooui-toolbar-more": "Més" + "ooui-toolbar-more": "Més", + "ooui-dialog-process-dismiss": "Descarta" } diff --git a/resources/lib/oojs-ui/i18n/de.json b/resources/lib/oojs-ui/i18n/de.json index 97ed48c779..546689b9e0 100644 --- a/resources/lib/oojs-ui/i18n/de.json +++ b/resources/lib/oojs-ui/i18n/de.json @@ -13,13 +13,13 @@ "Tomabrafix" ] }, - "ooui-dialog-action-close": "Schließen", "ooui-outline-control-move-down": "Element nach unten verschieben", "ooui-outline-control-move-up": "Element nach oben verschieben", "ooui-outline-control-remove": "Element entfernen", "ooui-toolbar-more": "Mehr", - "ooui-dialog-confirm-title": "Bestätigen", - "ooui-dialog-confirm-default-prompt": "Bist du sicher?", - "ooui-dialog-confirm-default-ok": "Okay", - "ooui-dialog-confirm-default-cancel": "Abbrechen" + "ooui-dialog-message-accept": "Okay", + "ooui-dialog-message-reject": "Abbrechen", + "ooui-dialog-process-error": "Etwas ist schief gelaufen", + "ooui-dialog-process-dismiss": "Ausblenden", + "ooui-dialog-process-retry": "Erneut versuchen" } diff --git a/resources/lib/oojs-ui/i18n/en.json b/resources/lib/oojs-ui/i18n/en.json index 2498a76be3..602efc89f4 100644 --- a/resources/lib/oojs-ui/i18n/en.json +++ b/resources/lib/oojs-ui/i18n/en.json @@ -16,13 +16,13 @@ "Amir E. Aharoni" ] }, - "ooui-dialog-action-close": "Close", "ooui-outline-control-move-down": "Move item down", "ooui-outline-control-move-up": "Move item up", "ooui-outline-control-remove": "Remove item", "ooui-toolbar-more": "More", - "ooui-dialog-confirm-title": "Confirm", - "ooui-dialog-confirm-default-prompt": "Are you sure?", - "ooui-dialog-confirm-default-ok": "OK", - "ooui-dialog-confirm-default-cancel": "Cancel" + "ooui-dialog-message-accept": "OK", + "ooui-dialog-message-reject": "Cancel", + "ooui-dialog-process-error": "Something went wrong", + "ooui-dialog-process-dismiss": "Dismiss", + "ooui-dialog-process-retry": "Try again" } diff --git a/resources/lib/oojs-ui/i18n/es.json b/resources/lib/oojs-ui/i18n/es.json index 76485eab76..805897deca 100644 --- a/resources/lib/oojs-ui/i18n/es.json +++ b/resources/lib/oojs-ui/i18n/es.json @@ -17,13 +17,11 @@ "Gloria sah" ] }, - "ooui-dialog-action-close": "Cerrar", "ooui-outline-control-move-down": "Bajar elemento", "ooui-outline-control-move-up": "Subir elemento", "ooui-outline-control-remove": "Eliminar elemento", "ooui-toolbar-more": "Más", - "ooui-dialog-confirm-title": "Confirmar", - "ooui-dialog-confirm-default-prompt": "¿Está seguro?", - "ooui-dialog-confirm-default-ok": "Aceptar", - "ooui-dialog-confirm-default-cancel": "Cancelar" + "ooui-dialog-message-accept": "Aceptar", + "ooui-dialog-message-reject": "Cancelar", + "ooui-dialog-process-retry": "Intentar de nuevo" } diff --git a/resources/lib/oojs-ui/i18n/fa.json b/resources/lib/oojs-ui/i18n/fa.json index ec051acbdc..b0ec80306f 100644 --- a/resources/lib/oojs-ui/i18n/fa.json +++ b/resources/lib/oojs-ui/i18n/fa.json @@ -13,13 +13,13 @@ "Armin1392" ] }, - "ooui-dialog-action-close": "بستن", "ooui-outline-control-move-down": "انتقال مورد به پایین", "ooui-outline-control-move-up": "انتقال مورد به بالا", "ooui-outline-control-remove": "حذف مورد", "ooui-toolbar-more": "بیشتر", - "ooui-dialog-confirm-title": "تأیید", - "ooui-dialog-confirm-default-prompt": "آیا مطمئن هستید؟", - "ooui-dialog-confirm-default-ok": "تأیید", - "ooui-dialog-confirm-default-cancel": "لغو" + "ooui-dialog-message-accept": "تأیید", + "ooui-dialog-message-reject": "لغو", + "ooui-dialog-process-error": "مشکلی وجود دارد", + "ooui-dialog-process-dismiss": "نپذیرفتن", + "ooui-dialog-process-retry": "دوباره امتحان کن" } diff --git a/resources/lib/oojs-ui/i18n/fi.json b/resources/lib/oojs-ui/i18n/fi.json index 8e8b81eb0f..efaabed562 100644 --- a/resources/lib/oojs-ui/i18n/fi.json +++ b/resources/lib/oojs-ui/i18n/fi.json @@ -16,13 +16,13 @@ "VezonThunder" ] }, - "ooui-dialog-action-close": "Sulje", "ooui-outline-control-move-down": "Siirrä kohdetta alaspäin", "ooui-outline-control-move-up": "Siirrä kohdetta ylöspäin", "ooui-outline-control-remove": "Poista kohde", "ooui-toolbar-more": "Lisää", - "ooui-dialog-confirm-title": "Vahvista", - "ooui-dialog-confirm-default-prompt": "Oletko varma?", - "ooui-dialog-confirm-default-ok": "OK", - "ooui-dialog-confirm-default-cancel": "Peruuta" + "ooui-dialog-message-accept": "OK", + "ooui-dialog-message-reject": "Peruuta", + "ooui-dialog-process-error": "Jokin meni pieleen", + "ooui-dialog-process-dismiss": "Hylkää", + "ooui-dialog-process-retry": "Yritä uudelleen" } diff --git a/resources/lib/oojs-ui/i18n/fr.json b/resources/lib/oojs-ui/i18n/fr.json index 6b8871a5fd..8ff54750f4 100644 --- a/resources/lib/oojs-ui/i18n/fr.json +++ b/resources/lib/oojs-ui/i18n/fr.json @@ -25,16 +25,17 @@ "Trizek", "Urhixidur", "Verdy p", - "Wyz" + "Wyz", + "SnowedEarth" ] }, - "ooui-dialog-action-close": "Fermer", "ooui-outline-control-move-down": "Faire descendre l’élément", "ooui-outline-control-move-up": "Faire monter l’élément", "ooui-outline-control-remove": "Supprimer l’élément", "ooui-toolbar-more": "Plus", - "ooui-dialog-confirm-title": "Confirmer", - "ooui-dialog-confirm-default-prompt": "Êtes-vous sûr ?", - "ooui-dialog-confirm-default-ok": "OK", - "ooui-dialog-confirm-default-cancel": "Annuler" + "ooui-dialog-message-accept": "OK", + "ooui-dialog-message-reject": "Annuler", + "ooui-dialog-process-error": "Quelque chose a mal tourné", + "ooui-dialog-process-dismiss": "Rejeter", + "ooui-dialog-process-retry": "Réessayez" } diff --git a/resources/lib/oojs-ui/i18n/gd.json b/resources/lib/oojs-ui/i18n/gd.json new file mode 100644 index 0000000000..6a83c9c027 --- /dev/null +++ b/resources/lib/oojs-ui/i18n/gd.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "GunChleoc" + ] + }, + "ooui-outline-control-move-down": "Gluais nì sìos", + "ooui-outline-control-move-up": "Gluais nì suas", + "ooui-outline-control-remove": "Thoir air falbh an nì", + "ooui-toolbar-more": "Barrachd", + "ooui-dialog-message-accept": "Ceart ma-thà", + "ooui-dialog-message-reject": "Sguir dheth" +} diff --git a/resources/lib/oojs-ui/i18n/gl.json b/resources/lib/oojs-ui/i18n/gl.json index a4b6787c45..eac992fd58 100644 --- a/resources/lib/oojs-ui/i18n/gl.json +++ b/resources/lib/oojs-ui/i18n/gl.json @@ -6,13 +6,13 @@ "Toliño" ] }, - "ooui-dialog-action-close": "Pechar", "ooui-outline-control-move-down": "Mover o elemento abaixo", "ooui-outline-control-move-up": "Mover o elemento arriba", "ooui-outline-control-remove": "Eliminar o elemento", "ooui-toolbar-more": "Máis", - "ooui-dialog-confirm-title": "Confirmar", - "ooui-dialog-confirm-default-prompt": "Está seguro?", - "ooui-dialog-confirm-default-ok": "Aceptar", - "ooui-dialog-confirm-default-cancel": "Cancelar" + "ooui-dialog-message-accept": "Aceptar", + "ooui-dialog-message-reject": "Cancelar", + "ooui-dialog-process-error": "Algo foi mal", + "ooui-dialog-process-dismiss": "Agochar", + "ooui-dialog-process-retry": "Inténteo de novo" } diff --git a/resources/lib/oojs-ui/i18n/he.json b/resources/lib/oojs-ui/i18n/he.json index 26660f99d8..bbaf4c1f04 100644 --- a/resources/lib/oojs-ui/i18n/he.json +++ b/resources/lib/oojs-ui/i18n/he.json @@ -15,13 +15,13 @@ "קיפודנחש" ] }, - "ooui-dialog-action-close": "סגירה", "ooui-outline-control-move-down": "להזיז את הפריט מטה", "ooui-outline-control-move-up": "להזיז את הפריט מעלה", "ooui-outline-control-remove": "להסיר את הפריט", "ooui-toolbar-more": "עוד", - "ooui-dialog-confirm-title": "אישור", - "ooui-dialog-confirm-default-prompt": "באמת?", - "ooui-dialog-confirm-default-ok": "אישור", - "ooui-dialog-confirm-default-cancel": "ביטול" + "ooui-dialog-message-accept": "אישור", + "ooui-dialog-message-reject": "ביטול", + "ooui-dialog-process-error": "משהו השתבש", + "ooui-dialog-process-dismiss": "לוותר", + "ooui-dialog-process-retry": "לנסות שוב" } diff --git a/resources/lib/oojs-ui/i18n/hu.json b/resources/lib/oojs-ui/i18n/hu.json index 0f423b3505..6069625296 100644 --- a/resources/lib/oojs-ui/i18n/hu.json +++ b/resources/lib/oojs-ui/i18n/hu.json @@ -5,15 +5,14 @@ "Einstein2", "Misibacsi", "ViDam", - "Tacsipacsi" + "Tacsipacsi", + "Csega" ] }, - "ooui-dialog-action-close": "Bezár", "ooui-outline-control-move-down": "Elem mozgatása lefelé", "ooui-outline-control-move-up": "Elem mozgatása felfelé", "ooui-outline-control-remove": "Elem eltávolítása", "ooui-toolbar-more": "Tovább...", - "ooui-dialog-confirm-title": "Megerősítés", - "ooui-dialog-confirm-default-prompt": "Biztos vagy benne?", - "ooui-dialog-confirm-default-cancel": "Mégse" + "ooui-dialog-message-reject": "Mégse", + "ooui-dialog-process-retry": "Próbáld újra" } diff --git a/resources/lib/oojs-ui/i18n/ia.json b/resources/lib/oojs-ui/i18n/ia.json index f1c9ced37b..b374b6f60a 100644 --- a/resources/lib/oojs-ui/i18n/ia.json +++ b/resources/lib/oojs-ui/i18n/ia.json @@ -4,13 +4,13 @@ "McDutchie" ] }, - "ooui-dialog-action-close": "Clauder", "ooui-outline-control-move-down": "Displaciar elemento in basso", "ooui-outline-control-move-up": "Displaciar elemento in alto", "ooui-outline-control-remove": "Remover elemento", "ooui-toolbar-more": "Plus", - "ooui-dialog-confirm-title": "Confirmation", - "ooui-dialog-confirm-default-prompt": "Es tu secur?", - "ooui-dialog-confirm-default-ok": "OK", - "ooui-dialog-confirm-default-cancel": "Cancellar" + "ooui-dialog-message-accept": "OK", + "ooui-dialog-message-reject": "Cancellar", + "ooui-dialog-process-error": "Qualcosa ha vadite mal", + "ooui-dialog-process-dismiss": "Clauder", + "ooui-dialog-process-retry": "Reprobar" } diff --git a/resources/lib/oojs-ui/i18n/lb.json b/resources/lib/oojs-ui/i18n/lb.json index e2e12abc4b..1cbcb8adb9 100644 --- a/resources/lib/oojs-ui/i18n/lb.json +++ b/resources/lib/oojs-ui/i18n/lb.json @@ -10,13 +10,13 @@ "Викиней" ] }, - "ooui-dialog-action-close": "Zoumaachen", "ooui-outline-control-move-down": "Element erof réckelen", "ooui-outline-control-move-up": "Element erop réckelen", "ooui-outline-control-remove": "Element ewechhuelen", "ooui-toolbar-more": "Méi", - "ooui-dialog-confirm-title": "Confirméieren", - "ooui-dialog-confirm-default-prompt": "Sidd Dir sécher?", - "ooui-dialog-confirm-default-ok": "OK", - "ooui-dialog-confirm-default-cancel": "Ofbriechen" + "ooui-dialog-message-accept": "OK", + "ooui-dialog-message-reject": "Ofbriechen", + "ooui-dialog-process-error": "Et ass eppes schif gaang", + "ooui-dialog-process-dismiss": "Verwerfen", + "ooui-dialog-process-retry": "Nach eng Kéier probéieren" } diff --git a/resources/lib/oojs-ui/i18n/mk.json b/resources/lib/oojs-ui/i18n/mk.json index 90685eaf33..d628034b4b 100644 --- a/resources/lib/oojs-ui/i18n/mk.json +++ b/resources/lib/oojs-ui/i18n/mk.json @@ -6,13 +6,13 @@ "Iwan Novirion" ] }, - "ooui-dialog-action-close": "Затвори", "ooui-outline-control-move-down": "Помести надолу", "ooui-outline-control-move-up": "Помести нагоре", "ooui-outline-control-remove": "Отстрани ставка", "ooui-toolbar-more": "Повеќе", - "ooui-dialog-confirm-title": "Потврди", - "ooui-dialog-confirm-default-prompt": "Дали сте сигурни?", - "ooui-dialog-confirm-default-ok": "ОК", - "ooui-dialog-confirm-default-cancel": "Откажи" + "ooui-dialog-message-accept": "ОК", + "ooui-dialog-message-reject": "Откажи", + "ooui-dialog-process-error": "Нешто не е во ред", + "ooui-dialog-process-dismiss": "Тргни", + "ooui-dialog-process-retry": "Обиди се пак" } diff --git a/resources/lib/oojs-ui/i18n/pl.json b/resources/lib/oojs-ui/i18n/pl.json index bea0c3a1a8..2431096939 100644 --- a/resources/lib/oojs-ui/i18n/pl.json +++ b/resources/lib/oojs-ui/i18n/pl.json @@ -19,13 +19,12 @@ "Andrzej aa" ] }, - "ooui-dialog-action-close": "Zamknij", "ooui-outline-control-move-down": "Przenieś niżej", "ooui-outline-control-move-up": "Przenieś wyżej", "ooui-outline-control-remove": "Usuń element", "ooui-toolbar-more": "Więcej", - "ooui-dialog-confirm-title": "Potwierdź", - "ooui-dialog-confirm-default-prompt": "Jesteś pewien?", - "ooui-dialog-confirm-default-ok": "OK", - "ooui-dialog-confirm-default-cancel": "Anuluj" + "ooui-dialog-message-accept": "OK", + "ooui-dialog-message-reject": "Anuluj", + "ooui-dialog-process-error": "Coś poszło nie tak", + "ooui-dialog-process-retry": "Spróbuj ponownie" } diff --git a/resources/lib/oojs-ui/i18n/pt.json b/resources/lib/oojs-ui/i18n/pt.json index e9ad6de3a9..5cb3e3d8e5 100644 --- a/resources/lib/oojs-ui/i18n/pt.json +++ b/resources/lib/oojs-ui/i18n/pt.json @@ -13,13 +13,13 @@ "SandroHc" ] }, - "ooui-dialog-action-close": "Fechar", "ooui-outline-control-move-down": "Mover item para baixo", "ooui-outline-control-move-up": "Mover item para cima", "ooui-outline-control-remove": "Remover elemento", "ooui-toolbar-more": "Mais", - "ooui-dialog-confirm-title": "Confirmar", - "ooui-dialog-confirm-default-prompt": "Tem a certeza?", - "ooui-dialog-confirm-default-ok": "Aceitar", - "ooui-dialog-confirm-default-cancel": "Cancelar" + "ooui-dialog-message-accept": "Aceitar", + "ooui-dialog-message-reject": "Cancelar", + "ooui-dialog-process-error": "Algo correu mal", + "ooui-dialog-process-dismiss": "Ignorar", + "ooui-dialog-process-retry": "Tentar novamente" } diff --git a/resources/lib/oojs-ui/i18n/qqq.json b/resources/lib/oojs-ui/i18n/qqq.json index 87198e54d6..e8ab9f9e55 100644 --- a/resources/lib/oojs-ui/i18n/qqq.json +++ b/resources/lib/oojs-ui/i18n/qqq.json @@ -16,16 +16,17 @@ "Sayak Sarkar", "Shirayuki", "Siebrand", - "Trevor Parscal" + "Trevor Parscal", + "Liuxinyu970226" ] }, - "ooui-dialog-action-close": "Label text for button to exit from dialog.\n\n{{Identical|Close}}", "ooui-outline-control-move-down": "Tool tip for a button that moves items in a list down one place", "ooui-outline-control-move-up": "Tool tip for a button that moves items in a list up one place", "ooui-outline-control-remove": "Tool tip for a button that removes items from a list.\n{{Identical|Remove item}}", "ooui-toolbar-more": "Label for the toolbar group that contains a list of all other available tools.\n{{Identical|More}}", - "ooui-dialog-confirm-title": "Title of the generic dialog used to confirm things.\n{{Identical|Confirm}}", - "ooui-dialog-confirm-default-prompt": "The default prompt of a confirmation dialog.\n{{Identical|Are you sure?}}", - "ooui-dialog-confirm-default-ok": "The default OK button text on a confirmation dialog.\n{{Identical|OK}}", - "ooui-dialog-confirm-default-cancel": "The default cancel button text on a confirmation dialog.\n{{Identical|Cancel}}" + "ooui-dialog-message-accept": "Default label for the accept button of a message dialog", + "ooui-dialog-message-reject": "Default label for the reject button of a message dialog", + "ooui-dialog-process-error": "Title for process dialog error description", + "ooui-dialog-process-dismiss": "Label for process dialog dismiss error button, visible when describing errors\n{{Identical|Dismiss}}", + "ooui-dialog-process-retry": "Label for process dialog retry action button, visible when describing recoverable errors\n{{Identical|Try again}}" } diff --git a/resources/lib/oojs-ui/i18n/ro.json b/resources/lib/oojs-ui/i18n/ro.json index 01815148f3..06e0f1decf 100644 --- a/resources/lib/oojs-ui/i18n/ro.json +++ b/resources/lib/oojs-ui/i18n/ro.json @@ -8,13 +8,13 @@ "Gloria sah" ] }, - "ooui-dialog-action-close": "Închide", "ooui-outline-control-move-down": "Mută elementul mai jos", "ooui-outline-control-move-up": "Mută elementul mai sus", "ooui-outline-control-remove": "Elimină elementul", "ooui-toolbar-more": "Mai mult", - "ooui-dialog-confirm-title": "Confirmare", - "ooui-dialog-confirm-default-prompt": "Sunteți sigur(ă)?", - "ooui-dialog-confirm-default-ok": "OK", - "ooui-dialog-confirm-default-cancel": "Revocare" + "ooui-dialog-message-accept": "OK", + "ooui-dialog-message-reject": "Revocare", + "ooui-dialog-process-error": "Ceva nu a funcționat", + "ooui-dialog-process-dismiss": "Renunțare", + "ooui-dialog-process-retry": "Reîncearcă" } diff --git a/resources/lib/oojs-ui/i18n/ru.json b/resources/lib/oojs-ui/i18n/ru.json index 435f20c482..efd106275d 100644 --- a/resources/lib/oojs-ui/i18n/ru.json +++ b/resources/lib/oojs-ui/i18n/ru.json @@ -18,13 +18,13 @@ "Умар" ] }, - "ooui-dialog-action-close": "Закрыть", "ooui-outline-control-move-down": "Переместить элемент вниз", "ooui-outline-control-move-up": "Переместить элемент вверх", "ooui-outline-control-remove": "Удалить пункт", "ooui-toolbar-more": "Ещё", - "ooui-dialog-confirm-title": "Подтвердить", - "ooui-dialog-confirm-default-prompt": "Вы уверены?", - "ooui-dialog-confirm-default-ok": "ОК", - "ooui-dialog-confirm-default-cancel": "Отмена" + "ooui-dialog-message-accept": "ОК", + "ooui-dialog-message-reject": "Отмена", + "ooui-dialog-process-error": "Что-то пошло не так", + "ooui-dialog-process-dismiss": "Закрыть", + "ooui-dialog-process-retry": "Попробовать ещё раз" } diff --git a/resources/lib/oojs-ui/i18n/sq.json b/resources/lib/oojs-ui/i18n/sq.json index 44dfd60928..ec180199d1 100644 --- a/resources/lib/oojs-ui/i18n/sq.json +++ b/resources/lib/oojs-ui/i18n/sq.json @@ -4,16 +4,16 @@ "Euriditi", "Kushtrim", "Elioqoshi", - "GretaDoci" + "GretaDoci", + "Gertakapllani" ] }, - "ooui-dialog-action-close": "Mbylle", "ooui-outline-control-move-down": "Zhvendose artikullin më poshtë", "ooui-outline-control-move-up": "Zhvendose artikullin më lart", "ooui-outline-control-remove": "Hiq artikullin", "ooui-toolbar-more": "Më tepër...", - "ooui-dialog-confirm-title": "Konfirmo", - "ooui-dialog-confirm-default-prompt": "A jeni i sigurt?", - "ooui-dialog-confirm-default-ok": "Në rregull", - "ooui-dialog-confirm-default-cancel": "Anullo" + "ooui-dialog-message-accept": "Në rregull", + "ooui-dialog-message-reject": "Anullo", + "ooui-dialog-process-error": "Diçka shkoi keq", + "ooui-dialog-process-retry": "Provo përsëri" } diff --git a/resources/lib/oojs-ui/i18n/uk.json b/resources/lib/oojs-ui/i18n/uk.json index 2bdac54297..11aeed488c 100644 --- a/resources/lib/oojs-ui/i18n/uk.json +++ b/resources/lib/oojs-ui/i18n/uk.json @@ -21,5 +21,9 @@ "ooui-outline-control-move-down": "Перемістити елемент униз", "ooui-outline-control-move-up": "Перемістити елемент вгору", "ooui-outline-control-remove": "Видалити елемент", - "ooui-toolbar-more": "Більше" + "ooui-toolbar-more": "Більше", + "ooui-dialog-confirm-title": "Підтвердити", + "ooui-dialog-confirm-default-prompt": "Ви впевнені?", + "ooui-dialog-confirm-default-ok": "Готово", + "ooui-dialog-confirm-default-cancel": "Скасувати" } diff --git a/resources/lib/oojs-ui/i18n/yi.json b/resources/lib/oojs-ui/i18n/yi.json index 01a22d1bf4..e26af708cd 100644 --- a/resources/lib/oojs-ui/i18n/yi.json +++ b/resources/lib/oojs-ui/i18n/yi.json @@ -6,13 +6,12 @@ "十弌" ] }, - "ooui-dialog-action-close": "שליסן", "ooui-outline-control-move-down": "רוקן עלעמענט אראפ", "ooui-outline-control-move-up": "רוקן עלעמענט ארויף", "ooui-outline-control-remove": "אַראָפנעמען איינס", "ooui-toolbar-more": "נאך", - "ooui-dialog-confirm-title": "באַשטעטיקן", - "ooui-dialog-confirm-default-prompt": "איר זענט זיכער?", - "ooui-dialog-confirm-default-ok": "יאָ", - "ooui-dialog-confirm-default-cancel": "אַנולירן" + "ooui-dialog-message-accept": "יאָ", + "ooui-dialog-message-reject": "אַנולירן", + "ooui-dialog-process-error": "עפעס איז דורכגעפאלן", + "ooui-dialog-process-retry": "פרובירט נאכאמאל" } diff --git a/resources/lib/oojs-ui/i18n/zh-hans.json b/resources/lib/oojs-ui/i18n/zh-hans.json index 8d1c09fa8d..50df67a8b9 100644 --- a/resources/lib/oojs-ui/i18n/zh-hans.json +++ b/resources/lib/oojs-ui/i18n/zh-hans.json @@ -18,13 +18,13 @@ "乌拉跨氪" ] }, - "ooui-dialog-action-close": "关闭", "ooui-outline-control-move-down": "下移项", "ooui-outline-control-move-up": "上移项", "ooui-outline-control-remove": "删除项", "ooui-toolbar-more": "更多", - "ooui-dialog-confirm-title": "确认", - "ooui-dialog-confirm-default-prompt": "您确定吗?", - "ooui-dialog-confirm-default-ok": "好", - "ooui-dialog-confirm-default-cancel": "取消" + "ooui-dialog-message-accept": "好", + "ooui-dialog-message-reject": "取消", + "ooui-dialog-process-error": "发生一些错误", + "ooui-dialog-process-dismiss": "解除", + "ooui-dialog-process-retry": "重试" } diff --git a/resources/lib/oojs-ui/images/tail.svg b/resources/lib/oojs-ui/images/anchor.svg similarity index 97% rename from resources/lib/oojs-ui/images/tail.svg rename to resources/lib/oojs-ui/images/anchor.svg index 4df8bb2be6..417bc96381 100644 --- a/resources/lib/oojs-ui/images/tail.svg +++ b/resources/lib/oojs-ui/images/anchor.svg @@ -2,7 +2,7 @@ - + diff --git a/resources/lib/oojs-ui/oojs-ui-agora.css b/resources/lib/oojs-ui/oojs-ui-agora.css index dc999cd549..5b35674e70 100644 --- a/resources/lib/oojs-ui/oojs-ui-agora.css +++ b/resources/lib/oojs-ui/oojs-ui-agora.css @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.1.0-pre (85cfc2e735) + * OOjs UI v0.1.0-pre (d2451ac748) * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2014 OOjs Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2014-07-03T02:33:09Z + * Date: 2014-07-14T16:49:51Z */ .oo-ui-dialog-content .oo-ui-window-closeButton { position: absolute; diff --git a/resources/lib/oojs-ui/oojs-ui-apex.css b/resources/lib/oojs-ui/oojs-ui-apex.css index 7018b52067..aeba58250e 100644 --- a/resources/lib/oojs-ui/oojs-ui-apex.css +++ b/resources/lib/oojs-ui/oojs-ui-apex.css @@ -1,53 +1,16 @@ /*! - * OOjs UI v0.1.0-pre (85cfc2e735) + * OOjs UI v0.1.0-pre (d2451ac748) * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2014 OOjs Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2014-07-03T02:33:09Z + * Date: 2014-07-14T16:49:51Z */ -.oo-ui-dialog { - background-color: #fff; - background-color: rgba(255, 255, 255, 0.5); - /* Opening and closing animation */ - - opacity: 0; -} - -.oo-ui-dialog > .oo-ui-window-frame { - -webkit-transform: scale(0.5); - -moz-transform: scale(0.5); - -ms-transform: scale(0.5); - -o-transform: scale(0.5); - transform: scale(0.5); -} - -.oo-ui-dialog.oo-ui-window-setup, -.oo-ui-dialog.oo-ui-window-setup > .oo-ui-window-frame { - -webkit-transition: all 250ms ease-in-out; - -moz-transition: all 250ms ease-in-out; - -ms-transition: all 250ms ease-in-out; - -o-transition: all 250ms ease-in-out; - transition: all 250ms ease-in-out; -} - -.oo-ui-dialog.oo-ui-window-ready { - opacity: 1; -} - -.oo-ui-dialog.oo-ui-window-ready > .oo-ui-window-frame { - -webkit-transform: scale(1); - -moz-transform: scale(1); - -ms-transform: scale(1); - -o-transform: scale(1); - transform: scale(1); -} - -.oo-ui-dialog-content .oo-ui-window-head, -.oo-ui-dialog-content .oo-ui-window-body, -.oo-ui-dialog-content .oo-ui-window-foot { +.oo-ui-dialog-content > .oo-ui-window-head, +.oo-ui-dialog-content > .oo-ui-window-body, +.oo-ui-dialog-content > .oo-ui-window-foot { position: absolute; right: 0; left: 0; @@ -57,81 +20,24 @@ box-sizing: border-box; } -.oo-ui-dialog-content .oo-ui-window-head { +.oo-ui-dialog-content > .oo-ui-window-head { top: 0; - height: 3.8em; - padding: 0.5em; -} - -.oo-ui-dialog-content .oo-ui-window-title { - line-height: 2.8em; -} - -.oo-ui-dialog-content .oo-ui-window-icon { - width: 2.4em; - height: 2.8em; - line-height: 2.8em; -} - -.oo-ui-dialog-content .oo-ui-window-closeButton { - float: right; - margin: 0.25em 0.25em; -} - -.oo-ui-dialog-content .oo-ui-window-body { - top: 3.8em; - bottom: 4.8em; -} - -.oo-ui-dialog-content-footless .oo-ui-window-body { - bottom: 0; -} - -.oo-ui-dialog > .oo-ui-window-frame { - top: 1em; - bottom: 1em; - background-color: #fff; - border: solid 1px #ccc; - border-radius: 0.5em; - box-shadow: 0 0.2em 1em rgba(0, 0, 0, 0.3); -} - -.oo-ui-dialog-small > .oo-ui-window-frame { - width: 400px; - max-height: 230px; -} - -.oo-ui-dialog-medium > .oo-ui-window-frame { - width: 600px; - max-height: 460px; -} - -.oo-ui-dialog-large > .oo-ui-window-frame { - width: 800px; - max-height: 690px; -} - -.oo-ui-dialog-content .oo-ui-window-head, -.oo-ui-dialog-content .oo-ui-window-foot { z-index: 1; } -.oo-ui-dialog-content .oo-ui-window-body { +.oo-ui-dialog-content > .oo-ui-window-body { + top: 0; + bottom: 0; z-index: 2; box-shadow: 0 0 0.66em rgba(0, 0, 0, 0.25); } -.oo-ui-dialog-content .oo-ui-window-foot { +.oo-ui-dialog-content > .oo-ui-window-foot { bottom: 0; - height: 4.8em; - padding: 1em; -} - -.oo-ui-dialog-content .oo-ui-window-foot .oo-ui-buttonedElement-framed { - margin: 0.125em 0.25em; + z-index: 1; } -.oo-ui-dialog-content .oo-ui-window-overlay { +.oo-ui-dialog-content > .oo-ui-window-overlay { z-index: 3; } @@ -187,20 +93,14 @@ color: #000; } -.oo-ui-window-body { - padding: 0 0.75em; -} - -.oo-ui-window-icon { - width: 2em; - height: 2em; - margin-right: 0.5em; - line-height: 2em; +.oo-ui-window > .oo-ui-window-frame { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } -.oo-ui-window-title { - line-height: 2em; - color: #333; +.oo-ui-window-content { + background: transparent; } .oo-ui-window-overlay { @@ -209,47 +109,436 @@ line-height: 1.5em; } -.oo-ui-buttonedElement .oo-ui-buttonedElement-button { +.oo-ui-windowManager-modal > .oo-ui-dialog { + background-color: #fff; + background-color: rgba(255, 255, 255, 0.5); + opacity: 0; + -webkit-transition: opacity 250ms ease-in-out; + -moz-transition: opacity 250ms ease-in-out; + -ms-transition: opacity 250ms ease-in-out; + -o-transition: opacity 250ms ease-in-out; + transition: opacity 250ms ease-in-out; +} + +.oo-ui-windowManager-modal > .oo-ui-dialog > .oo-ui-window-frame { + top: 1em; + bottom: 1em; + background-color: #fff; + border: solid 1px #ccc; + border-radius: 0.5em; + -webkit-transform: scale(0.5); + -moz-transform: scale(0.5); + -ms-transform: scale(0.5); + -o-transform: scale(0.5); + transform: scale(0.5); + box-shadow: 0 0.2em 1em rgba(0, 0, 0, 0.3); + -webkit-transition: all 250ms ease-in-out; + -moz-transition: all 250ms ease-in-out; + -ms-transition: all 250ms ease-in-out; + -o-transition: all 250ms ease-in-out; + transition: all 250ms ease-in-out; +} + +.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-ready { + opacity: 1; +} + +.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-ready > .oo-ui-window-frame { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -ms-transform: scale(1); + -o-transform: scale(1); + transform: scale(1); +} + +.oo-ui-windowManager-fullscreen > .oo-ui-dialog > .oo-ui-window-frame { + top: 0; + bottom: 0; + width: 100%; + height: 100%; + border: none; + border-radius: 0; + box-shadow: none; +} + +.oo-ui-messageDialog-text.oo-ui-panelLayout { + bottom: auto; +} + +.oo-ui-messageDialog-title { + display: block; + padding-top: 0; + font-size: 1.5em; + color: #000; + text-align: center; +} + +.oo-ui-messageDialog-message { + display: block; + font-size: 0.9em; + line-height: 1.25em; + color: #666; + text-align: center; +} + +.oo-ui-messageDialog-message-verbose { + font-size: 1.1em; + line-height: 1.5em; + text-align: left; +} + +.oo-ui-messageDialog-content > .oo-ui-window-body { + bottom: 3.4em; + box-shadow: 0 0 0.66em rgba(0, 0, 0, 0.25); +} + +.oo-ui-messageDialog-content > .oo-ui-window-foot { + min-height: 3.4em; +} + +.oo-ui-messageDialog-actions-horizontal { + display: table; + width: 100%; + table-layout: fixed; +} + +.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget { + display: table-cell; + width: 1%; + border-right: solid 1px #e5e5e5; +} + +.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget:last-child { + border-right-width: 0; +} + +.oo-ui-messageDialog-actions-vertical { + display: block; +} + +.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget { + display: block; + overflow: hidden; + text-overflow: ellipsis; + border-bottom: solid 1px #e5e5e5; +} + +.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget:last-child { + border-bottom-width: 0; +} + +.oo-ui-messageDialog-actions .oo-ui-actionWidget { + position: relative; + height: 3.4em; + padding: 0; + text-align: center; +} + +.oo-ui-messageDialog-actions .oo-ui-actionWidget:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.oo-ui-messageDialog-actions .oo-ui-actionWidget:active { + background-color: rgba(0, 0, 0, 0.1); +} + +.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-primary:hover { + background-color: rgba(8, 126, 204, 0.05); +} + +.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-primary:active { + background-color: rgba(8, 126, 204, 0.1); +} + +.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-primary .oo-ui-labeledElement-label { + font-weight: bold; +} + +.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:hover { + background-color: rgba(118, 171, 54, 0.05); +} + +.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:active { + background-color: rgba(118, 171, 54, 0.1); +} + +.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:hover { + background-color: rgba(212, 83, 83, 0.05); +} + +.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:active { + background-color: rgba(212, 83, 83, 0.1); +} + +.oo-ui-messageDialog-actions .oo-ui-actionWidget .oo-ui-buttonedElement-button { + display: block; +} + +.oo-ui-messageDialog-actions .oo-ui-actionWidget .oo-ui-labeledElement-label { + position: relative; + top: auto; + bottom: auto; + display: inline; + padding: 0 2em; + line-height: 3.4em; + white-space: nowrap; +} + +.oo-ui-processDialog-content > .oo-ui-window-head { + height: 3.4em; +} + +.oo-ui-processDialog-content > .oo-ui-window-body { + top: 3.4em; + box-shadow: 0 0 0.66em rgba(0, 0, 0, 0.25); +} + +.oo-ui-processDialog-navigation { + position: relative; + height: 3.4em; + padding: 0 1em; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-touch-callout: none; +} + +.oo-ui-processDialog-location { + height: 1.9em; + padding: 0.75em 0; + overflow: hidden; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; + cursor: default; +} + +.oo-ui-processDialog-location .oo-ui-labelWidget { + display: inline; +} + +.oo-ui-processDialog-title { + font-weight: bold; + line-height: 1.9em; +} + +.oo-ui-processDialog-actions-safe .oo-ui-actionWidget, +.oo-ui-processDialog-actions-primary .oo-ui-actionWidget, +.oo-ui-processDialog-actions-other .oo-ui-actionWidget { + white-space: nowrap; +} + +.oo-ui-processDialog-actions-safe .oo-ui-actionWidget .oo-ui-buttonedElement-button, +.oo-ui-processDialog-actions-primary .oo-ui-actionWidget .oo-ui-buttonedElement-button, +.oo-ui-processDialog-actions-other .oo-ui-actionWidget .oo-ui-buttonedElement-button { + min-width: 1.9em; + min-height: 1.9em; + padding-top: 0.75em; + padding-bottom: 0.75em; +} + +.oo-ui-processDialog-actions-safe .oo-ui-actionWidget .oo-ui-labeledElement-label, +.oo-ui-processDialog-actions-primary .oo-ui-actionWidget .oo-ui-labeledElement-label, +.oo-ui-processDialog-actions-other .oo-ui-actionWidget .oo-ui-labeledElement-label { + padding: 0 1em; + line-height: 1.9em; +} + +.oo-ui-processDialog-actions-safe .oo-ui-actionWidget .oo-ui-iconedElement-icon, +.oo-ui-processDialog-actions-primary .oo-ui-actionWidget .oo-ui-iconedElement-icon, +.oo-ui-processDialog-actions-other .oo-ui-actionWidget .oo-ui-iconedElement-icon { + position: absolute; + margin-top: -0.125em; +} + +.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button, +.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button, +.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button { + padding: 0; + vertical-align: middle; +} + +.oo-ui-processDialog-actions-safe, +.oo-ui-processDialog-actions-primary { + position: absolute; + top: 0; + bottom: 0; +} + +.oo-ui-processDialog-actions-safe.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button, +.oo-ui-processDialog-actions-primary.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button { + margin: 0.75em; +} + +.oo-ui-processDialog-actions-safe .oo-ui-actionWidget:hover, +.oo-ui-processDialog-actions-primary .oo-ui-actionWidget:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.oo-ui-processDialog-actions-safe .oo-ui-actionWidget:active, +.oo-ui-processDialog-actions-primary .oo-ui-actionWidget:active { + background-color: rgba(0, 0, 0, 0.1); +} + +.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-primary:hover, +.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-primary:hover { + background-color: rgba(8, 126, 204, 0.05); +} + +.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-primary:active, +.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-primary:active { + background-color: rgba(8, 126, 204, 0.1); +} + +.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-primary .oo-ui-labeledElement-label, +.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-primary .oo-ui-labeledElement-label { + font-weight: bold; +} + +.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:hover, +.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:hover { + background-color: rgba(118, 171, 54, 0.05); +} + +.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:active, +.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:active { + background-color: rgba(118, 171, 54, 0.1); +} + +.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:hover, +.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:hover { + background-color: rgba(212, 83, 83, 0.05); +} + +.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:active, +.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:active { + background-color: rgba(212, 83, 83, 0.1); +} + +.oo-ui-processDialog-actions-safe { + left: 0; +} + +.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-iconedElement .oo-ui-iconedElement-icon { + left: 0.5em; +} + +.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-iconedElement .oo-ui-labeledElement-label { + padding-left: 2.25em; +} + +.oo-ui-processDialog-actions-primary { + right: 0; +} + +.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-iconedElement .oo-ui-iconedElement-icon { + right: 0.5em; +} + +.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-iconedElement .oo-ui-labeledElement-label { + padding-right: 2.25em; +} + +.oo-ui-processDialog-actions-other:not(:empty) { + padding: 0.75em; +} + +.oo-ui-processDialog-actions-other:not(:empty) .oo-ui-actionWidget { + margin: 0 0.75em 0 0; +} + +.oo-ui-processDialog > .oo-ui-window-frame { + min-height: 5em; +} + +.oo-ui-processDialog-errors { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 2; + display: none; + padding: 3em 3em 1.5em 3em; + overflow-x: hidden; + overflow-y: auto; + text-align: center; + background-color: rgba(255, 255, 255, 0.9); +} + +.oo-ui-processDialog-errors .oo-ui-buttonWidget { + margin: 2em 1em 2em 1em; +} + +.oo-ui-processDialog-errors-title { + margin-bottom: 2em; + font-size: 1.5em; + color: #000; +} + +.oo-ui-processDialog-error { + padding: 1em; + margin: 1em; + text-align: left; + background-color: #fff7f7; + border: solid 1px #ff9e9e; + border-radius: 0.25em; +} + +.oo-ui-buttonedElement > .oo-ui-buttonedElement-button { color: #333; } -.oo-ui-buttonedElement.oo-ui-indicatedElement .oo-ui-buttonedElement-button > .oo-ui-indicatedElement-indicator, -.oo-ui-buttonedElement.oo-ui-iconedElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon { +.oo-ui-buttonedElement.oo-ui-indicatedElement > .oo-ui-buttonedElement-button > .oo-ui-indicatedElement-indicator, +.oo-ui-buttonedElement.oo-ui-iconedElement > .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon { width: 1.9em; height: 1.9em; opacity: 0.8; } -.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon { +.oo-ui-buttonedElement-frameless > .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon { /* Don't animate opacities for now, causes wiggling in Chrome (bug 63020) */ /*.oo-ui-transition(opacity 200ms);*/ } -.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button:hover > .oo-ui-iconedElement-icon, -.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button:focus > .oo-ui-iconedElement-icon { +.oo-ui-buttonedElement-frameless > .oo-ui-buttonedElement-button:hover > .oo-ui-iconedElement-icon, +.oo-ui-buttonedElement-frameless > .oo-ui-buttonedElement-button:focus > .oo-ui-iconedElement-icon { opacity: 1; } -.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button:hover > .oo-ui-labeledElement-label, -.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button:focus > .oo-ui-labeledElement-label { +.oo-ui-buttonedElement-frameless > .oo-ui-buttonedElement-button:hover > .oo-ui-labeledElement-label, +.oo-ui-buttonedElement-frameless > .oo-ui-buttonedElement-button:focus > .oo-ui-labeledElement-label { color: #000; } -.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label { +.oo-ui-buttonedElement-frameless > .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label { color: #333; } -.oo-ui-buttonedElement-frameless.oo-ui-widget-disabled .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon { +.oo-ui-buttonedElement-frameless.oo-ui-flaggableElement-primary > .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label { + color: #087ecc; +} + +.oo-ui-buttonedElement-frameless.oo-ui-flaggableElement-constructive > .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label { + color: #76ab36; +} + +.oo-ui-buttonedElement-frameless.oo-ui-flaggableElement-destructive > .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label { + color: #d45353; +} + +.oo-ui-buttonedElement-frameless.oo-ui-widget-disabled > .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon { opacity: 0.2; } -.oo-ui-buttonedElement-frameless.oo-ui-widget-disabled .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label { +.oo-ui-buttonedElement-frameless.oo-ui-widget-disabled > .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label { color: #ccc; } -.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button { +.oo-ui-buttonedElement-framed > .oo-ui-buttonedElement-button { padding: 0.2em 0.8em; margin: 0.1em 0; text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5); @@ -270,13 +559,13 @@ transition: border-color 100ms ease-in-out; } -.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button:hover, -.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button:focus { +.oo-ui-buttonedElement-framed > .oo-ui-buttonedElement-button:hover, +.oo-ui-buttonedElement-framed > .oo-ui-buttonedElement-button:focus { border-color: #aaa; } -.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active, -.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed { +.oo-ui-buttonedElement-framed > .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active, +.oo-ui-buttonedElement-framed > .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed { color: black; background: #eeeeee; background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #dddddd), color-stop(100%, #ffffff)); @@ -290,17 +579,17 @@ box-shadow: inset 0 1px 4px 0 rgba(0, 0, 0, 0.07); } -.oo-ui-buttonedElement-framed.oo-ui-iconedElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon { +.oo-ui-buttonedElement-framed.oo-ui-iconedElement > .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon { margin-right: -0.5em; margin-left: -0.5em; } -.oo-ui-buttonedElement-framed.oo-ui-iconedElement.oo-ui-labeledElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon { +.oo-ui-buttonedElement-framed.oo-ui-iconedElement.oo-ui-labeledElement > .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon { margin-right: 0.3em; margin-left: -0.5em; } -.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button { +.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary > .oo-ui-buttonedElement-button { background: #cde7f4; background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #eaf4fa), color-stop(100%, #b0d9ee)); background-image: -webkit-linear-gradient(top, #eaf4fa 0%, #b0d9ee 100%); @@ -312,13 +601,13 @@ filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#eaf4fa', endColorstr='#b0d9ee'); } -.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button:hover, -.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button:focus { +.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary > .oo-ui-buttonedElement-button:hover, +.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary > .oo-ui-buttonedElement-button:focus { border-color: #9dc2d4; } -.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active, -.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed { +.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary > .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active, +.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary > .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed { background: #cde7f4; background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #b0d9ee), color-stop(100%, #eaf4fa)); background-image: -webkit-linear-gradient(top, #b0d9ee 0%, #eaf4fa 100%); @@ -330,7 +619,7 @@ filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#b0d9ee', endColorstr='#eaf4fa'); } -.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button { +.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive > .oo-ui-buttonedElement-button { background: #daf0be; background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #f0fbe1), color-stop(100%, #c3e59a)); background-image: -webkit-linear-gradient(top, #f0fbe1 0%, #c3e59a 100%); @@ -342,13 +631,13 @@ filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#f0fbe1', endColorstr='#c3e59a'); } -.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button:hover, -.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button:focus { +.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive > .oo-ui-buttonedElement-button:hover, +.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive > .oo-ui-buttonedElement-button:focus { border-color: #adcb89; } -.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active, -.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed { +.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive > .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active, +.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive > .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed { background: #daf0be; background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #c3e59a), color-stop(100%, #f0fbe1)); background-image: -webkit-linear-gradient(top, #c3e59a 0%, #f0fbe1 100%); @@ -360,13 +649,13 @@ filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#c3e59a', endColorstr='#f0fbe1'); } -.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-destructive .oo-ui-buttonedElement-button { +.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-destructive > .oo-ui-buttonedElement-button { color: #d45353; } -.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button, -.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active, -.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed { +.oo-ui-buttonedElement-framed.oo-ui-widget-disabled > .oo-ui-buttonedElement-button, +.oo-ui-buttonedElement-framed.oo-ui-widget-disabled > .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active, +.oo-ui-buttonedElement-framed.oo-ui-widget-disabled > .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed { color: #333; background: #eee; border-color: #ccc; @@ -374,12 +663,12 @@ box-shadow: none; } -.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button:hover, -.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active:hover, -.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed:hover, -.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button:focus, -.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active:focus, -.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed:focus { +.oo-ui-buttonedElement-framed.oo-ui-widget-disabled > .oo-ui-buttonedElement-button:hover, +.oo-ui-buttonedElement-framed.oo-ui-widget-disabled > .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active:hover, +.oo-ui-buttonedElement-framed.oo-ui-widget-disabled > .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed:hover, +.oo-ui-buttonedElement-framed.oo-ui-widget-disabled > .oo-ui-buttonedElement-button:focus, +.oo-ui-buttonedElement-framed.oo-ui-widget-disabled > .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active:focus, +.oo-ui-buttonedElement-framed.oo-ui-widget-disabled > .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed:focus { border-color: #ccc; box-shadow: none; } @@ -417,7 +706,7 @@ } .oo-ui-panelLayout-padded { - padding: 2em; + padding: 1.25em; } .oo-ui-barToolGroup .oo-ui-tool { @@ -630,11 +919,11 @@ box-shadow: 0 0.15em 0.5em 0 rgba(0, 0, 0, 0.2); } -.oo-ui-popupWidget-tailed .oo-ui-popupWidget-tail { +.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor { width: 15px; height: 8px; margin-left: -7px; - background-image: /* @embed */ url(images/tail.svg); + background-image: /* @embed */ url(images/anchor.svg); } .oo-ui-popupWidget-transitioning .oo-ui-popupWidget-popup { diff --git a/resources/lib/oojs-ui/oojs-ui.js b/resources/lib/oojs-ui/oojs-ui.js index f2e3202bcc..68d4be641c 100644 --- a/resources/lib/oojs-ui/oojs-ui.js +++ b/resources/lib/oojs-ui/oojs-ui.js @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.1.0-pre (85cfc2e735) + * OOjs UI v0.1.0-pre (d2451ac748) * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2014 OOjs Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2014-07-03T02:33:09Z + * Date: 2014-07-14T16:49:51Z */ ( function ( OO ) { @@ -94,7 +94,6 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) { }; ( function () { - /** * Message store for the default implementation of OO.ui.msg * @@ -104,8 +103,6 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) { * @private */ var messages = { - // Label text for button to exit from dialog - 'ooui-dialog-action-close': 'Close', // Tool tip for a button that moves items in a list down one place 'ooui-outline-control-move-down': 'Move item down', // Tool tip for a button that moves items in a list up one place @@ -114,15 +111,16 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) { 'ooui-outline-control-remove': 'Remove item', // Label for the toolbar group that contains a list of all other available tools 'ooui-toolbar-more': 'More', - - // Label for the generic dialog used to confirm things - 'ooui-dialog-confirm-title': 'Confirm', - // The default prompt of a confirmation dialog - 'ooui-dialog-confirm-default-prompt': 'Are you sure?', - // The default OK button text on a confirmation dialog - 'ooui-dialog-confirm-default-ok': 'OK', - // The default cancel button text on a confirmation dialog - 'ooui-dialog-confirm-default-cancel': 'Cancel' + // Default label for the accept button of a confirmation dialog + 'ooui-dialog-message-accept': 'OK', + // Default label for the reject button of a confirmation dialog + 'ooui-dialog-message-reject': 'Cancel', + // Title for process dialog error description + 'ooui-dialog-process-error': 'Something went wrong', + // Label for process dialog dismiss error button, visible when describing errors + 'ooui-dialog-process-dismiss': 'Dismiss', + // Label for process dialog retry action button, visible when describing recoverable errors + 'ooui-dialog-process-retry': 'Try again' }; /** @@ -157,14 +155,30 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) { return message; }; - /** */ - OO.ui.deferMsg = function ( key ) { + /** + * Package a message and arguments for deferred resolution. + * + * Use this when you are statically specifying a message and the message may not yet be present. + * + * @param {string} key Message key + * @param {Mixed...} [params] Message parameters + * @return {Function} Function that returns the resolved message when executed + */ + OO.ui.deferMsg = function () { + var args = arguments; return function () { - return OO.ui.msg( key ); + return OO.ui.msg.apply( OO.ui, args ); }; }; - /** */ + /** + * Resolve a message. + * + * If the message is a function it will be executed, otherwise it will pass through directly. + * + * @param {Function|string} msg Deferred message, or message text + * @return {string} Resolved message + */ OO.ui.resolveMsg = function ( msg ) { if ( $.isFunction( msg ) ) { return msg(); @@ -174,6 +188,414 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) { } )(); +/** + * List of actions. + * + * @abstract + * @class + * @mixins OO.EventEmitter + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.ActionSet = function OoUiActionSet( config ) { + // Configuration intialization + config = config || {}; + + // Mixin constructors + OO.EventEmitter.call( this ); + + // Properties + this.list = []; + this.categories = { + 'actions': 'getAction', + 'flags': 'getFlags', + 'modes': 'getModes' + }; + this.categorized = {}; + this.special = {}; + this.others = []; + this.organized = false; + this.changing = false; + this.changed = false; +}; + +/* Setup */ + +OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter ); + +/* Static Properties */ + +/** + * Symbolic name of dialog. + * + * @abstract + * @static + * @inheritable + * @property {string} + */ +OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ]; + +/* Events */ + +/** + * @event click + * @param {OO.ui.ActionWidget} action Action that was clicked + */ + +/** + * @event resize + * @param {OO.ui.ActionWidget} action Action that was resized + */ + +/** + * @event add + * @param {OO.ui.ActionWidget[]} added Actions added + */ + +/** + * @event remove + * @param {OO.ui.ActionWidget[]} added Actions removed + */ + +/** + * @event change + */ + +/* Methods */ + +/** + * Handle action change events. + * + * @fires change + */ +OO.ui.ActionSet.prototype.onActionChange = function () { + this.organized = false; + if ( this.changing ) { + this.changed = true; + } else { + this.emit( 'change' ); + } +}; + +/** + * Check if a action is one of the special actions. + * + * @param {OO.ui.ActionWidget} action Action to check + * @return {boolean} Action is special + */ +OO.ui.ActionSet.prototype.isSpecial = function ( action ) { + var flag; + + for ( flag in this.special ) { + if ( action === this.special[flag] ) { + return true; + } + } + + return false; +}; + +/** + * Get actions. + * + * @param {Object} [filters] Filters to use, omit to get all actions + * @param {string|string[]} [filters.actions] Actions that actions must have + * @param {string|string[]} [filters.flags] Flags that actions must have + * @param {string|string[]} [filters.modes] Modes that actions must have + * @param {boolean} [filters.visible] Actions must be visible + * @param {boolean} [filters.disabled] Actions must be disabled + * @return {OO.ui.ActionWidget[]} Actions matching all criteria + */ +OO.ui.ActionSet.prototype.get = function ( filters ) { + var i, len, list, category, actions, index, match, matches; + + if ( filters ) { + this.organize(); + + // Collect category candidates + matches = []; + for ( category in this.categorized ) { + list = filters[category]; + if ( list ) { + if ( !Array.isArray( list ) ) { + list = [ list ]; + } + for ( i = 0, len = list.length; i < len; i++ ) { + actions = this.categorized[category][list[i]]; + if ( Array.isArray( actions ) ) { + matches.push.apply( matches, actions ); + } + } + } + } + // Remove by boolean filters + for ( i = 0, len = matches.length; i < len; i++ ) { + match = matches[i]; + if ( + ( filters.visible !== undefined && match.isVisible() !== filters.visible ) || + ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled ) + ) { + matches.splice( i, 1 ); + len--; + i--; + } + } + // Remove duplicates + for ( i = 0, len = matches.length; i < len; i++ ) { + match = matches[i]; + index = matches.lastIndexOf( match ); + while ( index !== i ) { + matches.splice( index, 1 ); + len--; + index = matches.lastIndexOf( match ); + } + } + return matches; + } + return this.list.slice(); +}; + +/** + * Get special actions. + * + * Special actions are the first visible actions with special flags, such as 'safe' and 'primary'. + * Special flags can be configured by changing #static-specialFlags in a subclass. + * + * @return {OO.ui.ActionWidget|null} Safe action + */ +OO.ui.ActionSet.prototype.getSpecial = function () { + this.organize(); + return $.extend( {}, this.special ); +}; + +/** + * Get other actions. + * + * Other actions include all non-special visible actions. + * + * @return {OO.ui.ActionWidget[]} Other actions + */ +OO.ui.ActionSet.prototype.getOthers = function () { + this.organize(); + return this.others.slice(); +}; + +/** + * Toggle actions based on their modes. + * + * Unlike calling toggle on actions with matching flags, this will enforce mutually exclusive + * visibility; matching actions will be shown, non-matching actions will be hidden. + * + * @param {string} mode Mode actions must have + * @chainable + * @fires toggle + * @fires change + */ +OO.ui.ActionSet.prototype.setMode = function ( mode ) { + var i, len, action; + + this.changing = true; + for ( i = 0, len = this.list.length; i < len; i++ ) { + action = this.list[i]; + action.toggle( action.hasMode( mode ) ); + } + + this.organized = false; + this.changing = false; + this.emit( 'change' ); + + return this; +}; + +/** + * Change which actions are able to be performed. + * + * Actions with matching actions will be disabled/enabled. Other actions will not be changed. + * + * @param {Object.} actions List of abilities, keyed by action name, values + * indicate actions are able to be performed + * @chainable + */ +OO.ui.ActionSet.prototype.setAbilities = function ( actions ) { + var i, len, action, item; + + for ( i = 0, len = this.list.length; i < len; i++ ) { + item = this.list[i]; + action = item.getAction(); + if ( actions[action] !== undefined ) { + item.setDisabled( !actions[action] ); + } + } + + return this; +}; + +/** + * Executes a function once per action. + * + * When making changes to multiple actions, use this method instead of iterating over the actions + * manually to defer emitting a change event until after all actions have been changed. + * + * @param {Object|null} actions Filters to use for which actions to iterate over; see #get + * @param {Function} callback Callback to run for each action; callback is invoked with three + * arguments: the action, the action's index, the list of actions being iterated over + * @chainable + */ +OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) { + this.changed = false; + this.changing = true; + this.get( filter ).forEach( callback ); + this.changing = false; + if ( this.changed ) { + this.emit( 'change' ); + } + + return this; +}; + +/** + * Add actions. + * + * @param {OO.ui.ActionWidget[]} actions Actions to add + * @chainable + * @fires add + * @fires change + */ +OO.ui.ActionSet.prototype.add = function ( actions ) { + var i, len, action; + + this.changing = true; + for ( i = 0, len = actions.length; i < len; i++ ) { + action = actions[i]; + action.connect( this, { + 'click': [ 'emit', 'click', action ], + 'resize': [ 'emit', 'resize', action ], + 'toggle': [ 'onActionChange' ] + } ); + this.list.push( action ); + } + this.organized = false; + this.emit( 'add', actions ); + this.changing = false; + this.emit( 'change' ); + + return this; +}; + +/** + * Remove actions. + * + * @param {OO.ui.ActionWidget[]} actions Actions to remove + * @chainable + * @fires remove + * @fires change + */ +OO.ui.ActionSet.prototype.remove = function ( actions ) { + var i, len, index, action; + + this.changing = true; + for ( i = 0, len = actions.length; i < len; i++ ) { + action = actions[i]; + index = this.list.indexOf( action ); + if ( index !== -1 ) { + action.disconnect( this ); + this.list.splice( index, 1 ); + } + } + this.organized = false; + this.emit( 'remove', actions ); + this.changing = false; + this.emit( 'change' ); + + return this; +}; + +/** + * Remove all actions. + * + * @chainable + * @fires remove + * @fires change + */ +OO.ui.ActionSet.prototype.clear = function () { + var i, len, action, + removed = this.list.slice(); + + this.changing = true; + for ( i = 0, len = this.list.length; i < len; i++ ) { + action = this.list[i]; + action.disconnect( this ); + } + + this.list = []; + + this.organized = false; + this.emit( 'remove', removed ); + this.changing = false; + this.emit( 'change' ); + + return this; +}; + +/** + * Organize actions. + * + * This is called whenver organized information is requested. It will only reorganize the actions + * if something has changed since the last time it ran. + * + * @private + * @chainable + */ +OO.ui.ActionSet.prototype.organize = function () { + var i, iLen, j, jLen, flag, action, category, list, item, special, + specialFlags = this.constructor.static.specialFlags; + + if ( !this.organized ) { + this.categorized = {}; + this.special = {}; + this.others = []; + for ( i = 0, iLen = this.list.length; i < iLen; i++ ) { + action = this.list[i]; + if ( action.isVisible() ) { + // Populate catgeories + for ( category in this.categories ) { + if ( !this.categorized[category] ) { + this.categorized[category] = {}; + } + list = action[this.categories[category]](); + if ( !Array.isArray( list ) ) { + list = [ list ]; + } + for ( j = 0, jLen = list.length; j < jLen; j++ ) { + item = list[j]; + if ( !this.categorized[category][item] ) { + this.categorized[category][item] = []; + } + this.categorized[category][item].push( action ); + } + } + // Populate special/others + special = false; + for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) { + flag = specialFlags[j]; + if ( !this.special[flag] && action.hasFlag( flag ) ) { + this.special[flag] = action; + special = true; + break; + } + } + if ( !special ) { + this.others.push( action ); + } + } + } + this.organized = true; + } + + return this; +}; + /** * DOM element abstraction. * @@ -851,7 +1273,8 @@ OO.ui.Frame.static.transplantStyles = function ( parentDoc, frameDoc, timeout ) * @fires load */ OO.ui.Frame.prototype.load = function () { - var win, doc; + var win, doc, + frame = this; // Return existing promise if already loading or loaded if ( this.loading ) { @@ -872,8 +1295,7 @@ OO.ui.Frame.prototype.load = function () { doc.write( '' + '' + - '' + - '
' + + '' + '' + '' ); @@ -886,10 +1308,10 @@ OO.ui.Frame.prototype.load = function () { // Initialization this.constructor.static.transplantStyles( this.getElementDocument(), this.$document[0] ) - .always( OO.ui.bind( function () { - this.emit( 'load' ); - this.loading.resolve(); - }, this ) ); + .always( function () { + frame.emit( 'load' ); + frame.loading.resolve(); + } ); return this.loading.promise(); }; @@ -907,10 +1329,7 @@ OO.ui.Frame.prototype.setSize = function ( width, height ) { }; /** - * Container for elements in a child frame. - * - * There are two ways to specify a title: set the static `title` property or provide a `title` - * property in the configuration options. The latter will override the former. + * Container for elements. * * @abstract * @class @@ -919,305 +1338,369 @@ OO.ui.Frame.prototype.setSize = function ( width, height ) { * * @constructor * @param {Object} [config] Configuration options - * @cfg {string|Function} [title] Title string or function that returns a string - * @cfg {string} [icon] Symbolic name of icon - * @fires initialize */ -OO.ui.Window = function OoUiWindow( config ) { - var element = this; +OO.ui.Layout = function OoUiLayout( config ) { + // Initialize config + config = config || {}; + // Parent constructor - OO.ui.Window.super.call( this, config ); + OO.ui.Layout.super.call( this, config ); // Mixin constructors OO.EventEmitter.call( this ); - // Properties - this.visible = false; - this.opening = null; - this.closing = null; - this.opened = null; - this.title = OO.ui.resolveMsg( config.title || this.constructor.static.title ); - this.icon = config.icon || this.constructor.static.icon; - this.frame = new OO.ui.Frame( { '$': this.$ } ); - this.$frame = this.$( '
' ); - this.$ = function () { - throw new Error( 'this.$() cannot be used until the frame has been initialized.' ); - }; - // Initialization - this.$element - .addClass( 'oo-ui-window' ) - // Hide the window using visibility: hidden; while the iframe is still loading - // Can't use display: none; because that prevents the iframe from loading in Firefox - .css( 'visibility', 'hidden' ) - .append( this.$frame ); - this.$frame - .addClass( 'oo-ui-window-frame' ) - .append( this.frame.$element ); - - // Events - this.frame.on( 'load', function () { - element.initialize(); - // Undo the visibility: hidden; hack and apply display: none; - // We can do this safely now that the iframe has initialized - // (don't do this from within #initialize because it has to happen - // after the all subclasses have been handled as well). - element.$element.hide().css( 'visibility', '' ); - } ); + this.$element.addClass( 'oo-ui-layout' ); }; /* Setup */ -OO.inheritClass( OO.ui.Window, OO.ui.Element ); -OO.mixinClass( OO.ui.Window, OO.EventEmitter ); - -/* Events */ +OO.inheritClass( OO.ui.Layout, OO.ui.Element ); +OO.mixinClass( OO.ui.Layout, OO.EventEmitter ); /** - * Window is setup. + * User interface control. * - * Fired after the setup process has been executed. + * @abstract + * @class + * @extends OO.ui.Element + * @mixins OO.EventEmitter * - * @event setup - * @param {Object} data Window opening data + * @constructor + * @param {Object} [config] Configuration options + * @cfg {boolean} [disabled=false] Disable */ +OO.ui.Widget = function OoUiWidget( config ) { + // Initialize config + config = $.extend( { 'disabled': false }, config ); + + // Parent constructor + OO.ui.Widget.super.call( this, config ); + + // Mixin constructors + OO.EventEmitter.call( this ); + + // Properties + this.visible = true; + this.disabled = null; + this.wasDisabled = null; + + // Initialization + this.$element.addClass( 'oo-ui-widget' ); + this.setDisabled( !!config.disabled ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.Widget, OO.ui.Element ); +OO.mixinClass( OO.ui.Widget, OO.EventEmitter ); + +/* Events */ /** - * Window is ready. - * - * Fired after the ready process has been executed. - * - * @event ready - * @param {Object} data Window opening data + * @event disable + * @param {boolean} disabled Widget is disabled */ /** - * Window is torn down - * - * Fired after the teardown process has been executed. - * - * @event teardown - * @param {Object} data Window closing data + * @event toggle + * @param {boolean} visible Widget is visible */ -/* Static Properties */ +/* Methods */ /** - * Symbolic name of icon. + * Check if the widget is disabled. * - * @static - * @inheritable - * @property {string} + * @param {boolean} Button is disabled */ -OO.ui.Window.static.icon = 'window'; +OO.ui.Widget.prototype.isDisabled = function () { + return this.disabled; +}; /** - * Window title. + * Check if widget is visible. * - * Subclasses must implement this property before instantiating the window. - * Alternatively, override #getTitle with an alternative implementation. + * @return {boolean} Widget is visible + */ +OO.ui.Widget.prototype.isVisible = function () { + return this.visible; +}; + +/** + * Set the disabled state of the widget. * - * @static - * @abstract - * @inheritable - * @property {string|Function} Title string or function that returns a string + * This should probably change the widgets' appearance and prevent it from being used. + * + * @param {boolean} disabled Disable widget + * @chainable */ -OO.ui.Window.static.title = null; +OO.ui.Widget.prototype.setDisabled = function ( disabled ) { + var isDisabled; -/* Methods */ + this.disabled = !!disabled; + isDisabled = this.isDisabled(); + if ( isDisabled !== this.wasDisabled ) { + this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled ); + this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled ); + this.emit( 'disable', isDisabled ); + } + this.wasDisabled = isDisabled; + + return this; +}; /** - * Check if window is visible. + * Toggle visibility of widget. * - * @return {boolean} Window is visible + * @param {boolean} [show] Make widget visible, omit to toggle visibility + * @fires visible + * @chainable */ -OO.ui.Window.prototype.isVisible = function () { - return this.visible; +OO.ui.Widget.prototype.toggle = function ( show ) { + show = show === undefined ? !this.visible : !!show; + + if ( show !== this.isVisible() ) { + this.visible = show; + this.$element.toggle( show ); + this.emit( 'toggle', show ); + } + + return this; }; /** - * Check if window is opening. + * Update the disabled state, in case of changes in parent widget. * - * @return {boolean} Window is opening + * @chainable */ -OO.ui.Window.prototype.isOpening = function () { - return !!this.opening && this.opening.state() === 'pending'; +OO.ui.Widget.prototype.updateDisabled = function () { + this.setDisabled( this.disabled ); + return this; }; /** - * Check if window is closing. + * Container for elements in a child frame. * - * @return {boolean} Window is closing + * Use together with OO.ui.WindowManager. + * + * @abstract + * @class + * @extends OO.ui.Element + * @mixins OO.EventEmitter + * + * When a window is opened, the setup and ready processes are executed. Similarly, the hold and + * teardown processes are executed when the window is closed. + * + * - {@link OO.ui.WindowManager#openWindow} or {@link #open} methods are used to start opening + * - Window manager begins opening window + * - {@link #getSetupProcess} method is called and its result executed + * - {@link #getReadyProcess} method is called and its result executed + * - Window is now open + * + * - {@link OO.ui.WindowManager#closeWindow} or {@link #close} methods are used to start closing + * - Window manager begins closing window + * - {@link #getHoldProcess} method is called and its result executed + * - {@link #getTeardownProcess} method is called and its result executed + * - Window is now closed + * + * Each process (setup, ready, hold and teardown) can be extended in subclasses by overriding + * {@link #getSetupProcess}, {@link #getReadyProcess}, {@link #getHoldProcess} and + * {@link #getTeardownProcess} respectively. Each process is executed in series, so asynchonous + * processing can complete. Always assume window processes are executed asychronously. See + * OO.ui.Process for more details about how to work with processes. Some events, as well as the + * #open and #close methods, provide promises which are resolved when the window enters a new state. + * + * Sizing of windows is specified using symbolic names which are interpreted by the window manager. + * If the requested size is not recognized, the window manager will choose a sensible fallback. + * + * @constructor + * @param {OO.ui.WindowManager} manager Manager of window + * @param {Object} [config] Configuration options + * @cfg {string} [size] Symbolic name of dialog size, `small`, `medium`, `large` or `full`; omit to + * use #static-size + * @fires initialize */ -OO.ui.Window.prototype.isClosing = function () { - return !!this.closing && this.closing.state() === 'pending'; +OO.ui.Window = function OoUiWindow( manager, config ) { + var win = this; + + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.Window.super.call( this, config ); + + // Mixin constructors + OO.EventEmitter.call( this ); + + if ( !( manager instanceof OO.ui.WindowManager ) ) { + throw new Error( 'Cannot construct window: window must have a manager' ); + } + + // Properties + this.manager = manager; + this.initialized = false; + this.visible = false; + this.opening = null; + this.closing = null; + this.opened = null; + this.timing = null; + this.size = config.size || this.constructor.static.size; + this.frame = new OO.ui.Frame( { '$': this.$ } ); + this.$frame = this.$( '
' ); + this.$ = function () { + throw new Error( 'this.$() cannot be used until the frame has been initialized.' ); + }; + + // Initialization + this.$element + .addClass( 'oo-ui-window' ) + // Hide the window using visibility: hidden; while the iframe is still loading + // Can't use display: none; because that prevents the iframe from loading in Firefox + .css( 'visibility', 'hidden' ) + .append( this.$frame ); + this.$frame + .addClass( 'oo-ui-window-frame' ) + .append( this.frame.$element ); + + // Events + this.frame.on( 'load', function () { + win.initialize(); + win.initialized = true; + // Undo the visibility: hidden; hack and apply display: none; + // We can do this safely now that the iframe has initialized + // (don't do this from within #initialize because it has to happen + // after the all subclasses have been handled as well). + win.$element.hide().css( 'visibility', '' ); + } ); }; +/* Setup */ + +OO.inheritClass( OO.ui.Window, OO.ui.Element ); +OO.mixinClass( OO.ui.Window, OO.EventEmitter ); + +/* Events */ + /** - * Check if window is opened. + * @event resize + * @param {string} size Symbolic size name, e.g. 'small', 'medium', 'large', 'full' + */ + +/* Static Properties */ + +/** + * Symbolic name of size. * - * @return {boolean} Window is opened + * Size is used if no size is configured during construction. + * + * @static + * @inheritable + * @property {string} */ -OO.ui.Window.prototype.isOpened = function () { - return !!this.opened && this.opened.state() === 'pending'; -}; +OO.ui.Window.static.size = 'medium'; + +/* Methods */ /** - * Get the window frame. + * Check if window has been initialized. * - * @return {OO.ui.Frame} Frame of window + * @return {boolean} Window has been initialized */ -OO.ui.Window.prototype.getFrame = function () { - return this.frame; +OO.ui.Window.prototype.isInitialized = function () { + return this.initialized; }; /** - * Get the title of the window. + * Check if window is visible. * - * @return {string} Title text + * @return {boolean} Window is visible */ -OO.ui.Window.prototype.getTitle = function () { - return this.title; +OO.ui.Window.prototype.isVisible = function () { + return this.visible; }; /** - * Get the window icon. + * Check if window is opening. + * + * This is a wrapper around OO.ui.WindowManager#isOpening. * - * @return {string} Symbolic name of icon + * @return {boolean} Window is opening */ -OO.ui.Window.prototype.getIcon = function () { - return this.icon; +OO.ui.Window.prototype.isOpening = function () { + return this.manager.isOpening( this ); }; /** - * Set the size of window frame. + * Check if window is closing. * - * @param {number} [width=auto] Custom width - * @param {number} [height=auto] Custom height - * @chainable + * This is a wrapper around OO.ui.WindowManager#isClosing. + * + * @return {boolean} Window is closing */ -OO.ui.Window.prototype.setSize = function ( width, height ) { - if ( !this.frame.$content ) { - return; - } - - this.frame.$element.css( { - 'width': width === undefined ? 'auto' : width, - 'height': height === undefined ? 'auto' : height - } ); - - return this; +OO.ui.Window.prototype.isClosing = function () { + return this.manager.isClosing( this ); }; /** - * Set the title of the window. + * Check if window is opened. * - * @param {string|Function} title Title text or a function that returns text - * @chainable + * This is a wrapper around OO.ui.WindowManager#isOpened. + * + * @return {boolean} Window is opened */ -OO.ui.Window.prototype.setTitle = function ( title ) { - this.title = OO.ui.resolveMsg( title ); - if ( this.$title ) { - this.$title.text( title ); - } - return this; +OO.ui.Window.prototype.isOpened = function () { + return this.manager.isOpened( this ); }; /** - * Set the icon of the window. + * Get the window manager. * - * @param {string} icon Symbolic name of icon - * @chainable + * @return {OO.ui.WindowManager} Manager of window */ -OO.ui.Window.prototype.setIcon = function ( icon ) { - if ( this.$icon ) { - this.$icon.removeClass( 'oo-ui-icon-' + this.icon ); - } - this.icon = icon; - if ( this.$icon ) { - this.$icon.addClass( 'oo-ui-icon-' + this.icon ); - } - - return this; +OO.ui.Window.prototype.getManager = function () { + return this.manager; }; /** - * Set the position of window to fit with contents. + * Get the window frame. * - * @param {string} left Left offset - * @param {string} top Top offset - * @chainable + * @return {OO.ui.Frame} Frame of window */ -OO.ui.Window.prototype.setPosition = function ( left, top ) { - this.$element.css( { 'left': left, 'top': top } ); - return this; +OO.ui.Window.prototype.getFrame = function () { + return this.frame; }; /** - * Set the height of window to fit with contents. + * Get the window size. * - * @param {number} [min=0] Min height - * @param {number} [max] Max height (defaults to content's outer height) - * @chainable + * @return {string} Symbolic size name, e.g. 'small', 'medium', 'large', 'full' */ -OO.ui.Window.prototype.fitHeightToContents = function ( min, max ) { - var height = this.frame.$content.outerHeight(); - - this.frame.$element.css( - 'height', Math.max( min || 0, max === undefined ? height : Math.min( max, height ) ) - ); - - return this; +OO.ui.Window.prototype.getSize = function () { + return this.size; }; /** - * Set the width of window to fit with contents. + * Get the height of the dialog contents. * - * @param {number} [min=0] Min height - * @param {number} [max] Max height (defaults to content's outer width) - * @chainable + * @return {number} Content height */ -OO.ui.Window.prototype.fitWidthToContents = function ( min, max ) { - var width = this.frame.$content.outerWidth(); - - this.frame.$element.css( - 'width', Math.max( min || 0, max === undefined ? width : Math.min( max, width ) ) +OO.ui.Window.prototype.getContentHeight = function () { + return Math.round( + // Add buffer for border + ( ( this.$frame.outerHeight() - this.$frame.innerHeight() ) * 2 ) + + // Height of contents + ( this.$head.outerHeight( true ) + this.getBodyHeight() + this.$foot.outerHeight( true ) ) ); - - return this; }; /** - * Initialize window contents. - * - * The first time the window is opened, #initialize is called when it's safe to begin populating - * its contents. See #setup for a way to make changes each time the window opens. - * - * Once this method is called, this.$$ can be used to create elements within the frame. + * Get the height of the dialog contents. * - * @chainable + * @return {number} Height of content */ -OO.ui.Window.prototype.initialize = function () { - // Properties - this.$ = this.frame.$; - this.$title = this.$( '
' ) - .text( this.title ); - this.$icon = this.$( '
' ) - .addClass( 'oo-ui-icon-' + this.icon ); - this.$head = this.$( '
' ); - this.$body = this.$( '
' ); - this.$foot = this.$( '
' ); - this.$overlay = this.$( '
' ); - - // Initialization - this.frame.$content.append( - this.$head.append( this.$icon, this.$title ), - this.$body, - this.$foot, - this.$overlay - ); - - return this; +OO.ui.Window.prototype.getBodyHeight = function () { + return this.$body[0].scrollHeight; }; /** @@ -1255,322 +1738,298 @@ OO.ui.Window.prototype.getReadyProcess = function () { }; /** - * Get a process for tearing down a window after use. + * Get a process for holding a window from use. * - * Each time the window is closed this process will tear it down and do something with the user's - * interactions within the window, based on the `data` argument. + * Each time the window is closed, this process will hold it from use in a particular context, based + * on the `data` argument. * - * When you override this method, you can add additional teardown steps to the process the parent + * When you override this method, you can add additional setup steps to the process the parent * method provides using the 'first' and 'next' methods. * * @abstract * @param {Object} [data] Window closing data - * @return {OO.ui.Process} Teardown process + * @return {OO.ui.Process} Hold process */ -OO.ui.Window.prototype.getTeardownProcess = function () { +OO.ui.Window.prototype.getHoldProcess = function () { return new OO.ui.Process(); }; /** - * Open window. + * Get a process for tearing down a window after use. * - * Do not override this method. Use #getSetupProcess to do something each time the window closes. + * Each time the window is closed this process will tear it down and do something with the user's + * interactions within the window, based on the `data` argument. * - * @param {Object} [data] Window opening data - * @fires initialize - * @fires opening - * @fires open - * @fires ready - * @return {jQuery.Promise} Promise resolved when window is opened; when the promise is resolved the - * first argument will be a promise which will be resolved when the window begins closing + * When you override this method, you can add additional teardown steps to the process the parent + * method provides using the 'first' and 'next' methods. + * + * @abstract + * @param {Object} [data] Window closing data + * @return {OO.ui.Process} Teardown process */ -OO.ui.Window.prototype.open = function ( data ) { - // Return existing promise if already opening or open - if ( this.opening ) { - return this.opening.promise(); - } - - // Open the window - this.opening = $.Deferred(); - - this.$ariaHidden = $( 'body' ).children().not( this.$element.parentsUntil( 'body' ).last() ) - .attr( 'aria-hidden', '' ); - - this.frame.load().done( OO.ui.bind( function () { - this.$element.show(); - this.visible = true; - this.getSetupProcess( data ).execute().done( OO.ui.bind( function () { - this.$element.addClass( 'oo-ui-window-setup' ); - this.emit( 'setup', data ); - setTimeout( OO.ui.bind( function () { - this.frame.$content.focus(); - this.getReadyProcess( data ).execute().done( OO.ui.bind( function () { - this.$element.addClass( 'oo-ui-window-ready' ); - this.emit( 'ready', data ); - this.opened = $.Deferred(); - // Now that we are totally done opening, it's safe to allow closing - this.closing = null; - this.opening.resolve( this.opened.promise() ); - }, this ) ); - }, this ) ); - }, this ) ); - }, this ) ); - - return this.opening.promise(); +OO.ui.Window.prototype.getTeardownProcess = function () { + return new OO.ui.Process(); }; /** - * Close window. - * - * Do not override this method. Use #getTeardownProcess to do something each time the window closes. + * Set the window size. * - * @param {Object} [data] Window closing data - * @fires closing - * @fires close - * @return {jQuery.Promise} Promise resolved when window is closed + * @param {string} size Symbolic size name, e.g. 'small', 'medium', 'large', 'full' + * @chainable */ -OO.ui.Window.prototype.close = function ( data ) { - var close; - - // Return existing promise if already closing or closed - if ( this.closing ) { - return this.closing.promise(); - } - - // Close after opening is done if opening is in progress - if ( this.opening && this.opening.state() === 'pending' ) { - close = OO.ui.bind( function () { - return this.close( data ); - }, this ); - return this.opening.then( close, close ); - } - - // Close the window - // This.closing needs to exist before we emit the closing event so that handlers can call - // window.close() and trigger the safety check above - this.closing = $.Deferred(); - this.frame.$content.find( ':focus' ).blur(); - this.$element.removeClass( 'oo-ui-window-ready' ); - this.getTeardownProcess( data ).execute().done( OO.ui.bind( function () { - this.$element.removeClass( 'oo-ui-window-setup' ); - this.emit( 'teardown', data ); - // To do something different with #opened, resolve/reject #opened in the teardown process - if ( this.opened && this.opened.state() === 'pending' ) { - this.opened.resolve(); - } - this.$element.hide(); - if ( this.$ariaHidden ) { - this.$ariaHidden.removeAttr( 'aria-hidden' ); - this.$ariaHidden = undefined; - } - this.visible = false; - this.closing.resolve(); - // Now that we are totally done closing, it's safe to allow opening - this.opening = null; - }, this ) ); - - return this.closing.promise(); +OO.ui.Window.prototype.setSize = function ( size ) { + this.size = size; + this.manager.updateWindowSize( this ); + return this; }; /** - * Set of mutually exclusive windows. + * Set window dimensions. * - * @class - * @extends OO.ui.Element - * @mixins OO.EventEmitter + * Properties are applied to the frame container. * - * @constructor - * @param {OO.Factory} factory Window factory - * @param {Object} [config] Configuration options + * @param {Object} dim CSS dimension properties + * @param {string|number} [dim.width] Width + * @param {string|number} [dim.minWidth] Minimum width + * @param {string|number} [dim.maxWidth] Maximum width + * @param {string|number} [dim.width] Height, omit to set based on height of contents + * @param {string|number} [dim.minWidth] Minimum height + * @param {string|number} [dim.maxWidth] Maximum height + * @chainable */ -OO.ui.WindowSet = function OoUiWindowSet( factory, config ) { - // Parent constructor - OO.ui.WindowSet.super.call( this, config ); - - // Mixin constructors - OO.EventEmitter.call( this ); - - // Properties - this.factory = factory; - - /** - * List of all windows associated with this window set. - * - * @property {OO.ui.Window[]} - */ - this.windowList = []; - - /** - * Mapping of OO.ui.Window objects created by name from the #factory. - * - * @property {Object} - */ - this.windows = {}; - this.currentWindow = null; - - // Initialization - this.$element.addClass( 'oo-ui-windowSet' ); +OO.ui.Window.prototype.setDimensions = function ( dim ) { + // Apply width before height so height is not based on wrapping content using the wrong width + this.$frame.css( { + 'width': dim.width || '', + 'min-width': dim.minWidth || '', + 'max-width': dim.maxWidth || '' + } ); + this.$frame.css( { + 'height': ( dim.height !== undefined ? dim.height : this.getContentHeight() ) || '', + 'min-height': dim.minHeight || '', + 'max-height': dim.maxHeight || '' + } ); + return this; }; -/* Setup */ - -OO.inheritClass( OO.ui.WindowSet, OO.ui.Element ); -OO.mixinClass( OO.ui.WindowSet, OO.EventEmitter ); - -/* Events */ - -/** - * @event setup - * @param {OO.ui.Window} win Window that's been setup - * @param {Object} config Window opening information - */ - /** - * @event ready - * @param {OO.ui.Window} win Window that's ready - * @param {Object} config Window opening information + * Initialize window contents. + * + * The first time the window is opened, #initialize is called when it's safe to begin populating + * its contents. See #getSetupProcess for a way to make changes each time the window opens. + * + * Once this method is called, this.$ can be used to create elements within the frame. + * + * @chainable */ +OO.ui.Window.prototype.initialize = function () { + // Properties + this.$ = this.frame.$; + this.$head = this.$( '
' ); + this.$body = this.$( '
' ); + this.$foot = this.$( '
' ); + this.$overlay = this.$( '
' ); -/** - * @event teardown - * @param {OO.ui.Window} win Window that's been torn down - * @param {Object} config Window closing information - */ + // Initialization + this.$head.addClass( 'oo-ui-window-head' ); + this.$body.addClass( 'oo-ui-window-body' ); + this.$foot.addClass( 'oo-ui-window-foot' ); + this.$overlay.addClass( 'oo-ui-window-overlay' ); + this.frame.$content + .addClass( 'oo-ui-window-content' ) + .append( this.$head, this.$body, this.$foot, this.$overlay ); -/* Methods */ + return this; +}; /** - * Handle a window setup event. + * Open window. * - * @param {OO.ui.Window} win Window that's been setup - * @param {Object} [config] Window opening information - * @fires setup + * This is a wrapper around calling {@link OO.ui.WindowManager#openWindow} on the window manager. + * To do something each time the window opens, use #getSetupProcess or #getReadyProcess. + * + * @param {Object} [data] Window opening data + * @return {jQuery.Promise} Promise resolved when window is opened; when the promise is resolved the + * first argument will be a promise which will be resolved when the window begins closing */ -OO.ui.WindowSet.prototype.onWindowSetup = function ( win, config ) { - if ( this.currentWindow && this.currentWindow !== win ) { - this.currentWindow.close(); - } - this.currentWindow = win; - this.emit( 'setup', win, config ); +OO.ui.Window.prototype.open = function ( data ) { + return this.manager.openWindow( this, data ); }; /** - * Handle a window ready event. + * Close window. + * + * This is a wrapper around calling OO.ui.WindowManager#closeWindow on the window manager. + * To do something each time the window closes, use #getHoldProcess or #getTeardownProcess. * - * @param {OO.ui.Window} win Window that's ready - * @param {Object} [config] Window opening information - * @fires ready + * @param {Object} [data] Window closing data + * @return {jQuery.Promise} Promise resolved when window is closed */ -OO.ui.WindowSet.prototype.onWindowReady = function ( win, config ) { - this.emit( 'ready', win, config ); +OO.ui.Window.prototype.close = function ( data ) { + return this.manager.closeWindow( this, data ); }; /** - * Handle a window teardown event. + * Load window. + * + * This is called by OO.ui.WindowManager durring window adding, and should not be called directly + * by other systems. * - * @param {OO.ui.Window} win Window that's been torn down - * @param {Object} [config] Window closing information - * @fires teardown + * @return {jQuery.Promise} Promise resolved when window is loaded */ -OO.ui.WindowSet.prototype.onWindowTeardown = function ( win, config ) { - this.currentWindow = null; - this.emit( 'teardown', win, config ); +OO.ui.Window.prototype.load = function () { + return this.frame.load(); }; /** - * Get the current window. + * Setup window. + * + * This is called by OO.ui.WindowManager durring window opening, and should not be called directly + * by other systems. * - * @return {OO.ui.Window|null} Current window or null if none open + * @param {Object} [data] Window opening data + * @return {jQuery.Promise} Promise resolved when window is setup */ -OO.ui.WindowSet.prototype.getCurrentWindow = function () { - return this.currentWindow; +OO.ui.Window.prototype.setup = function ( data ) { + var win = this, + deferred = $.Deferred(); + + this.$element.show(); + this.visible = true; + this.getSetupProcess( data ).execute().done( function () { + win.manager.updateWindowSize( win ); + // Force redraw by asking the browser to measure the elements' widths + win.$element.addClass( 'oo-ui-window-setup' ).width(); + win.frame.$content.addClass( 'oo-ui-window-content-setup' ).width(); + deferred.resolve(); + } ); + + return deferred.promise(); }; /** - * Return a given window. + * Ready window. * - * @param {string} name Symbolic name of window - * @return {OO.ui.Window} Window with specified name + * This is called by OO.ui.WindowManager durring window opening, and should not be called directly + * by other systems. + * + * @param {Object} [data] Window opening data + * @return {jQuery.Promise} Promise resolved when window is ready */ -OO.ui.WindowSet.prototype.getWindow = function ( name ) { - var win; +OO.ui.Window.prototype.ready = function ( data ) { + var win = this, + deferred = $.Deferred(); - if ( !this.factory.lookup( name ) ) { - throw new Error( 'Unknown window: ' + name ); - } - if ( !( name in this.windows ) ) { - win = this.windows[name] = this.createWindow( name ); - this.addWindow( win ); - } - return this.windows[name]; + this.frame.$content.focus(); + this.getReadyProcess( data ).execute().done( function () { + // Force redraw by asking the browser to measure the elements' widths + win.$element.addClass( 'oo-ui-window-ready' ).width(); + win.frame.$content.addClass( 'oo-ui-window-content-ready' ).width(); + deferred.resolve(); + } ); + + return deferred.promise(); }; /** - * Create a window for use in this window set. + * Hold window. + * + * This is called by OO.ui.WindowManager durring window closing, and should not be called directly + * by other systems. * - * @param {string} name Symbolic name of window - * @return {OO.ui.Window} Window with specified name + * @param {Object} [data] Window closing data + * @return {jQuery.Promise} Promise resolved when window is held */ -OO.ui.WindowSet.prototype.createWindow = function ( name ) { - return this.factory.create( name, { '$': this.$ } ); +OO.ui.Window.prototype.hold = function ( data ) { + var win = this, + deferred = $.Deferred(); + + this.getHoldProcess( data ).execute().done( function () { + win.frame.$content.find( ':focus' ).blur(); + // Force redraw by asking the browser to measure the elements' widths + win.$element.removeClass( 'oo-ui-window-ready' ).width(); + win.frame.$content.removeClass( 'oo-ui-window-content-ready' ).width(); + deferred.resolve(); + } ); + + return deferred.promise(); }; /** - * Add a given window to this window set. + * Teardown window. * - * Connects event handlers and attaches it to the DOM. Calling - * OO.ui.Window#open will not work until the window is added to the set. + * This is called by OO.ui.WindowManager durring window closing, and should not be called directly + * by other systems. * - * @param {OO.ui.Window} win Window to add + * @param {Object} [data] Window closing data + * @return {jQuery.Promise} Promise resolved when window is torn down */ -OO.ui.WindowSet.prototype.addWindow = function ( win ) { - if ( this.windowList.indexOf( win ) !== -1 ) { - // Already set up - return; - } - this.windowList.push( win ); +OO.ui.Window.prototype.teardown = function ( data ) { + var win = this, + deferred = $.Deferred(); - win.connect( this, { - 'setup': [ 'onWindowSetup', win ], - 'ready': [ 'onWindowReady', win ], - 'teardown': [ 'onWindowTeardown', win ] + this.getTeardownProcess( data ).execute().done( function () { + // Force redraw by asking the browser to measure the elements' widths + win.$element.removeClass( 'oo-ui-window-setup' ).width(); + win.frame.$content.removeClass( 'oo-ui-window-content-setup' ).width(); + win.$element.hide(); + win.visible = false; + deferred.resolve(); } ); - this.$element.append( win.$element ); + + return deferred.promise(); }; /** - * Modal dialog window. + * Base class for all dialogs. + * + * Logic: + * - Manage the window (open and close, etc.). + * - Store the internal name and display title. + * - A stack to track one or more pending actions. + * - Manage a set of actions that can be performed. + * - Configure and create action widgets. + * + * User interface: + * - Close the dialog with Escape key. + * - Visually lock the dialog while an action is in + * progress (aka "pending"). + * + * Subclass responsibilities: + * - Display the title somewhere. + * - Add content to the dialog. + * - Provide a UI to close the dialog. + * - Display the action widgets somewhere. * * @abstract * @class * @extends OO.ui.Window + * @mixins OO.ui.LabeledElement * * @constructor * @param {Object} [config] Configuration options - * @cfg {boolean} [footless] Hide foot - * @cfg {string} [size='large'] Symbolic name of dialog size, `small`, `medium` or `large` */ -OO.ui.Dialog = function OoUiDialog( config ) { - // Configuration initialization - config = $.extend( { 'size': 'large' }, config ); - +OO.ui.Dialog = function OoUiDialog( manager, config ) { // Parent constructor - OO.ui.Dialog.super.call( this, config ); + OO.ui.Dialog.super.call( this, manager, config ); // Properties - this.visible = false; - this.footless = !!config.footless; - this.size = null; + this.actions = new OO.ui.ActionSet(); + this.attachedActions = []; + this.currentAction = null; this.pending = 0; - this.onWindowMouseWheelHandler = OO.ui.bind( this.onWindowMouseWheel, this ); - this.onDocumentKeyDownHandler = OO.ui.bind( this.onDocumentKeyDown, this ); // Events - this.$element.on( 'mousedown', false ); + this.actions.connect( this, { + 'click': 'onActionClick', + 'resize': 'onActionResize', + 'change': 'onActionsChange' + } ); // Initialization - this.$element.addClass( 'oo-ui-dialog' ).attr( 'role', 'dialog' ); - this.setSize( config.size ); + this.$element + .addClass( 'oo-ui-dialog' ) + .attr( 'role', 'dialog' ); }; /* Setup */ @@ -1590,55 +2049,35 @@ OO.inheritClass( OO.ui.Dialog, OO.ui.Window ); OO.ui.Dialog.static.name = ''; /** - * Map of symbolic size names and CSS classes. + * Dialog title. * + * @abstract * @static * @inheritable - * @property {Object} - */ -OO.ui.Dialog.static.sizeCssClasses = { - 'small': 'oo-ui-dialog-small', - 'medium': 'oo-ui-dialog-medium', - 'large': 'oo-ui-dialog-large' -}; - -/* Methods */ - -/** - * Handle close button click events. + * @property {jQuery|string|Function} Label nodes, text or a function that returns nodes or text */ -OO.ui.Dialog.prototype.onCloseButtonClick = function () { - this.close( { 'action': 'cancel' } ); -}; +OO.ui.Dialog.static.title = ''; /** - * Handle window mouse wheel events. + * List of OO.ui.ActionWidget configuration options. * - * @param {jQuery.Event} e Mouse wheel event + * @static + * inheritable + * @property {Object[]} */ -OO.ui.Dialog.prototype.onWindowMouseWheel = function () { - return false; -}; +OO.ui.Dialog.static.actions = []; /** - * Handle document key down events. + * Close dialog when the escape key is pressed. * - * @param {jQuery.Event} e Key down event + * @static + * @abstract + * @inheritable + * @property {boolean} */ -OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) { - switch ( e.which ) { - case OO.ui.Keys.PAGEUP: - case OO.ui.Keys.PAGEDOWN: - case OO.ui.Keys.END: - case OO.ui.Keys.HOME: - case OO.ui.Keys.LEFT: - case OO.ui.Keys.UP: - case OO.ui.Keys.RIGHT: - case OO.ui.Keys.DOWN: - // Prevent any key events that might cause scrolling - return false; - } -}; +OO.ui.Dialog.static.escapable = true; + +/* Methods */ /** * Handle frame document key down events. @@ -1647,68 +2086,109 @@ OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) { */ OO.ui.Dialog.prototype.onFrameDocumentKeyDown = function ( e ) { if ( e.which === OO.ui.Keys.ESCAPE ) { - this.close( { 'action': 'cancel' } ); + this.close(); return false; } }; /** - * Set dialog size. + * Handle action resized events. * - * @param {string} [size='large'] Symbolic name of dialog size, `small`, `medium` or `large` + * @param {OO.ui.ActionWidget} action Action that was resized */ -OO.ui.Dialog.prototype.setSize = function ( size ) { - var name, state, cssClass, - sizeCssClasses = OO.ui.Dialog.static.sizeCssClasses; +OO.ui.Dialog.prototype.onActionResize = function () { + // Override in subclass +}; - if ( !sizeCssClasses[size] ) { - size = 'large'; - } - this.size = size; - for ( name in sizeCssClasses ) { - state = name === size; - cssClass = sizeCssClasses[name]; - this.$element.toggleClass( cssClass, state ); +/** + * Handle action click events. + * + * @param {OO.ui.ActionWidget} action Action that was clicked + */ +OO.ui.Dialog.prototype.onActionClick = function ( action ) { + if ( !this.isPending() ) { + this.currentAction = action; + this.executeAction( action.getAction() ); } }; /** - * @inheritdoc + * Handle actions change event. */ -OO.ui.Dialog.prototype.initialize = function () { - // Parent method - OO.ui.Dialog.super.prototype.initialize.call( this ); +OO.ui.Dialog.prototype.onActionsChange = function () { + this.detachActions(); + if ( !this.isClosing() ) { + this.attachActions(); + } +}; - // Properties - this.closeButton = new OO.ui.ButtonWidget( { - '$': this.$, - 'frameless': true, - 'icon': 'close', - 'title': OO.ui.msg( 'ooui-dialog-action-close' ) - } ); +/** + * Check if input is pending. + * + * @return {boolean} + */ +OO.ui.Dialog.prototype.isPending = function () { + return !!this.pending; +}; - // Events - this.closeButton.connect( this, { 'click': 'onCloseButtonClick' } ); - this.frame.$document.on( 'keydown', OO.ui.bind( this.onFrameDocumentKeyDown, this ) ); +/** + * Get set of actions. + * + * @return {OO.ui.ActionSet} + */ +OO.ui.Dialog.prototype.getActions = function () { + return this.actions; +}; - // Initialization - this.frame.$content.addClass( 'oo-ui-dialog-content' ); - if ( this.footless ) { - this.frame.$content.addClass( 'oo-ui-dialog-content-footless' ); - } - this.closeButton.$element.addClass( 'oo-ui-window-closeButton' ); - this.$head.append( this.closeButton.$element ); +/** + * Get a process for taking action. + * + * When you override this method, you can add additional accept steps to the process the parent + * method provides using the 'first' and 'next' methods. + * + * @abstract + * @param {string} [action] Symbolic name of action + * @return {OO.ui.Process} Action process + */ +OO.ui.Dialog.prototype.getActionProcess = function ( action ) { + return new OO.ui.Process() + .next( function () { + if ( !action ) { + // An empty action always closes the dialog without data, which should always be + // safe and make no changes + this.close(); + } + }, this ); }; /** * @inheritdoc + * + * @param {Object} [data] Dialog opening data + * @param {jQuery|string|Function|null} [data.label] Dialog label, omit to use #static-label + * @param {Object[]} [data.actions] List of OO.ui.ActionWidget configuration options for each + * action item, omit to use #static-actions */ OO.ui.Dialog.prototype.getSetupProcess = function ( data ) { + data = data || {}; + + // Parent method return OO.ui.Dialog.super.prototype.getSetupProcess.call( this, data ) .next( function () { - // Prevent scrolling in top-level window - this.$( window ).on( 'mousewheel', this.onWindowMouseWheelHandler ); - this.$( document ).on( 'keydown', this.onDocumentKeyDownHandler ); + var i, len, + items = [], + config = this.constructor.static, + actions = data.actions !== undefined ? data.actions : config.actions; + + this.title.setLabel( + data.title !== undefined ? data.title : this.constructor.static.title + ); + for ( i = 0, len = actions.length; i < len; i++ ) { + items.push( + new OO.ui.ActionWidget( $.extend( { '$': this.$ }, actions[i] ) ) + ); + } + this.actions.add( items ); }, this ); }; @@ -1716,37 +2196,77 @@ OO.ui.Dialog.prototype.getSetupProcess = function ( data ) { * @inheritdoc */ OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) { + // Parent method return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data ) .first( function () { - // Wait for closing transition - return OO.ui.Process.static.delay( 250 ); - }, this ) - .next( function () { - // Allow scrolling in top-level window - this.$( window ).off( 'mousewheel', this.onWindowMouseWheelHandler ); - this.$( document ).off( 'keydown', this.onDocumentKeyDownHandler ); + this.actions.clear(); + this.currentAction = null; }, this ); }; /** - * Check if input is pending. - * - * @return {boolean} + * @inheritdoc */ -OO.ui.Dialog.prototype.isPending = function () { - return !!this.pending; +OO.ui.Dialog.prototype.initialize = function () { + // Parent method + OO.ui.Dialog.super.prototype.initialize.call( this ); + + // Properties + this.title = new OO.ui.LabelWidget( { '$': this.$ } ); + + // Events + if ( this.constructor.static.escapable ) { + this.frame.$document.on( 'keydown', OO.ui.bind( this.onFrameDocumentKeyDown, this ) ); + } + + // Initialization + this.frame.$content.addClass( 'oo-ui-dialog-content' ); }; /** - * Increase the pending stack. + * Attach action actions. + */ +OO.ui.Dialog.prototype.attachActions = function () { + // Remember the list of potentially attached actions + this.attachedActions = this.actions.get(); +}; + +/** + * Detach action actions. + * + * @chainable + */ +OO.ui.Dialog.prototype.detachActions = function () { + var i, len; + + // Detach all actions that may have been previously attached + for ( i = 0, len = this.attachedActions.length; i < len; i++ ) { + this.attachedActions[i].$element.detach(); + } + this.attachedActions = []; +}; + +/** + * Execute an action. + * + * @param {string} action Symbolic name of action to execute + * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails + */ +OO.ui.Dialog.prototype.executeAction = function ( action ) { + this.pushPending(); + return this.getActionProcess( action ).execute() + .always( OO.ui.bind( this.popPending, this ) ); +}; + +/** + * Increase the pending stack. * * @chainable */ OO.ui.Dialog.prototype.pushPending = function () { if ( this.pending === 0 ) { - this.frame.$content.addClass( 'oo-ui-dialog-pending' ); + this.frame.$content.addClass( 'oo-ui-actionDialog-content-pending' ); this.$head.addClass( 'oo-ui-texture-pending' ); - this.$foot.addClass( 'oo-ui-texture-pending' ); } this.pending++; @@ -1762,9 +2282,8 @@ OO.ui.Dialog.prototype.pushPending = function () { */ OO.ui.Dialog.prototype.popPending = function () { if ( this.pending === 1 ) { - this.frame.$content.removeClass( 'oo-ui-dialog-pending' ); + this.frame.$content.removeClass( 'oo-ui-actionDialog-content-pending' ); this.$head.removeClass( 'oo-ui-texture-pending' ); - this.$foot.removeClass( 'oo-ui-texture-pending' ); } this.pending = Math.max( 0, this.pending - 1 ); @@ -1772,2032 +2291,2617 @@ OO.ui.Dialog.prototype.popPending = function () { }; /** - * Container for elements. + * Collection of windows. * - * @abstract * @class * @extends OO.ui.Element * @mixins OO.EventEmitter * + * Managed windows are mutually exclusive. If a window is opened while there is a current window + * already opening or opened, the current window will be closed without data. Empty closing data + * should always result in the window being closed without causing constructive or destructive + * action. + * + * As a window is opened and closed, it passes through several stages and the manager emits several + * corresponding events. + * + * - {@link #openWindow} or {@link OO.ui.Window#open} methods are used to start opening + * - {@link #event-opening} is emitted with `opening` promise + * - {@link #getSetupDelay} is called the returned value is used to time a pause in execution + * - {@link OO.ui.Window#getSetupProcess} method is called on the window and its result executed + * - `setup` progress notification is emitted from opening promise + * - {@link #getReadyDelay} is called the returned value is used to time a pause in execution + * - {@link OO.ui.Window#getReadyProcess} method is called on the window and its result executed + * - `ready` progress notification is emitted from opening promise + * - `opening` promise is resolved with `opened` promise + * - Window is now open + * + * - {@link #closeWindow} or {@link OO.ui.Window#close} methods are used to start closing + * - `opened` promise is resolved with `closing` promise + * - {@link #event-opening} is emitted with `closing` promise + * - {@link #getHoldDelay} is called the returned value is used to time a pause in execution + * - {@link OO.ui.Window#getHoldProcess} method is called on the window and its result executed + * - `hold` progress notification is emitted from opening promise + * - {@link #getTeardownDelay} is called the returned value is used to time a pause in execution + * - {@link OO.ui.Window#getTeardownProcess} method is called on the window and its result executed + * - `teardown` progress notification is emitted from opening promise + * - Closing promise is resolved + * - Window is now closed + * * @constructor * @param {Object} [config] Configuration options + * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation + * @cfg {boolean} [modal=true] Prevent interaction outside the dialog */ -OO.ui.Layout = function OoUiLayout( config ) { - // Initialize config +OO.ui.WindowManager = function OoUiWindowManager( config ) { + // Configuration initialization config = config || {}; // Parent constructor - OO.ui.Layout.super.call( this, config ); + OO.ui.WindowManager.super.call( this, config ); // Mixin constructors OO.EventEmitter.call( this ); + // Properties + this.factory = config.factory; + this.modal = config.modal === undefined ? true : !!config.modal; + this.windows = {}; + this.opening = null; + this.opened = null; + this.closing = null; + this.size = null; + this.currentWindow = null; + this.$ariaHidden = null; + this.requestedSize = null; + this.onWindowResizeTimeout = null; + this.onWindowResizeHandler = OO.ui.bind( this.onWindowResize, this ); + this.afterWindowResizeHandler = OO.ui.bind( this.afterWindowResize, this ); + this.onWindowMouseWheelHandler = OO.ui.bind( this.onWindowMouseWheel, this ); + this.onDocumentKeyDownHandler = OO.ui.bind( this.onDocumentKeyDown, this ); + + // Events + this.$element.on( 'mousedown', false ); + // Initialization - this.$element.addClass( 'oo-ui-layout' ); + this.$element + .addClass( 'oo-ui-windowManager' ) + .toggleClass( 'oo-ui-windowManager-modal', this.modal ); }; /* Setup */ -OO.inheritClass( OO.ui.Layout, OO.ui.Element ); -OO.mixinClass( OO.ui.Layout, OO.EventEmitter ); +OO.inheritClass( OO.ui.WindowManager, OO.ui.Element ); +OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter ); + +/* Events */ /** - * User interface control. + * Window is opening. * - * @abstract - * @class - * @extends OO.ui.Element - * @mixins OO.EventEmitter + * Fired when the window begins to be opened. * - * @constructor - * @param {Object} [config] Configuration options - * @cfg {boolean} [disabled=false] Disable + * @event opening + * @param {OO.ui.Window} win Window that's being opened + * @param {jQuery.Promise} opening Promise resolved when window is opened; when the promise is + * resolved the first argument will be a promise which will be resolved when the window begins + * closing, the second argument will be the opening data; progress notifications will be fired on + * the promise for `setup` and `ready` when those processes are completed respectively. + * @param {Object} data Window opening data */ -OO.ui.Widget = function OoUiWidget( config ) { - // Initialize config - config = $.extend( { 'disabled': false }, config ); - - // Parent constructor - OO.ui.Widget.super.call( this, config ); - // Mixin constructors - OO.EventEmitter.call( this ); +/** + * Window is closing. + * + * Fired when the window begins to be closed. + * + * @event closing + * @param {OO.ui.Window} win Window that's being closed + * @param {jQuery.Promise} opening Promise resolved when window is closed; when the promise + * is resolved the first argument will be a the closing data; progress notifications will be fired + * on the promise for `hold` and `teardown` when those processes are completed respectively. + * @param {Object} data Window closing data + */ - // Properties - this.disabled = null; - this.wasDisabled = null; +/* Static Properties */ - // Initialization - this.$element.addClass( 'oo-ui-widget' ); - this.setDisabled( !!config.disabled ); +/** + * Map of symbolic size names and CSS properties. + * + * @static + * @inheritable + * @property {Object} + */ +OO.ui.WindowManager.static.sizes = { + 'small': { + 'width': 300 + }, + 'medium': { + 'width': 500 + }, + 'large': { + 'width': 700 + }, + 'full': { + // These can be non-numeric because they are never used in calculations + 'width': '100%', + 'height': '100%' + } }; -/* Setup */ - -OO.inheritClass( OO.ui.Widget, OO.ui.Element ); -OO.mixinClass( OO.ui.Widget, OO.EventEmitter ); - -/* Events */ - /** - * @event disable - * @param {boolean} disabled Widget is disabled + * Symbolic name of default size. + * + * Default size is used if the window's requested size is not recognized. + * + * @static + * @inheritable + * @property {string} */ +OO.ui.WindowManager.static.defaultSize = 'medium'; /* Methods */ /** - * Check if the widget is disabled. + * Handle window resize events. * - * @param {boolean} Button is disabled + * @param {jQuery.Event} e Window resize event */ -OO.ui.Widget.prototype.isDisabled = function () { - return this.disabled; +OO.ui.WindowManager.prototype.onWindowResize = function () { + clearTimeout( this.onWindowResizeTimeout ); + this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 ); }; /** - * Update the disabled state, in case of changes in parent widget. + * Handle window resize events. * - * @chainable + * @param {jQuery.Event} e Window resize event */ -OO.ui.Widget.prototype.updateDisabled = function () { - this.setDisabled( this.disabled ); - return this; +OO.ui.WindowManager.prototype.afterWindowResize = function () { + if ( this.currentWindow ) { + this.updateWindowSize( this.currentWindow ); + } }; /** - * Set the disabled state of the widget. - * - * This should probably change the widgets' appearance and prevent it from being used. + * Handle window mouse wheel events. * - * @param {boolean} disabled Disable widget - * @chainable + * @param {jQuery.Event} e Mouse wheel event */ -OO.ui.Widget.prototype.setDisabled = function ( disabled ) { - var isDisabled; +OO.ui.WindowManager.prototype.onWindowMouseWheel = function () { + return false; +}; - this.disabled = !!disabled; - isDisabled = this.isDisabled(); - if ( isDisabled !== this.wasDisabled ) { - this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled ); - this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled ); - this.emit( 'disable', isDisabled ); +/** + * Handle document key down events. + * + * @param {jQuery.Event} e Key down event + */ +OO.ui.WindowManager.prototype.onDocumentKeyDown = function ( e ) { + switch ( e.which ) { + case OO.ui.Keys.PAGEUP: + case OO.ui.Keys.PAGEDOWN: + case OO.ui.Keys.END: + case OO.ui.Keys.HOME: + case OO.ui.Keys.LEFT: + case OO.ui.Keys.UP: + case OO.ui.Keys.RIGHT: + case OO.ui.Keys.DOWN: + // Prevent any key events that might cause scrolling + return false; } - this.wasDisabled = isDisabled; - return this; }; /** - * A list of functions, called in sequence. + * Check if window is opening. * - * If a function added to a process returns boolean false the process will stop; if it returns an - * object with a `promise` method the process will use the promise to either continue to the next - * step when the promise is resolved or stop when the promise is rejected. + * @return {boolean} Window is opening + */ +OO.ui.WindowManager.prototype.isOpening = function ( win ) { + return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending'; +}; + +/** + * Check if window is closing. * - * @class + * @return {boolean} Window is closing + */ +OO.ui.WindowManager.prototype.isClosing = function ( win ) { + return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending'; +}; + +/** + * Check if window is opened. * - * @constructor + * @return {boolean} Window is opened */ -OO.ui.Process = function () { - // Properties - this.steps = []; +OO.ui.WindowManager.prototype.isOpened = function ( win ) { + return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending'; }; -/* Setup */ +/** + * Check if a window is being managed. + * + * @param {OO.ui.Window} win Window to check + * @return {boolean} Window is being managed + */ +OO.ui.WindowManager.prototype.hasWindow = function ( win ) { + var name; -OO.initClass( OO.ui.Process ); + for ( name in this.windows ) { + if ( this.windows[name] === win ) { + return true; + } + } -/* Static Methods */ + return false; +}; /** - * Generate a promise which is resolved after a set amount of time. + * Get the number of milliseconds to wait between beginning opening and executing setup process. * - * @param {number} length Number of milliseconds before resolving the promise - * @return {jQuery.Promise} Promise that will be resolved after a set amount of time + * @param {OO.ui.Window} win Window being opened + * @param {Object} [data] Window opening data + * @return {number} Milliseconds to wait */ -OO.ui.Process.static.delay = function ( length ) { - var deferred = $.Deferred(); - - setTimeout( function () { - deferred.resolve(); - }, length ); +OO.ui.WindowManager.prototype.getSetupDelay = function () { + return 0; +}; - return deferred.promise(); +/** + * Get the number of milliseconds to wait between finishing setup and executing ready process. + * + * @param {OO.ui.Window} win Window being opened + * @param {Object} [data] Window opening data + * @return {number} Milliseconds to wait + */ +OO.ui.WindowManager.prototype.getReadyDelay = function () { + return 0; }; -/* Methods */ +/** + * Get the number of milliseconds to wait between beginning closing and executing hold process. + * + * @param {OO.ui.Window} win Window being closed + * @param {Object} [data] Window closing data + * @return {number} Milliseconds to wait + */ +OO.ui.WindowManager.prototype.getHoldDelay = function () { + return 0; +}; /** - * Start the process. + * Get the number of milliseconds to wait between finishing hold and executing teardown process. * - * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when - * any of the steps return boolean false or a promise which gets rejected; upon stopping the - * process, the remaining steps will not be taken + * @param {OO.ui.Window} win Window being closed + * @param {Object} [data] Window closing data + * @return {number} Milliseconds to wait */ -OO.ui.Process.prototype.execute = function () { - var i, len, promise; +OO.ui.WindowManager.prototype.getTeardownDelay = function () { + return this.modal ? 250 : 0; +}; - /** - * Continue execution. - * - * @ignore - * @param {Array} step A function and the context it should be called in - * @return {Function} Function that continues the process - */ - function proceed( step ) { - return function () { - // Execute step in the correct context - var result = step[0].call( step[1] ); +/** + * Get managed window by symbolic name. + * + * If window is not yet instantiated, it will be instantiated and added automatically. + * + * @param {string} name Symbolic window name + * @return {jQuery.Promise} Promise resolved when window is ready to be accessed; when resolved the + * first argument is an OO.ui.Window; when rejected the first argument is an OO.ui.Error + * @throws {Error} If the symbolic name is unrecognized by the factory + * @throws {Error} If the symbolic name unrecognized as a managed window + */ +OO.ui.WindowManager.prototype.getWindow = function ( name ) { + var deferred = $.Deferred(), + win = this.windows[name]; - if ( result === false ) { - // Use rejected promise for boolean false results - return $.Deferred().reject().promise(); - } - // Duck-type the object to see if it can produce a promise - if ( result && $.isFunction( result.promise ) ) { - // Use a promise generated from the result - return result.promise(); + if ( !( win instanceof OO.ui.Window ) ) { + if ( this.factory ) { + if ( !this.factory.lookup( name ) ) { + deferred.reject( new OO.ui.Error( + 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory' + ) ); + } else { + win = this.factory.create( name, this, { '$': this.$ } ); + this.addWindows( [ win ] ).then( + OO.ui.bind( deferred.resolve, deferred, win ), + deferred.reject + ); } - // Use resolved promise for other results - return $.Deferred().resolve().promise(); - }; - } - - if ( this.steps.length ) { - // Generate a chain reaction of promises - promise = proceed( this.steps[0] )(); - for ( i = 1, len = this.steps.length; i < len; i++ ) { - promise = promise.then( proceed( this.steps[i] ) ); + } else { + deferred.reject( new OO.ui.Error( + 'Cannot get unmanaged window: symbolic name unrecognized as a managed window' + ) ); } } else { - promise = $.Deferred().resolve().promise(); + deferred.resolve( win ); } - return promise; + return deferred.promise(); }; /** - * Add step to the beginning of the process. + * Get current window. * - * @param {Function} step Function to execute; if it returns boolean false the process will stop; if - * it returns an object with a `promise` method the process will use the promise to either - * continue to the next step when the promise is resolved or stop when the promise is rejected - * @param {Object} [context=null] Context to call the step function in - * @chainable + * @return {OO.ui.Window|null} Currently opening/opened/closing window */ -OO.ui.Process.prototype.first = function ( step, context ) { - this.steps.unshift( [ step, context || null ] ); - return this; +OO.ui.WindowManager.prototype.getCurrentWindow = function () { + return this.currentWindow; }; /** - * Add step to the end of the process. + * Open a window. * - * @param {Function} step Function to execute; if it returns boolean false the process will stop; if - * it returns an object with a `promise` method the process will use the promise to either - * continue to the next step when the promise is resolved or stop when the promise is rejected - * @param {Object} [context=null] Context to call the step function in - * @chainable + * @param {OO.ui.Window|string} win Window object or symbolic name of window to open + * @param {Object} [data] Window opening data + * @return {jQuery.Promise} Promise resolved when window is done opening; see {@link #event-opening} + * for more details about the `opening` promise + * @fires opening */ -OO.ui.Process.prototype.next = function ( step, context ) { - this.steps.push( [ step, context || null ] ); - return this; +OO.ui.WindowManager.prototype.openWindow = function ( win, data ) { + var manager = this, + preparing = [], + opening = $.Deferred(); + + // Argument handling + if ( typeof win === 'string' ) { + return this.getWindow( win ).then( function ( win ) { + return manager.openWindow( win, data ); + } ); + } + + // Error handling + if ( !this.hasWindow( win ) ) { + opening.reject( new OO.ui.Error( + 'Cannot open window: window is not attached to manager' + ) ); + } + + // Window opening + if ( opening.state() !== 'rejected' ) { + // Begin loading the window if it's not loaded already - may take noticable time and we want + // too do this in paralell with any preparatory actions + preparing.push( win.load() ); + + if ( this.opening || this.opened ) { + // If a window is currently opening or opened, close it first + preparing.push( this.closeWindow( this.currentWindow ) ); + } else if ( this.closing ) { + // If a window is currently closing, wait for it to complete + preparing.push( this.closing ); + } + + $.when.apply( $, preparing ).done( function () { + if ( manager.modal ) { + manager.$( manager.getElementDocument() ).on( { + // Prevent scrolling by keys in top-level window + 'keydown': manager.onDocumentKeyDownHandler + } ); + manager.$( manager.getElementWindow() ).on( { + // Prevent scrolling by wheel in top-level window + 'mousewheel': manager.onWindowMouseWheelHandler, + // Start listening for top-level window dimension changes + 'orientationchange resize': manager.onWindowResizeHandler + } ); + // Hide other content from screen readers + manager.$ariaHidden = $( 'body' ) + .children() + .not( manager.$element.parentsUntil( 'body' ).last() ) + .attr( 'aria-hidden', '' ); + } + manager.currentWindow = win; + manager.opening = opening; + manager.emit( 'opening', win, opening, data ); + manager.updateWindowSize( win ); + setTimeout( function () { + win.setup( data ).then( function () { + manager.opening.notify( { 'state': 'setup' } ); + setTimeout( function () { + win.ready( data ).then( function () { + manager.opening.notify( { 'state': 'ready' } ); + manager.opening = null; + manager.opened = $.Deferred(); + opening.resolve( manager.opened.promise(), data ); + } ); + }, manager.getReadyDelay() ); + } ); + }, manager.getSetupDelay() ); + } ); + } + + return opening; }; /** - * Dialog for showing a confirmation/warning message. + * Close a window. * - * @class - * @extends OO.ui.Dialog - * - * @constructor - * @param {Object} [config] Configuration options + * @param {OO.ui.Window|string} win Window object or symbolic name of window to close + * @param {Object} [data] Window closing data + * @return {jQuery.Promise} Promise resolved when window is done opening; see {@link #event-closing} + * for more details about the `closing` promise + * @throws {Error} If no window by that name is being managed + * @fires closing */ -OO.ui.ConfirmationDialog = function OoUiConfirmationDialog( config ) { - // Configuration initialization - config = $.extend( { 'size': 'small' }, config ); - - // Parent constructor - OO.ui.Dialog.call( this, config ); -}; +OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) { + var manager = this, + preparing = [], + closing = $.Deferred(), + opened = this.opened; + + // Argument handling + if ( typeof win === 'string' ) { + win = this.windows[win]; + } else if ( !this.hasWindow( win ) ) { + win = null; + } + + // Error handling + if ( !win ) { + closing.reject( new OO.ui.Error( + 'Cannot close window: window is not attached to manager' + ) ); + } else if ( win !== this.currentWindow ) { + closing.reject( new OO.ui.Error( + 'Cannot close window: window already closed with different data' + ) ); + } else if ( this.closing ) { + closing.reject( new OO.ui.Error( + 'Cannot close window: window already closing with different data' + ) ); + } + + // Window closing + if ( closing.state() !== 'rejected' ) { + if ( this.opening ) { + // If the window is currently opening, close it when it's done + preparing.push( this.opening ); + } -/* Inheritance */ + // Close the window + $.when.apply( $, preparing ).done( function () { + manager.closing = closing; + manager.emit( 'closing', win, closing, data ); + manager.opened = null; + opened.resolve( closing.promise(), data ); + setTimeout( function () { + win.hold( data ).then( function () { + closing.notify( { 'state': 'hold' } ); + setTimeout( function () { + win.teardown( data ).then( function () { + closing.notify( { 'state': 'teardown' } ); + if ( manager.modal ) { + manager.$( manager.getElementDocument() ).off( { + // Allow scrolling by keys in top-level window + 'keydown': manager.onDocumentKeyDownHandler + } ); + manager.$( manager.getElementWindow() ).off( { + // Allow scrolling by wheel in top-level window + 'mousewheel': manager.onWindowMouseWheelHandler, + // Stop listening for top-level window dimension changes + 'orientationchange resize': manager.onWindowResizeHandler + } ); + } + // Restore screen reader visiblity + if ( manager.$ariaHidden ) { + manager.$ariaHidden.removeAttr( 'aria-hidden' ); + manager.$ariaHidden = null; + } + manager.closing = null; + manager.currentWindow = null; + closing.resolve( data ); + } ); + }, manager.getTeardownDelay() ); + } ); + }, manager.getHoldDelay() ); + } ); + } -OO.inheritClass( OO.ui.ConfirmationDialog, OO.ui.Dialog ); + return closing; +}; -/* Static Properties */ +/** + * Add windows. + * + * If the window manager is attached to the DOM then windows will be automatically loaded as they + * are added. + * + * @param {Object.|OO.ui.Window[]} windows Windows to add + * @return {jQuery.Promise} Promise resolved when all windows are added + * @throws {Error} If one of the windows being added without an explicit symbolic name does not have + * a statically configured symbolic name + */ +OO.ui.WindowManager.prototype.addWindows = function ( windows ) { + var i, len, win, name, list, + promises = []; -OO.ui.ConfirmationDialog.static.name = 'confirm'; + if ( $.isArray( windows ) ) { + // Convert to map of windows by looking up symbolic names from static configuration + list = {}; + for ( i = 0, len = windows.length; i < len; i++ ) { + name = windows[i].constructor.static.name; + if ( typeof name !== 'string' ) { + throw new Error( 'Cannot add window' ); + } + list[name] = windows[i]; + } + } else if ( $.isPlainObject( windows ) ) { + list = windows; + } -OO.ui.ConfirmationDialog.static.icon = 'help'; + // Add windows + for ( name in list ) { + win = list[name]; + this.windows[name] = win; + this.$element.append( win.$element ); -OO.ui.ConfirmationDialog.static.title = OO.ui.deferMsg( 'ooui-dialog-confirm-title' ); + if ( this.isElementAttached() ) { + promises.push( win.load() ); + } + } -/* Methods */ + return $.when.apply( $, promises ); +}; /** - * @inheritdoc + * Remove windows. + * + * Windows will be closed before they are removed. + * + * @param {string} name Symbolic name of window to remove + * @return {jQuery.Promise} Promise resolved when window is closed and removed + * @throws {Error} If windows being removed are not being managed */ -OO.ui.ConfirmationDialog.prototype.initialize = function () { - // Parent method - OO.ui.Dialog.prototype.initialize.call( this ); - - // Set up the layout - var contentLayout = new OO.ui.PanelLayout( { - '$': this.$, - 'padded': true - } ); - - this.$promptContainer = this.$( '
' ).addClass( 'oo-ui-dialog-confirm-promptContainer' ); - - this.cancelButton = new OO.ui.ButtonWidget(); - this.cancelButton.connect( this, { 'click': [ 'close', 'cancel' ] } ); - - this.okButton = new OO.ui.ButtonWidget(); - this.okButton.connect( this, { 'click': [ 'close', 'ok' ] } ); +OO.ui.WindowManager.prototype.removeWindows = function ( names ) { + var i, len, win, name, + manager = this, + promises = [], + cleanup = function ( name, win ) { + delete manager.windows[name]; + win.$element.detach(); + }; - // Make the buttons - contentLayout.$element.append( this.$promptContainer ); - this.$body.append( contentLayout.$element ); + for ( i = 0, len = names.length; i < len; i++ ) { + name = names[i]; + win = this.windows[name]; + if ( !win ) { + throw new Error( 'Cannot remove window' ); + } + promises.push( this.closeWindow( name ).then( OO.ui.bind( cleanup, null, name, win ) ) ); + } - this.$foot.append( - this.okButton.$element, - this.cancelButton.$element - ); + return $.when.apply( $, promises ); }; -/* - * Setup a confirmation dialog. +/** + * Remove all windows. * - * @param {Object} [data] Window opening data including text of the dialog and text for the buttons - * @param {jQuery|string} [data.prompt] Text to display or list of nodes to use as content of the dialog. - * @param {jQuery|string|Function|null} [data.okLabel] Label of the OK button - * @param {jQuery|string|Function|null} [data.cancelLabel] Label of the cancel button - * @param {string|string[]} [data.okFlags="constructive"] Flags for the OK button - * @param {string|string[]} [data.cancelFlags="destructive"] Flags for the cancel button - * @return {OO.ui.Process} Setup process + * Windows will be closed before they are removed. + * + * @return {jQuery.Promise} Promise resolved when all windows are closed and removed */ -OO.ui.ConfirmationDialog.prototype.getSetupProcess = function ( data ) { - // Parent method - return OO.ui.ConfirmationDialog.super.prototype.getSetupProcess.call( this, data ) - .next( function () { - var prompt = data.prompt || OO.ui.deferMsg( 'ooui-dialog-confirm-default-prompt' ), - okLabel = data.okLabel || OO.ui.deferMsg( 'ooui-dialog-confirm-default-ok' ), - cancelLabel = data.cancelLabel || OO.ui.deferMsg( 'ooui-dialog-confirm-default-cancel' ), - okFlags = data.okFlags || 'constructive', - cancelFlags = data.cancelFlags || 'destructive'; - - if ( typeof prompt === 'string' ) { - this.$promptContainer.text( prompt ); - } else { - this.$promptContainer.empty().append( prompt ); - } - - this.okButton.setLabel( okLabel ).clearFlags().setFlags( okFlags ); - this.cancelButton.setLabel( cancelLabel ).clearFlags().setFlags( cancelFlags ); - }, this ); +OO.ui.WindowManager.prototype.clearWindows = function () { + return this.removeWindows( Object.keys( this.windows ) ); }; /** - * @inheritdoc + * Set dialog size. + * + * Fullscreen mode will be used if the dialog is too wide to fit in the screen. + * + * @chainable */ -OO.ui.ConfirmationDialog.prototype.getTeardownProcess = function ( data ) { - // Parent method - return OO.ui.ConfirmationDialog.super.prototype.getTeardownProcess.call( this, data ) - .first( function () { - if ( data === 'ok' ) { - this.opened.resolve(); - } else { // data === 'cancel', or no data - this.opened.reject(); - } - }, this ); +OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) { + // Bypass for non-current, and thus invisible, windows + if ( win !== this.currentWindow ) { + return; + } + + var viewport = OO.ui.Element.getDimensions( win.getElementWindow() ), + sizes = this.constructor.static.sizes, + size = win.getSize(); + + if ( !sizes[size] ) { + size = this.constructor.static.defaultSize; + } + if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[size].width ) { + size = 'full'; + } + + this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' ); + win.setDimensions( sizes[size] ); + + return this; }; /** - * Element with a button. + * Process error. * * @abstract * @class * * @constructor - * @param {jQuery} $button Button node, assigned to #$button + * @param {string|jQuery} message Description of error * @param {Object} [config] Configuration options - * @cfg {boolean} [frameless] Render button without a frame - * @cfg {number} [tabIndex=0] Button's tab index, use -1 to prevent tab focusing + * @cfg {boolean} [recoverable=true] Error is recoverable */ -OO.ui.ButtonedElement = function OoUiButtonedElement( $button, config ) { +OO.ui.Error = function OoUiElement( message, config ) { // Configuration initialization config = config || {}; // Properties - this.$button = $button; - this.tabIndex = null; - this.active = false; - this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this ); - - // Events - this.$button.on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) ); - - // Initialization - this.$element - .addClass( 'oo-ui-buttonedElement' ) - .prop( 'tabIndex', config.tabIndex || 0 ); - this.$button - .addClass( 'oo-ui-buttonedElement-button' ) - .attr( 'role', 'button' ); - if ( config.frameless ) { - this.$element.addClass( 'oo-ui-buttonedElement-frameless' ); - } else { - this.$element.addClass( 'oo-ui-buttonedElement-framed' ); - } + this.message = message instanceof jQuery ? message : String( message ); + this.recoverable = config.recoverable === undefined ? true : !!config.recoverable; }; /* Setup */ -OO.initClass( OO.ui.ButtonedElement ); - -/* Static Properties */ - -/** - * Cancel mouse down events. - * - * @static - * @inheritable - * @property {boolean} - */ -OO.ui.ButtonedElement.static.cancelButtonMouseDownEvents = true; +OO.initClass( OO.ui.Error ); /* Methods */ /** - * Handles mouse down events. + * Check if error can be recovered from. * - * @param {jQuery.Event} e Mouse down event + * @return {boolean} Error is recoverable */ -OO.ui.ButtonedElement.prototype.onMouseDown = function ( e ) { - if ( this.isDisabled() || e.which !== 1 ) { - return false; - } - // tabIndex should generally be interacted with via the property, but it's not possible to - // reliably unset a tabIndex via a property so we use the (lowercase) "tabindex" attribute - this.tabIndex = this.$button.attr( 'tabindex' ); - this.$button - // Remove the tab-index while the button is down to prevent the button from stealing focus - .removeAttr( 'tabindex' ) - .addClass( 'oo-ui-buttonedElement-pressed' ); - // Run the mouseup handler no matter where the mouse is when the button is let go, so we can - // reliably reapply the tabindex and remove the pressed class - this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true ); - // Prevent change of focus unless specifically configured otherwise - if ( this.constructor.static.cancelButtonMouseDownEvents ) { - return false; - } +OO.ui.Error.prototype.isRecoverable = function () { + return this.recoverable; }; /** - * Handles mouse up events. + * Get error message as DOM nodes. * - * @param {jQuery.Event} e Mouse up event + * @return {jQuery} Error message in DOM nodes */ -OO.ui.ButtonedElement.prototype.onMouseUp = function ( e ) { - if ( this.isDisabled() || e.which !== 1 ) { - return false; - } - this.$button - // Restore the tab-index after the button is up to restore the button's accesssibility - .attr( 'tabindex', this.tabIndex ) - .removeClass( 'oo-ui-buttonedElement-pressed' ); - // Stop listening for mouseup, since we only needed this once - this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true ); +OO.ui.Error.prototype.getMessage = function () { + return this.message instanceof jQuery ? + this.message.clone() : + $( '
' ).text( this.message ).contents(); }; /** - * Set active state. + * Get error message as text. * - * @param {boolean} [value] Make button active - * @chainable + * @return {string} Error message */ -OO.ui.ButtonedElement.prototype.setActive = function ( value ) { - this.$button.toggleClass( 'oo-ui-buttonedElement-active', !!value ); - return this; +OO.ui.Error.prototype.getMessageText = function () { + return this.message instanceof jQuery ? this.message.text() : this.message; }; /** - * Element that can be automatically clipped to visible boundaies. + * A list of functions, called in sequence. + * + * If a function added to a process returns boolean false the process will stop; if it returns an + * object with a `promise` method the process will use the promise to either continue to the next + * step when the promise is resolved or stop when the promise is rejected. * - * @abstract * @class * * @constructor - * @param {jQuery} $clippable Nodes to clip, assigned to #$clippable - * @param {Object} [config] Configuration options + * @param {number|jQuery.Promise|Function} step Time to wait, promise to wait for or function to + * call, see #createStep for more information + * @param {Object} [context=null] Context to call the step function in, ignored if step is a number + * or a promise + * @return {Object} Step object, with `callback` and `context` properties */ -OO.ui.ClippableElement = function OoUiClippableElement( $clippable, config ) { - // Configuration initialization - config = config || {}; - +OO.ui.Process = function ( step, context ) { // Properties - this.$clippable = $clippable; - this.clipping = false; - this.clipped = false; - this.$clippableContainer = null; - this.$clippableScroller = null; - this.$clippableWindow = null; - this.idealWidth = null; - this.idealHeight = null; - this.onClippableContainerScrollHandler = OO.ui.bind( this.clip, this ); - this.onClippableWindowResizeHandler = OO.ui.bind( this.clip, this ); + this.steps = []; // Initialization - this.$clippable.addClass( 'oo-ui-clippableElement-clippable' ); + if ( step !== undefined ) { + this.next( step, context ); + } }; +/* Setup */ + +OO.initClass( OO.ui.Process ); + /* Methods */ /** - * Set clipping. + * Start the process. * - * @param {boolean} value Enable clipping - * @chainable + * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when + * any of the steps return boolean false or a promise which gets rejected; upon stopping the + * process, the remaining steps will not be taken */ -OO.ui.ClippableElement.prototype.setClipping = function ( value ) { - value = !!value; +OO.ui.Process.prototype.execute = function () { + var i, len, promise; - if ( this.clipping !== value ) { - this.clipping = value; - if ( this.clipping ) { - this.$clippableContainer = this.$( this.getClosestScrollableElementContainer() ); - // If the clippable container is the body, we have to listen to scroll events and check - // jQuery.scrollTop on the window because of browser inconsistencies - this.$clippableScroller = this.$clippableContainer.is( 'body' ) ? - this.$( OO.ui.Element.getWindow( this.$clippableContainer ) ) : - this.$clippableContainer; - this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler ); - this.$clippableWindow = this.$( this.getElementWindow() ) - .on( 'resize', this.onClippableWindowResizeHandler ); - // Initial clip after visible - setTimeout( OO.ui.bind( this.clip, this ) ); - } else { - this.$clippableContainer = null; - this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler ); - this.$clippableScroller = null; - this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler ); - this.$clippableWindow = null; + /** + * Continue execution. + * + * @ignore + * @param {Array} step A function and the context it should be called in + * @return {Function} Function that continues the process + */ + function proceed( step ) { + return function () { + // Execute step in the correct context + var deferred, + result = step.callback.call( step.context ); + + if ( result === false ) { + // Use rejected promise for boolean false results + return $.Deferred().reject( [] ).promise(); + } + if ( typeof result === 'number' ) { + if ( result < 0 ) { + throw new Error( 'Cannot go back in time: flux capacitor is out of service' ); + } + // Use a delayed promise for numbers, expecting them to be in milliseconds + deferred = $.Deferred(); + setTimeout( deferred.resolve, result ); + return deferred.promise(); + } + if ( result instanceof OO.ui.Error ) { + // Use rejected promise for error + return $.Deferred().reject( [ result ] ).promise(); + } + if ( $.isArray( result ) && result.length && result[0] instanceof OO.ui.Error ) { + // Use rejected promise for list of errors + return $.Deferred().reject( result ).promise(); + } + // Duck-type the object to see if it can produce a promise + if ( result && $.isFunction( result.promise ) ) { + // Use a promise generated from the result + return result.promise(); + } + // Use resolved promise for other results + return $.Deferred().resolve().promise(); + }; + } + + if ( this.steps.length ) { + // Generate a chain reaction of promises + promise = proceed( this.steps[0] )(); + for ( i = 1, len = this.steps.length; i < len; i++ ) { + promise = promise.then( proceed( this.steps[i] ) ); } + } else { + promise = $.Deferred().resolve().promise(); } - return this; + return promise; }; /** - * Check if the element will be clipped to fit the visible area of the nearest scrollable container. + * Create a process step. * - * @return {boolean} Element will be clipped to the visible area - */ -OO.ui.ClippableElement.prototype.isClipping = function () { - return this.clipping; + * @private + * @param {number|jQuery.Promise|Function} step + * + * - Number of milliseconds to wait; or + * - Promise to wait to be resolved; or + * - Function to execute + * - If it returns boolean false the process will stop + * - If it returns an object with a `promise` method the process will use the promise to either + * continue to the next step when the promise is resolved or stop when the promise is rejected + * - If it returns a number, the process will wait for that number of milliseconds before + * proceeding + * @param {Object} [context=null] Context to call the step function in, ignored if step is a number + * or a promise + * @return {Object} Step object, with `callback` and `context` properties + */ +OO.ui.Process.prototype.createStep = function ( step, context ) { + if ( typeof step === 'number' || $.isFunction( step.promise ) ) { + return { + 'callback': function () { + return step; + }, + 'context': null + }; + } + if ( $.isFunction( step ) ) { + return { + 'callback': step, + 'context': context + }; + } + throw new Error( 'Cannot create process step: number, promise or function expected' ); }; /** - * Check if the bottom or right of the element is being clipped by the nearest scrollable container. + * Add step to the beginning of the process. * - * @return {boolean} Part of the element is being clipped + * @inheritdoc #createStep + * @return {OO.ui.Process} this + * @chainable */ -OO.ui.ClippableElement.prototype.isClipped = function () { - return this.clipped; -}; - -/** - * Set the ideal size. - * - * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix - * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix - */ -OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) { - this.idealWidth = width; - this.idealHeight = height; +OO.ui.Process.prototype.first = function ( step, context ) { + this.steps.unshift( this.createStep( step, context ) ); + return this; }; /** - * Clip element to visible boundaries and allow scrolling when needed. - * - * Element will be clipped the bottom or right of the element is within 10px of the edge of, or - * overlapped by, the visible area of the nearest scrollable container. + * Add step to the end of the process. * + * @inheritdoc #createStep + * @return {OO.ui.Process} this * @chainable */ -OO.ui.ClippableElement.prototype.clip = function () { - if ( !this.clipping ) { - // this.$clippableContainer and this.$clippableWindow are null, so the below will fail - return this; - } - - var buffer = 10, - cOffset = this.$clippable.offset(), - ccOffset = this.$clippableContainer.offset() || { 'top': 0, 'left': 0 }, - ccHeight = this.$clippableContainer.innerHeight() - buffer, - ccWidth = this.$clippableContainer.innerWidth() - buffer, - scrollTop = this.$clippableScroller.scrollTop(), - scrollLeft = this.$clippableScroller.scrollLeft(), - desiredWidth = ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left, - desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top, - naturalWidth = this.$clippable.prop( 'scrollWidth' ), - naturalHeight = this.$clippable.prop( 'scrollHeight' ), - clipWidth = desiredWidth < naturalWidth, - clipHeight = desiredHeight < naturalHeight; - - if ( clipWidth ) { - this.$clippable.css( { 'overflow-x': 'auto', 'width': desiredWidth } ); - } else { - this.$clippable.css( 'width', this.idealWidth || '' ); - this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290 - this.$clippable.css( 'overflow-x', '' ); - } - if ( clipHeight ) { - this.$clippable.css( { 'overflow-y': 'auto', 'height': desiredHeight } ); - } else { - this.$clippable.css( 'height', this.idealHeight || '' ); - this.$clippable.height(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290 - this.$clippable.css( 'overflow-y', '' ); - } - - this.clipped = clipWidth || clipHeight; - +OO.ui.Process.prototype.next = function ( step, context ) { + this.steps.push( this.createStep( step, context ) ); return this; }; /** - * Element with named flags that can be added, removed, listed and checked. - * - * A flag, when set, adds a CSS class on the `$element` by combing `oo-ui-flaggableElement-` with - * the flag name. Flags are primarily useful for styling. + * Factory for tools. * - * @abstract * @class - * + * @extends OO.Factory * @constructor - * @param {Object} [config] Configuration options - * @cfg {string[]} [flags=[]] Styling flags, e.g. 'primary', 'destructive' or 'constructive' */ -OO.ui.FlaggableElement = function OoUiFlaggableElement( config ) { - // Config initialization - config = config || {}; +OO.ui.ToolFactory = function OoUiToolFactory() { + // Parent constructor + OO.ui.ToolFactory.super.call( this ); +}; - // Properties - this.flags = {}; +/* Setup */ - // Initialization - this.setFlags( config.flags ); -}; +OO.inheritClass( OO.ui.ToolFactory, OO.Factory ); /* Methods */ -/** - * Check if a flag is set. - * - * @param {string} flag Name of flag - * @return {boolean} Has flag - */ -OO.ui.FlaggableElement.prototype.hasFlag = function ( flag ) { - return flag in this.flags; -}; +/** */ +OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) { + var i, len, included, promoted, demoted, + auto = [], + used = {}; -/** - * Get the names of all flags set. - * - * @return {string[]} flags Flag names - */ -OO.ui.FlaggableElement.prototype.getFlags = function () { - return Object.keys( this.flags ); -}; + // Collect included and not excluded tools + included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) ); -/** - * Clear all flags. - * - * @chainable - */ -OO.ui.FlaggableElement.prototype.clearFlags = function () { - var flag, - classPrefix = 'oo-ui-flaggableElement-'; + // Promotion + promoted = this.extract( promote, used ); + demoted = this.extract( demote, used ); - for ( flag in this.flags ) { - delete this.flags[flag]; - this.$element.removeClass( classPrefix + flag ); + // Auto + for ( i = 0, len = included.length; i < len; i++ ) { + if ( !used[included[i]] ) { + auto.push( included[i] ); + } } - return this; + return promoted.concat( auto ).concat( demoted ); }; /** - * Add one or more flags. + * Get a flat list of names from a list of names or groups. * - * @param {string|string[]|Object.} flags One or more flags to add, or an object - * keyed by flag name containing boolean set/remove instructions. - * @chainable + * Tools can be specified in the following ways: + * + * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'` + * - All tools in a group: `{ 'group': 'group-name' }` + * - All tools: `'*'` + * + * @private + * @param {Array|string} collection List of tools + * @param {Object} [used] Object with names that should be skipped as properties; extracted + * names will be added as properties + * @return {string[]} List of extracted names */ -OO.ui.FlaggableElement.prototype.setFlags = function ( flags ) { - var i, len, flag, - classPrefix = 'oo-ui-flaggableElement-'; +OO.ui.ToolFactory.prototype.extract = function ( collection, used ) { + var i, len, item, name, tool, + names = []; - if ( typeof flags === 'string' ) { - // Set - this.flags[flags] = true; - this.$element.addClass( classPrefix + flags ); - } else if ( $.isArray( flags ) ) { - for ( i = 0, len = flags.length; i < len; i++ ) { - flag = flags[i]; - // Set - this.flags[flag] = true; - this.$element.addClass( classPrefix + flag ); + if ( collection === '*' ) { + for ( name in this.registry ) { + tool = this.registry[name]; + if ( + // Only add tools by group name when auto-add is enabled + tool.static.autoAddToCatchall && + // Exclude already used tools + ( !used || !used[name] ) + ) { + names.push( name ); + if ( used ) { + used[name] = true; + } + } } - } else if ( OO.isPlainObject( flags ) ) { - for ( flag in flags ) { - if ( flags[flag] ) { - // Set - this.flags[flag] = true; - this.$element.addClass( classPrefix + flag ); - } else { - // Remove - delete this.flags[flag]; - this.$element.removeClass( classPrefix + flag ); + } else if ( $.isArray( collection ) ) { + for ( i = 0, len = collection.length; i < len; i++ ) { + item = collection[i]; + // Allow plain strings as shorthand for named tools + if ( typeof item === 'string' ) { + item = { 'name': item }; + } + if ( OO.isPlainObject( item ) ) { + if ( item.group ) { + for ( name in this.registry ) { + tool = this.registry[name]; + if ( + // Include tools with matching group + tool.static.group === item.group && + // Only add tools by group name when auto-add is enabled + tool.static.autoAddToGroup && + // Exclude already used tools + ( !used || !used[name] ) + ) { + names.push( name ); + if ( used ) { + used[name] = true; + } + } + } + // Include tools with matching name and exclude already used tools + } else if ( item.name && ( !used || !used[item.name] ) ) { + names.push( item.name ); + if ( used ) { + used[item.name] = true; + } + } } } } - return this; + return names; }; /** - * Element containing a sequence of child elements. + * Factory for tool groups. * - * @abstract * @class - * + * @extends OO.Factory * @constructor - * @param {jQuery} $group Container node, assigned to #$group - * @param {Object} [config] Configuration options */ -OO.ui.GroupElement = function OoUiGroupElement( $group, config ) { - // Configuration - config = config || {}; +OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() { + // Parent constructor + OO.Factory.call( this ); - // Properties - this.$group = $group; - this.items = []; - this.aggregateItemEvents = {}; + var i, l, + defaultClasses = this.constructor.static.getDefaultClasses(); + + // Register default toolgroups + for ( i = 0, l = defaultClasses.length; i < l; i++ ) { + this.register( defaultClasses[i] ); + } }; -/* Methods */ +/* Setup */ -/** - * Get items. - * - * @return {OO.ui.Element[]} Items - */ -OO.ui.GroupElement.prototype.getItems = function () { - return this.items.slice( 0 ); -}; +OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory ); + +/* Static Methods */ /** - * Add an aggregate item event. + * Get a default set of classes to be registered on construction * - * Aggregated events are listened to on each item and then emitted by the group under a new name, - * and with an additional leading parameter containing the item that emitted the original event. - * Other arguments that were emitted from the original event are passed through. + * @return {Function[]} Default classes + */ +OO.ui.ToolGroupFactory.static.getDefaultClasses = function () { + return [ + OO.ui.BarToolGroup, + OO.ui.ListToolGroup, + OO.ui.MenuToolGroup + ]; +}; + +/** + * Element with a button. * - * @param {Object.} events Aggregate events emitted by group, keyed by item - * event, use null value to remove aggregation - * @throws {Error} If aggregation already exists + * @abstract + * @class + * + * @constructor + * @param {jQuery} $button Button node, assigned to #$button + * @param {Object} [config] Configuration options + * @cfg {boolean} [framed=true] Render button with a frame + * @cfg {number} [tabIndex=0] Button's tab index, use null to have no tabIndex + * @cfg {string} [accessKey] Button's access key */ -OO.ui.GroupElement.prototype.aggregate = function ( events ) { - var i, len, item, add, remove, itemEvent, groupEvent; +OO.ui.ButtonedElement = function OoUiButtonedElement( $button, config ) { + // Configuration initialization + config = config || {}; - for ( itemEvent in events ) { - groupEvent = events[itemEvent]; + // Properties + this.$button = $button; + this.tabIndex = null; + this.framed = null; + this.active = false; + this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this ); - // Remove existing aggregated event - if ( itemEvent in this.aggregateItemEvents ) { - // Don't allow duplicate aggregations - if ( groupEvent ) { - throw new Error( 'Duplicate item event aggregation for ' + itemEvent ); - } - // Remove event aggregation from existing items - for ( i = 0, len = this.items.length; i < len; i++ ) { - item = this.items[i]; - if ( item.connect && item.disconnect ) { - remove = {}; - remove[itemEvent] = [ 'emit', groupEvent, item ]; - item.disconnect( this, remove ); - } - } - // Prevent future items from aggregating event - delete this.aggregateItemEvents[itemEvent]; - } + // Events + this.$button.on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) ); - // Add new aggregate event - if ( groupEvent ) { - // Make future items aggregate event - this.aggregateItemEvents[itemEvent] = groupEvent; - // Add event aggregation to existing items - for ( i = 0, len = this.items.length; i < len; i++ ) { - item = this.items[i]; - if ( item.connect && item.disconnect ) { - add = {}; - add[itemEvent] = [ 'emit', groupEvent, item ]; - item.connect( this, add ); - } - } - } - } + // Initialization + this.$element.addClass( 'oo-ui-buttonedElement' ); + this.$button + .addClass( 'oo-ui-buttonedElement-button' ) + .attr( 'role', 'button' ); + this.setTabIndex( config.tabIndex || 0 ); + this.setAccessKey( config.accessKey ); + this.toggleFramed( config.framed === undefined || config.framed ); }; +/* Setup */ + +OO.initClass( OO.ui.ButtonedElement ); + +/* Static Properties */ + /** - * Add items. + * Cancel mouse down events. * - * @param {OO.ui.Element[]} items Item - * @param {number} [index] Index to insert items at - * @chainable + * @static + * @inheritable + * @property {boolean} */ -OO.ui.GroupElement.prototype.addItems = function ( items, index ) { - var i, len, item, event, events, currentIndex, - itemElements = []; +OO.ui.ButtonedElement.static.cancelButtonMouseDownEvents = true; - for ( i = 0, len = items.length; i < len; i++ ) { - item = items[i]; +/* Methods */ - // Check if item exists then remove it first, effectively "moving" it - currentIndex = $.inArray( item, this.items ); - if ( currentIndex >= 0 ) { - this.removeItems( [ item ] ); - // Adjust index to compensate for removal - if ( currentIndex < index ) { - index--; - } - } - // Add the item - if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) { - events = {}; - for ( event in this.aggregateItemEvents ) { - events[event] = [ 'emit', this.aggregateItemEvents[event], item ]; - } - item.connect( this, events ); - } - item.setElementGroup( this ); - itemElements.push( item.$element.get( 0 ) ); +/** + * Handles mouse down events. + * + * @param {jQuery.Event} e Mouse down event + */ +OO.ui.ButtonedElement.prototype.onMouseDown = function ( e ) { + if ( this.isDisabled() || e.which !== 1 ) { + return false; } - - if ( index === undefined || index < 0 || index >= this.items.length ) { - this.$group.append( itemElements ); - this.items.push.apply( this.items, items ); - } else if ( index === 0 ) { - this.$group.prepend( itemElements ); - this.items.unshift.apply( this.items, items ); - } else { - this.items[index].$element.before( itemElements ); - this.items.splice.apply( this.items, [ index, 0 ].concat( items ) ); + // tabIndex should generally be interacted with via the property, but it's not possible to + // reliably unset a tabIndex via a property so we use the (lowercase) "tabindex" attribute + this.tabIndex = this.$button.attr( 'tabindex' ); + this.$button + // Remove the tab-index while the button is down to prevent the button from stealing focus + .removeAttr( 'tabindex' ) + .addClass( 'oo-ui-buttonedElement-pressed' ); + // Run the mouseup handler no matter where the mouse is when the button is let go, so we can + // reliably reapply the tabindex and remove the pressed class + this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true ); + // Prevent change of focus unless specifically configured otherwise + if ( this.constructor.static.cancelButtonMouseDownEvents ) { + return false; } - - return this; }; /** - * Remove items. + * Handles mouse up events. * - * Items will be detached, not removed, so they can be used later. + * @param {jQuery.Event} e Mouse up event + */ +OO.ui.ButtonedElement.prototype.onMouseUp = function ( e ) { + if ( this.isDisabled() || e.which !== 1 ) { + return false; + } + this.$button + // Restore the tab-index after the button is up to restore the button's accesssibility + .attr( 'tabindex', this.tabIndex ) + .removeClass( 'oo-ui-buttonedElement-pressed' ); + // Stop listening for mouseup, since we only needed this once + this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true ); +}; + +/** + * Toggle frame. * - * @param {OO.ui.Element[]} items Items to remove + * @param {boolean} [framed] Make button framed, omit to toggle * @chainable */ -OO.ui.GroupElement.prototype.removeItems = function ( items ) { - var i, len, item, index, remove, itemEvent; - - // Remove specific items - for ( i = 0, len = items.length; i < len; i++ ) { - item = items[i]; - index = $.inArray( item, this.items ); - if ( index !== -1 ) { - if ( - item.connect && item.disconnect && - !$.isEmptyObject( this.aggregateItemEvents ) - ) { - remove = {}; - if ( itemEvent in this.aggregateItemEvents ) { - remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ]; - } - item.disconnect( this, remove ); - } - item.setElementGroup( null ); - this.items.splice( index, 1 ); - item.$element.detach(); - } +OO.ui.ButtonedElement.prototype.toggleFramed = function ( framed ) { + framed = framed === undefined ? !this.framed : !!framed; + if ( framed !== this.framed ) { + this.framed = framed; + this.$element + .toggleClass( 'oo-ui-buttonedElement-frameless', !framed ) + .toggleClass( 'oo-ui-buttonedElement-framed', framed ); } return this; }; /** - * Clear all items. - * - * Items will be detached, not removed, so they can be used later. + * Set tab index. * + * @param {number|null} tabIndex Button's tab index, use null to remove * @chainable */ -OO.ui.GroupElement.prototype.clearItems = function () { - var i, len, item, remove, itemEvent; +OO.ui.ButtonedElement.prototype.setTabIndex = function ( tabIndex ) { + if ( typeof tabIndex === 'number' && tabIndex >= 0 ) { + this.$button.attr( 'tabindex', tabIndex ); + } else { + this.$button.removeAttr( 'tabindex' ); + } + return this; +}; - // Remove all items - for ( i = 0, len = this.items.length; i < len; i++ ) { - item = this.items[i]; - if ( - item.connect && item.disconnect && - !$.isEmptyObject( this.aggregateItemEvents ) - ) { - remove = {}; - if ( itemEvent in this.aggregateItemEvents ) { - remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ]; - } - item.disconnect( this, remove ); - } - item.setElementGroup( null ); - item.$element.detach(); +/** + * Set access key + * + * @param {string} accessKey Button's access key, use empty string to remove + * @chainable + */ +OO.ui.ButtonedElement.prototype.setAccessKey = function ( accessKey ) { + if ( typeof accessKey === 'string' && accessKey.length ) { + this.$button.attr( 'accesskey', accessKey ); + } else { + this.$button.removeAttr( 'accesskey' ); } + return this; +}; - this.items = []; +/** + * Set active state. + * + * @param {boolean} [value] Make button active + * @chainable + */ +OO.ui.ButtonedElement.prototype.setActive = function ( value ) { + this.$button.toggleClass( 'oo-ui-buttonedElement-active', !!value ); return this; }; /** - * Element containing an icon. + * Element that can be automatically clipped to visible boundaies. * * @abstract * @class * * @constructor - * @param {jQuery} $icon Icon node, assigned to #$icon + * @param {jQuery} $clippable Nodes to clip, assigned to #$clippable * @param {Object} [config] Configuration options - * @cfg {Object|string} [icon=''] Symbolic icon name, or map of icon names keyed by language ID; - * use the 'default' key to specify the icon to be used when there is no icon in the user's - * language */ -OO.ui.IconedElement = function OoUiIconedElement( $icon, config ) { - // Config intialization +OO.ui.ClippableElement = function OoUiClippableElement( $clippable, config ) { + // Configuration initialization config = config || {}; // Properties - this.$icon = $icon; - this.icon = null; + this.$clippable = $clippable; + this.clipping = false; + this.clipped = false; + this.$clippableContainer = null; + this.$clippableScroller = null; + this.$clippableWindow = null; + this.idealWidth = null; + this.idealHeight = null; + this.onClippableContainerScrollHandler = OO.ui.bind( this.clip, this ); + this.onClippableWindowResizeHandler = OO.ui.bind( this.clip, this ); // Initialization - this.$icon.addClass( 'oo-ui-iconedElement-icon' ); - this.setIcon( config.icon || this.constructor.static.icon ); + this.$clippable.addClass( 'oo-ui-clippableElement-clippable' ); }; -/* Setup */ +/* Methods */ -OO.initClass( OO.ui.IconedElement ); +/** + * Set clipping. + * + * @param {boolean} value Enable clipping + * @chainable + */ +OO.ui.ClippableElement.prototype.setClipping = function ( value ) { + value = !!value; -/* Static Properties */ + if ( this.clipping !== value ) { + this.clipping = value; + if ( this.clipping ) { + this.$clippableContainer = this.$( this.getClosestScrollableElementContainer() ); + // If the clippable container is the body, we have to listen to scroll events and check + // jQuery.scrollTop on the window because of browser inconsistencies + this.$clippableScroller = this.$clippableContainer.is( 'body' ) ? + this.$( OO.ui.Element.getWindow( this.$clippableContainer ) ) : + this.$clippableContainer; + this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler ); + this.$clippableWindow = this.$( this.getElementWindow() ) + .on( 'resize', this.onClippableWindowResizeHandler ); + // Initial clip after visible + setTimeout( OO.ui.bind( this.clip, this ) ); + } else { + this.$clippableContainer = null; + this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler ); + this.$clippableScroller = null; + this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler ); + this.$clippableWindow = null; + } + } + + return this; +}; /** - * Icon. - * - * Value should be the unique portion of an icon CSS class name, such as 'up' for 'oo-ui-icon-up'. - * - * For i18n purposes, this property can be an object containing a `default` icon name property and - * additional icon names keyed by language code. + * Check if the element will be clipped to fit the visible area of the nearest scrollable container. * - * Example of i18n icon definition: - * { 'default': 'bold-a', 'en': 'bold-b', 'de': 'bold-f' } + * @return {boolean} Element will be clipped to the visible area + */ +OO.ui.ClippableElement.prototype.isClipping = function () { + return this.clipping; +}; + +/** + * Check if the bottom or right of the element is being clipped by the nearest scrollable container. * - * @static - * @inheritable - * @property {Object|string} Symbolic icon name, or map of icon names keyed by language ID; - * use the 'default' key to specify the icon to be used when there is no icon in the user's - * language + * @return {boolean} Part of the element is being clipped */ -OO.ui.IconedElement.static.icon = null; +OO.ui.ClippableElement.prototype.isClipped = function () { + return this.clipped; +}; -/* Methods */ +/** + * Set the ideal size. + * + * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix + * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix + */ +OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) { + this.idealWidth = width; + this.idealHeight = height; +}; /** - * Set icon. + * Clip element to visible boundaries and allow scrolling when needed. + * + * Element will be clipped the bottom or right of the element is within 10px of the edge of, or + * overlapped by, the visible area of the nearest scrollable container. * - * @param {Object|string} icon Symbolic icon name, or map of icon names keyed by language ID; - * use the 'default' key to specify the icon to be used when there is no icon in the user's - * language * @chainable */ -OO.ui.IconedElement.prototype.setIcon = function ( icon ) { - icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon; +OO.ui.ClippableElement.prototype.clip = function () { + if ( !this.clipping ) { + // this.$clippableContainer and this.$clippableWindow are null, so the below will fail + return this; + } - if ( this.icon ) { - this.$icon.removeClass( 'oo-ui-icon-' + this.icon ); + var buffer = 10, + cOffset = this.$clippable.offset(), + ccOffset = this.$clippableContainer.offset() || { 'top': 0, 'left': 0 }, + ccHeight = this.$clippableContainer.innerHeight() - buffer, + ccWidth = this.$clippableContainer.innerWidth() - buffer, + scrollTop = this.$clippableScroller.scrollTop(), + scrollLeft = this.$clippableScroller.scrollLeft(), + desiredWidth = ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left, + desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top, + naturalWidth = this.$clippable.prop( 'scrollWidth' ), + naturalHeight = this.$clippable.prop( 'scrollHeight' ), + clipWidth = desiredWidth < naturalWidth, + clipHeight = desiredHeight < naturalHeight; + + if ( clipWidth ) { + this.$clippable.css( { 'overflow-x': 'auto', 'width': desiredWidth } ); + } else { + this.$clippable.css( 'width', this.idealWidth || '' ); + this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290 + this.$clippable.css( 'overflow-x', '' ); } - if ( typeof icon === 'string' ) { - icon = icon.trim(); - if ( icon.length ) { - this.$icon.addClass( 'oo-ui-icon-' + icon ); - this.icon = icon; - } + if ( clipHeight ) { + this.$clippable.css( { 'overflow-y': 'auto', 'height': desiredHeight } ); + } else { + this.$clippable.css( 'height', this.idealHeight || '' ); + this.$clippable.height(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290 + this.$clippable.css( 'overflow-y', '' ); } - this.$element.toggleClass( 'oo-ui-iconedElement', !!this.icon ); + + this.clipped = clipWidth || clipHeight; return this; }; /** - * Get icon. + * Element with named flags that can be added, removed, listed and checked. * - * @return {string} Icon - */ -OO.ui.IconedElement.prototype.getIcon = function () { - return this.icon; -}; - -/** - * Element containing an indicator. + * A flag, when set, adds a CSS class on the `$element` by combing `oo-ui-flaggableElement-` with + * the flag name. Flags are primarily useful for styling. * * @abstract * @class * * @constructor - * @param {jQuery} $indicator Indicator node, assigned to #$indicator * @param {Object} [config] Configuration options - * @cfg {string} [indicator] Symbolic indicator name - * @cfg {string} [indicatorTitle] Indicator title text or a function that return text + * @cfg {string[]} [flags=[]] Styling flags, e.g. 'primary', 'destructive' or 'constructive' */ -OO.ui.IndicatedElement = function OoUiIndicatedElement( $indicator, config ) { - // Config intialization +OO.ui.FlaggableElement = function OoUiFlaggableElement( config ) { + // Config initialization config = config || {}; // Properties - this.$indicator = $indicator; - this.indicator = null; - this.indicatorLabel = null; + this.flags = {}; // Initialization - this.$indicator.addClass( 'oo-ui-indicatedElement-indicator' ); - this.setIndicator( config.indicator || this.constructor.static.indicator ); - this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle ); + this.setFlags( config.flags ); }; -/* Setup */ +/* Events */ -OO.initClass( OO.ui.IndicatedElement ); +/** + * @event flag + * @param {Object.} changes Object keyed by flag name containing boolean + * added/removed properties + */ -/* Static Properties */ +/* Methods */ /** - * indicator. + * Check if a flag is set. * - * @static - * @inheritable - * @property {string|null} Symbolic indicator name or null for no indicator + * @param {string} flag Name of flag + * @return {boolean} Has flag */ -OO.ui.IndicatedElement.static.indicator = null; +OO.ui.FlaggableElement.prototype.hasFlag = function ( flag ) { + return flag in this.flags; +}; /** - * Indicator title. + * Get the names of all flags set. * - * @static - * @inheritable - * @property {string|Function|null} Indicator title text, a function that return text or null for no - * indicator title + * @return {string[]} flags Flag names */ -OO.ui.IndicatedElement.static.indicatorTitle = null; - -/* Methods */ +OO.ui.FlaggableElement.prototype.getFlags = function () { + return Object.keys( this.flags ); +}; /** - * Set indicator. + * Clear all flags. * - * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator * @chainable + * @fires flag */ -OO.ui.IndicatedElement.prototype.setIndicator = function ( indicator ) { - if ( this.indicator ) { - this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator ); - this.indicator = null; - } - if ( typeof indicator === 'string' ) { - indicator = indicator.trim(); - if ( indicator.length ) { - this.$indicator.addClass( 'oo-ui-indicator-' + indicator ); - this.indicator = indicator; - } +OO.ui.FlaggableElement.prototype.clearFlags = function () { + var flag, + changes = {}, + classPrefix = 'oo-ui-flaggableElement-'; + + for ( flag in this.flags ) { + changes[flag] = false; + delete this.flags[flag]; + this.$element.removeClass( classPrefix + flag ); } - this.$element.toggleClass( 'oo-ui-indicatedElement', !!this.indicator ); + + this.emit( 'flag', changes ); return this; }; /** - * Set indicator label. + * Add one or more flags. * - * @param {string|Function|null} indicator Indicator title text, a function that return text or null - * for no indicator title + * @param {string|string[]|Object.} flags One or more flags to add, or an object + * keyed by flag name containing boolean set/remove instructions. * @chainable + * @fires flag */ -OO.ui.IndicatedElement.prototype.setIndicatorTitle = function ( indicatorTitle ) { - this.indicatorTitle = indicatorTitle = OO.ui.resolveMsg( indicatorTitle ); +OO.ui.FlaggableElement.prototype.setFlags = function ( flags ) { + var i, len, flag, + changes = {}, + classPrefix = 'oo-ui-flaggableElement-'; - if ( typeof indicatorTitle === 'string' && indicatorTitle.length ) { - this.$indicator.attr( 'title', indicatorTitle ); - } else { - this.$indicator.removeAttr( 'title' ); + if ( typeof flags === 'string' ) { + // Set + this.flags[flags] = true; + this.$element.addClass( classPrefix + flags ); + } else if ( $.isArray( flags ) ) { + for ( i = 0, len = flags.length; i < len; i++ ) { + flag = flags[i]; + // Set + changes[flag] = true; + this.flags[flag] = true; + this.$element.addClass( classPrefix + flag ); + } + } else if ( OO.isPlainObject( flags ) ) { + for ( flag in flags ) { + if ( flags[flag] ) { + // Set + changes[flag] = true; + this.flags[flag] = true; + this.$element.addClass( classPrefix + flag ); + } else { + // Remove + changes[flag] = false; + delete this.flags[flag]; + this.$element.removeClass( classPrefix + flag ); + } + } } + this.emit( 'flag', changes ); + return this; }; /** - * Get indicator. + * Element containing a sequence of child elements. * - * @return {string} title Symbolic name of indicator + * @abstract + * @class + * + * @constructor + * @param {jQuery} $group Container node, assigned to #$group + * @param {Object} [config] Configuration options */ -OO.ui.IndicatedElement.prototype.getIndicator = function () { - return this.indicator; +OO.ui.GroupElement = function OoUiGroupElement( $group, config ) { + // Configuration + config = config || {}; + + // Properties + this.$group = $group; + this.items = []; + this.aggregateItemEvents = {}; }; +/* Methods */ + /** - * Get indicator title. + * Get items. * - * @return {string} Indicator title text + * @return {OO.ui.Element[]} Items */ -OO.ui.IndicatedElement.prototype.getIndicatorTitle = function () { - return this.indicatorTitle; +OO.ui.GroupElement.prototype.getItems = function () { + return this.items.slice( 0 ); }; /** - * Element containing a label. + * Add an aggregate item event. * - * @abstract - * @class + * Aggregated events are listened to on each item and then emitted by the group under a new name, + * and with an additional leading parameter containing the item that emitted the original event. + * Other arguments that were emitted from the original event are passed through. * - * @constructor - * @param {jQuery} $label Label node, assigned to #$label - * @param {Object} [config] Configuration options - * @cfg {jQuery|string|Function} [label] Label nodes, text or a function that returns nodes or text - * @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not. + * @param {Object.} events Aggregate events emitted by group, keyed by item + * event, use null value to remove aggregation + * @throws {Error} If aggregation already exists */ -OO.ui.LabeledElement = function OoUiLabeledElement( $label, config ) { - // Config intialization - config = config || {}; - - // Properties - this.$label = $label; - this.label = null; - - // Initialization - this.$label.addClass( 'oo-ui-labeledElement-label' ); - this.setLabel( config.label || this.constructor.static.label ); - this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel; -}; +OO.ui.GroupElement.prototype.aggregate = function ( events ) { + var i, len, item, add, remove, itemEvent, groupEvent; -/* Setup */ + for ( itemEvent in events ) { + groupEvent = events[itemEvent]; -OO.initClass( OO.ui.LabeledElement ); + // Remove existing aggregated event + if ( itemEvent in this.aggregateItemEvents ) { + // Don't allow duplicate aggregations + if ( groupEvent ) { + throw new Error( 'Duplicate item event aggregation for ' + itemEvent ); + } + // Remove event aggregation from existing items + for ( i = 0, len = this.items.length; i < len; i++ ) { + item = this.items[i]; + if ( item.connect && item.disconnect ) { + remove = {}; + remove[itemEvent] = [ 'emit', groupEvent, item ]; + item.disconnect( this, remove ); + } + } + // Prevent future items from aggregating event + delete this.aggregateItemEvents[itemEvent]; + } -/* Static Properties */ + // Add new aggregate event + if ( groupEvent ) { + // Make future items aggregate event + this.aggregateItemEvents[itemEvent] = groupEvent; + // Add event aggregation to existing items + for ( i = 0, len = this.items.length; i < len; i++ ) { + item = this.items[i]; + if ( item.connect && item.disconnect ) { + add = {}; + add[itemEvent] = [ 'emit', groupEvent, item ]; + item.connect( this, add ); + } + } + } + } +}; /** - * Label. + * Add items. * - * @static - * @inheritable - * @property {string|Function|null} Label text; a function that returns a nodes or text; or null for - * no label + * @param {OO.ui.Element[]} items Item + * @param {number} [index] Index to insert items at + * @chainable */ -OO.ui.LabeledElement.static.label = null; +OO.ui.GroupElement.prototype.addItems = function ( items, index ) { + var i, len, item, event, events, currentIndex, + itemElements = []; -/* Methods */ + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[i]; + + // Check if item exists then remove it first, effectively "moving" it + currentIndex = $.inArray( item, this.items ); + if ( currentIndex >= 0 ) { + this.removeItems( [ item ] ); + // Adjust index to compensate for removal + if ( currentIndex < index ) { + index--; + } + } + // Add the item + if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) { + events = {}; + for ( event in this.aggregateItemEvents ) { + events[event] = [ 'emit', this.aggregateItemEvents[event], item ]; + } + item.connect( this, events ); + } + item.setElementGroup( this ); + itemElements.push( item.$element.get( 0 ) ); + } + + if ( index === undefined || index < 0 || index >= this.items.length ) { + this.$group.append( itemElements ); + this.items.push.apply( this.items, items ); + } else if ( index === 0 ) { + this.$group.prepend( itemElements ); + this.items.unshift.apply( this.items, items ); + } else { + this.items[index].$element.before( itemElements ); + this.items.splice.apply( this.items, [ index, 0 ].concat( items ) ); + } + + return this; +}; /** - * Set the label. + * Remove items. * - * An empty string will result in the label being hidden. A string containing only whitespace will - * be converted to a single   + * Items will be detached, not removed, so they can be used later. * - * @param {jQuery|string|Function|null} label Label nodes; text; a function that retuns nodes or - * text; or null for no label + * @param {OO.ui.Element[]} items Items to remove * @chainable */ -OO.ui.LabeledElement.prototype.setLabel = function ( label ) { - var empty = false; +OO.ui.GroupElement.prototype.removeItems = function ( items ) { + var i, len, item, index, remove, itemEvent; - this.label = label = OO.ui.resolveMsg( label ) || null; - if ( typeof label === 'string' && label.length ) { - if ( label.match( /^\s*$/ ) ) { - // Convert whitespace only string to a single non-breaking space - this.$label.html( ' ' ); - } else { - this.$label.text( label ); + // Remove specific items + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[i]; + index = $.inArray( item, this.items ); + if ( index !== -1 ) { + if ( + item.connect && item.disconnect && + !$.isEmptyObject( this.aggregateItemEvents ) + ) { + remove = {}; + if ( itemEvent in this.aggregateItemEvents ) { + remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ]; + } + item.disconnect( this, remove ); + } + item.setElementGroup( null ); + this.items.splice( index, 1 ); + item.$element.detach(); } - } else if ( label instanceof jQuery ) { - this.$label.empty().append( label ); - } else { - this.$label.empty(); - empty = true; } - this.$element.toggleClass( 'oo-ui-labeledElement', !empty ); - this.$label.css( 'display', empty ? 'none' : '' ); return this; }; /** - * Get the label. + * Clear all items. * - * @return {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or - * text; or null for no label - */ -OO.ui.LabeledElement.prototype.getLabel = function () { - return this.label; -}; - -/** - * Fit the label. + * Items will be detached, not removed, so they can be used later. * * @chainable */ -OO.ui.LabeledElement.prototype.fitLabel = function () { - if ( this.$label.autoEllipsis && this.autoFitLabel ) { - this.$label.autoEllipsis( { 'hasSpan': false, 'tooltip': true } ); +OO.ui.GroupElement.prototype.clearItems = function () { + var i, len, item, remove, itemEvent; + + // Remove all items + for ( i = 0, len = this.items.length; i < len; i++ ) { + item = this.items[i]; + if ( + item.connect && item.disconnect && + !$.isEmptyObject( this.aggregateItemEvents ) + ) { + remove = {}; + if ( itemEvent in this.aggregateItemEvents ) { + remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ]; + } + item.disconnect( this, remove ); + } + item.setElementGroup( null ); + item.$element.detach(); } + + this.items = []; return this; }; /** - * Popuppable element. + * Element containing an icon. * * @abstract * @class * * @constructor + * @param {jQuery} $icon Icon node, assigned to #$icon * @param {Object} [config] Configuration options - * @cfg {number} [popupWidth=320] Width of popup - * @cfg {number} [popupHeight] Height of popup - * @cfg {Object} [popup] Configuration to pass to popup + * @cfg {Object|string} [icon=''] Symbolic icon name, or map of icon names keyed by language ID; + * use the 'default' key to specify the icon to be used when there is no icon in the user's + * language */ -OO.ui.PopuppableElement = function OoUiPopuppableElement( config ) { - // Configuration initialization - config = $.extend( { 'popupWidth': 320 }, config ); +OO.ui.IconedElement = function OoUiIconedElement( $icon, config ) { + // Config intialization + config = config || {}; // Properties - this.popup = new OO.ui.PopupWidget( $.extend( - { 'align': 'center', 'autoClose': true }, - config.popup, - { '$': this.$, '$autoCloseIgnore': this.$element } - ) ); - this.popupWidth = config.popupWidth; - this.popupHeight = config.popupHeight; + this.$icon = $icon; + this.icon = null; + + // Initialization + this.$icon.addClass( 'oo-ui-iconedElement-icon' ); + this.setIcon( config.icon || this.constructor.static.icon ); }; -/* Methods */ +/* Setup */ -/** - * Get popup. - * - * @return {OO.ui.PopupWidget} Popup widget - */ -OO.ui.PopuppableElement.prototype.getPopup = function () { - return this.popup; -}; +OO.initClass( OO.ui.IconedElement ); -/** - * Show popup. - */ -OO.ui.PopuppableElement.prototype.showPopup = function () { - this.popup.show().display( this.popupWidth, this.popupHeight ); -}; +/* Static Properties */ /** - * Hide popup. - */ -OO.ui.PopuppableElement.prototype.hidePopup = function () { - this.popup.hide(); -}; - -/** - * Element with a title. + * Icon. * - * @abstract - * @class + * Value should be the unique portion of an icon CSS class name, such as 'up' for 'oo-ui-icon-up'. * - * @constructor - * @param {jQuery} $label Titled node, assigned to #$titled - * @param {Object} [config] Configuration options - * @cfg {string|Function} [title] Title text or a function that returns text - */ -OO.ui.TitledElement = function OoUiTitledElement( $titled, config ) { - // Config intialization - config = config || {}; - - // Properties - this.$titled = $titled; - this.title = null; - - // Initialization - this.setTitle( config.title || this.constructor.static.title ); -}; - -/* Setup */ - -OO.initClass( OO.ui.TitledElement ); - -/* Static Properties */ - -/** - * Title. + * For i18n purposes, this property can be an object containing a `default` icon name property and + * additional icon names keyed by language code. + * + * Example of i18n icon definition: + * { 'default': 'bold-a', 'en': 'bold-b', 'de': 'bold-f' } * * @static * @inheritable - * @property {string|Function} Title text or a function that returns text + * @property {Object|string} Symbolic icon name, or map of icon names keyed by language ID; + * use the 'default' key to specify the icon to be used when there is no icon in the user's + * language */ -OO.ui.TitledElement.static.title = null; +OO.ui.IconedElement.static.icon = null; /* Methods */ /** - * Set title. + * Set icon. * - * @param {string|Function|null} title Title text, a function that returns text or null for no title + * @param {Object|string} icon Symbolic icon name, or map of icon names keyed by language ID; + * use the 'default' key to specify the icon to be used when there is no icon in the user's + * language * @chainable */ -OO.ui.TitledElement.prototype.setTitle = function ( title ) { - this.title = title = OO.ui.resolveMsg( title ) || null; +OO.ui.IconedElement.prototype.setIcon = function ( icon ) { + icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon; - if ( typeof title === 'string' && title.length ) { - this.$titled.attr( 'title', title ); - } else { - this.$titled.removeAttr( 'title' ); + if ( this.icon ) { + this.$icon.removeClass( 'oo-ui-icon-' + this.icon ); + } + if ( typeof icon === 'string' ) { + icon = icon.trim(); + if ( icon.length ) { + this.$icon.addClass( 'oo-ui-icon-' + icon ); + this.icon = icon; + } } + this.$element.toggleClass( 'oo-ui-iconedElement', !!this.icon ); return this; }; /** - * Get title. + * Get icon. * - * @return {string} Title string + * @return {string} Icon */ -OO.ui.TitledElement.prototype.getTitle = function () { - return this.title; +OO.ui.IconedElement.prototype.getIcon = function () { + return this.icon; }; /** - * Generic toolbar tool. + * Element containing an indicator. * * @abstract * @class - * @extends OO.ui.Widget - * @mixins OO.ui.IconedElement * * @constructor - * @param {OO.ui.ToolGroup} toolGroup + * @param {jQuery} $indicator Indicator node, assigned to #$indicator * @param {Object} [config] Configuration options - * @cfg {string|Function} [title] Title text or a function that returns text + * @cfg {string} [indicator] Symbolic indicator name + * @cfg {string} [indicatorTitle] Indicator title text or a function that return text */ -OO.ui.Tool = function OoUiTool( toolGroup, config ) { +OO.ui.IndicatedElement = function OoUiIndicatedElement( $indicator, config ) { // Config intialization config = config || {}; - // Parent constructor - OO.ui.Tool.super.call( this, config ); - - // Mixin constructors - OO.ui.IconedElement.call( this, this.$( '' ), config ); - // Properties - this.toolGroup = toolGroup; - this.toolbar = this.toolGroup.getToolbar(); - this.active = false; - this.$title = this.$( '' ); - this.$link = this.$( '' ); - this.title = null; - - // Events - this.toolbar.connect( this, { 'updateState': 'onUpdateState' } ); + this.$indicator = $indicator; + this.indicator = null; + this.indicatorLabel = null; // Initialization - this.$title.addClass( 'oo-ui-tool-title' ); - this.$link - .addClass( 'oo-ui-tool-link' ) - .append( this.$icon, this.$title ) - .prop( 'tabIndex', 0 ) - .attr( 'role', 'button' ); - this.$element - .data( 'oo-ui-tool', this ) - .addClass( - 'oo-ui-tool ' + 'oo-ui-tool-name-' + - this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' ) - ) - .append( this.$link ); - this.setTitle( config.title || this.constructor.static.title ); + this.$indicator.addClass( 'oo-ui-indicatedElement-indicator' ); + this.setIndicator( config.indicator || this.constructor.static.indicator ); + this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle ); }; /* Setup */ -OO.inheritClass( OO.ui.Tool, OO.ui.Widget ); -OO.mixinClass( OO.ui.Tool, OO.ui.IconedElement ); - -/* Events */ - -/** - * @event select - */ +OO.initClass( OO.ui.IndicatedElement ); /* Static Properties */ /** - * @static - * @inheritdoc - */ -OO.ui.Tool.static.tagName = 'span'; - -/** - * Symbolic name of tool. + * indicator. * - * @abstract * @static * @inheritable - * @property {string} + * @property {string|null} Symbolic indicator name or null for no indicator */ -OO.ui.Tool.static.name = ''; +OO.ui.IndicatedElement.static.indicator = null; /** - * Tool group. + * Indicator title. * - * @abstract * @static * @inheritable - * @property {string} + * @property {string|Function|null} Indicator title text, a function that return text or null for no + * indicator title */ -OO.ui.Tool.static.group = ''; +OO.ui.IndicatedElement.static.indicatorTitle = null; -/** - * Tool title. - * - * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool - * is part of a list or menu tool group. If a trigger is associated with an action by the same name - * as the tool, a description of its keyboard shortcut for the appropriate platform will be - * appended to the title if the tool is part of a bar tool group. - * - * @abstract - * @static - * @inheritable - * @property {string|Function} Title text or a function that returns text - */ -OO.ui.Tool.static.title = ''; +/* Methods */ /** - * Tool can be automatically added to catch-all groups. + * Set indicator. * - * @static - * @inheritable - * @property {boolean} + * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator + * @chainable */ -OO.ui.Tool.static.autoAddToCatchall = true; +OO.ui.IndicatedElement.prototype.setIndicator = function ( indicator ) { + if ( this.indicator ) { + this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator ); + this.indicator = null; + } + if ( typeof indicator === 'string' ) { + indicator = indicator.trim(); + if ( indicator.length ) { + this.$indicator.addClass( 'oo-ui-indicator-' + indicator ); + this.indicator = indicator; + } + } + this.$element.toggleClass( 'oo-ui-indicatedElement', !!this.indicator ); -/** - * Tool can be automatically added to named groups. - * - * @static - * @property {boolean} - * @inheritable - */ -OO.ui.Tool.static.autoAddToGroup = true; + return this; +}; /** - * Check if this tool is compatible with given data. + * Set indicator label. * - * @static - * @inheritable - * @param {Mixed} data Data to check - * @return {boolean} Tool can be used with data + * @param {string|Function|null} indicator Indicator title text, a function that return text or null + * for no indicator title + * @chainable */ -OO.ui.Tool.static.isCompatibleWith = function () { - return false; -}; +OO.ui.IndicatedElement.prototype.setIndicatorTitle = function ( indicatorTitle ) { + this.indicatorTitle = indicatorTitle = OO.ui.resolveMsg( indicatorTitle ); -/* Methods */ + if ( typeof indicatorTitle === 'string' && indicatorTitle.length ) { + this.$indicator.attr( 'title', indicatorTitle ); + } else { + this.$indicator.removeAttr( 'title' ); + } -/** - * Handle the toolbar state being updated. - * - * This is an abstract method that must be overridden in a concrete subclass. - * - * @abstract - */ -OO.ui.Tool.prototype.onUpdateState = function () { - throw new Error( - 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor - ); + return this; }; /** - * Handle the tool being selected. - * - * This is an abstract method that must be overridden in a concrete subclass. + * Get indicator. * - * @abstract + * @return {string} title Symbolic name of indicator */ -OO.ui.Tool.prototype.onSelect = function () { - throw new Error( - 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor - ); +OO.ui.IndicatedElement.prototype.getIndicator = function () { + return this.indicator; }; /** - * Check if the button is active. + * Get indicator title. * - * @param {boolean} Button is active + * @return {string} Indicator title text */ -OO.ui.Tool.prototype.isActive = function () { - return this.active; +OO.ui.IndicatedElement.prototype.getIndicatorTitle = function () { + return this.indicatorTitle; }; /** - * Make the button appear active or inactive. + * Element containing a label. * - * @param {boolean} state Make button appear active - */ -OO.ui.Tool.prototype.setActive = function ( state ) { - this.active = !!state; - if ( this.active ) { - this.$element.addClass( 'oo-ui-tool-active' ); - } else { - this.$element.removeClass( 'oo-ui-tool-active' ); - } + * @abstract + * @class + * + * @constructor + * @param {jQuery} $label Label node, assigned to #$label + * @param {Object} [config] Configuration options + * @cfg {jQuery|string|Function} [label] Label nodes, text or a function that returns nodes or text + * @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not. + */ +OO.ui.LabeledElement = function OoUiLabeledElement( $label, config ) { + // Config intialization + config = config || {}; + + // Properties + this.$label = $label; + this.label = null; + + // Initialization + this.$label.addClass( 'oo-ui-labeledElement-label' ); + this.setLabel( config.label || this.constructor.static.label ); + this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel; }; +/* Setup */ + +OO.initClass( OO.ui.LabeledElement ); + +/* Static Properties */ + /** - * Get the tool title. + * Label. * - * @param {string|Function} title Title text or a function that returns text + * @static + * @inheritable + * @property {string|Function|null} Label text; a function that returns a nodes or text; or null for + * no label + */ +OO.ui.LabeledElement.static.label = null; + +/* Methods */ + +/** + * Set the label. + * + * An empty string will result in the label being hidden. A string containing only whitespace will + * be converted to a single   + * + * @param {jQuery|string|Function|null} label Label nodes; text; a function that retuns nodes or + * text; or null for no label * @chainable */ -OO.ui.Tool.prototype.setTitle = function ( title ) { - this.title = OO.ui.resolveMsg( title ); - this.updateTitle(); +OO.ui.LabeledElement.prototype.setLabel = function ( label ) { + var empty = false; + + this.label = label = OO.ui.resolveMsg( label ) || null; + if ( typeof label === 'string' && label.length ) { + if ( label.match( /^\s*$/ ) ) { + // Convert whitespace only string to a single non-breaking space + this.$label.html( ' ' ); + } else { + this.$label.text( label ); + } + } else if ( label instanceof jQuery ) { + this.$label.empty().append( label ); + } else { + this.$label.empty(); + empty = true; + } + this.$element.toggleClass( 'oo-ui-labeledElement', !empty ); + this.$label.css( 'display', empty ? 'none' : '' ); + return this; }; /** - * Get the tool title. + * Get the label. * - * @return {string} Title text + * @return {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or + * text; or null for no label */ -OO.ui.Tool.prototype.getTitle = function () { - return this.title; +OO.ui.LabeledElement.prototype.getLabel = function () { + return this.label; }; /** - * Get the tool's symbolic name. + * Fit the label. * - * @return {string} Symbolic name of tool + * @chainable */ -OO.ui.Tool.prototype.getName = function () { - return this.constructor.static.name; +OO.ui.LabeledElement.prototype.fitLabel = function () { + if ( this.$label.autoEllipsis && this.autoFitLabel ) { + this.$label.autoEllipsis( { 'hasSpan': false, 'tooltip': true } ); + } + return this; }; /** - * Update the title. + * Popuppable element. + * + * @abstract + * @class + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {Object} [popup] Configuration to pass to popup + * @cfg {boolean} [autoClose=true] Popup auto-closes when it loses focus */ -OO.ui.Tool.prototype.updateTitle = function () { - var titleTooltips = this.toolGroup.constructor.static.titleTooltips, - accelTooltips = this.toolGroup.constructor.static.accelTooltips, - accel = this.toolbar.getToolAccelerator( this.constructor.static.name ), - tooltipParts = []; - - this.$title.empty() - .text( this.title ) - .append( - this.$( '' ) - .addClass( 'oo-ui-tool-accel' ) - .text( accel ) - ); +OO.ui.PopuppableElement = function OoUiPopuppableElement( config ) { + // Configuration initialization + config = config || {}; - if ( titleTooltips && typeof this.title === 'string' && this.title.length ) { - tooltipParts.push( this.title ); - } - if ( accelTooltips && typeof accel === 'string' && accel.length ) { - tooltipParts.push( accel ); - } - if ( tooltipParts.length ) { - this.$link.attr( 'title', tooltipParts.join( ' ' ) ); - } else { - this.$link.removeAttr( 'title' ); - } + // Properties + this.popup = new OO.ui.PopupWidget( $.extend( + { 'autoClose': true }, + config.popup, + { '$': this.$, '$autoCloseIgnore': this.$element } + ) ); }; +/* Methods */ + /** - * Destroy tool. + * Get popup. + * + * @return {OO.ui.PopupWidget} Popup widget */ -OO.ui.Tool.prototype.destroy = function () { - this.toolbar.disconnect( this ); - this.$element.remove(); +OO.ui.PopuppableElement.prototype.getPopup = function () { + return this.popup; }; /** - * Collection of tool groups. + * Element with a title. * + * @abstract * @class - * @extends OO.ui.Element - * @mixins OO.EventEmitter - * @mixins OO.ui.GroupElement * * @constructor - * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools - * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups + * @param {jQuery} $label Titled node, assigned to #$titled * @param {Object} [config] Configuration options - * @cfg {boolean} [actions] Add an actions section opposite to the tools - * @cfg {boolean} [shadow] Add a shadow below the toolbar + * @cfg {string|Function} [title] Title text or a function that returns text */ -OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) { - // Configuration initialization +OO.ui.TitledElement = function OoUiTitledElement( $titled, config ) { + // Config intialization config = config || {}; - // Parent constructor - OO.ui.Toolbar.super.call( this, config ); - - // Mixin constructors - OO.EventEmitter.call( this ); - OO.ui.GroupElement.call( this, this.$( '
' ), config ); - // Properties - this.toolFactory = toolFactory; - this.toolGroupFactory = toolGroupFactory; - this.groups = []; - this.tools = {}; - this.$bar = this.$( '
' ); - this.$actions = this.$( '
' ); - this.initialized = false; - - // Events - this.$element - .add( this.$bar ).add( this.$group ).add( this.$actions ) - .on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) ); + this.$titled = $titled; + this.title = null; // Initialization - this.$group.addClass( 'oo-ui-toolbar-tools' ); - this.$bar.addClass( 'oo-ui-toolbar-bar' ).append( this.$group ); - if ( config.actions ) { - this.$actions.addClass( 'oo-ui-toolbar-actions' ); - this.$bar.append( this.$actions ); - } - this.$bar.append( '
' ); - if ( config.shadow ) { - this.$bar.append( '
' ); - } - this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar ); + this.setTitle( config.title || this.constructor.static.title ); }; /* Setup */ -OO.inheritClass( OO.ui.Toolbar, OO.ui.Element ); -OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter ); -OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement ); +OO.initClass( OO.ui.TitledElement ); -/* Methods */ +/* Static Properties */ /** - * Get the tool factory. + * Title. * - * @return {OO.ui.ToolFactory} Tool factory + * @static + * @inheritable + * @property {string|Function} Title text or a function that returns text */ -OO.ui.Toolbar.prototype.getToolFactory = function () { - return this.toolFactory; -}; +OO.ui.TitledElement.static.title = null; -/** - * Get the tool group factory. - * - * @return {OO.Factory} Tool group factory - */ -OO.ui.Toolbar.prototype.getToolGroupFactory = function () { - return this.toolGroupFactory; -}; +/* Methods */ /** - * Handles mouse down events. + * Set title. * - * @param {jQuery.Event} e Mouse down event + * @param {string|Function|null} title Title text, a function that returns text or null for no title + * @chainable */ -OO.ui.Toolbar.prototype.onMouseDown = function ( e ) { - var $closestWidgetToEvent = this.$( e.target ).closest( '.oo-ui-widget' ), - $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' ); - if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[0] === $closestWidgetToToolbar[0] ) { - return false; +OO.ui.TitledElement.prototype.setTitle = function ( title ) { + this.title = title = OO.ui.resolveMsg( title ) || null; + + if ( typeof title === 'string' && title.length ) { + this.$titled.attr( 'title', title ); + } else { + this.$titled.removeAttr( 'title' ); } + + return this; }; /** - * Sets up handles and preloads required information for the toolbar to work. - * This must be called immediately after it is attached to a visible document. + * Get title. + * + * @return {string} Title string */ -OO.ui.Toolbar.prototype.initialize = function () { - this.initialized = true; +OO.ui.TitledElement.prototype.getTitle = function () { + return this.title; }; /** - * Setup toolbar. + * Generic toolbar tool. * - * Tools can be specified in the following ways: + * @abstract + * @class + * @extends OO.ui.Widget + * @mixins OO.ui.IconedElement * - * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'` - * - All tools in a group: `{ 'group': 'group-name' }` - * - All tools: `'*'` - Using this will make the group a list with a "More" label by default - * - * @param {Object.} groups List of tool group configurations - * @param {Array|string} [groups.include] Tools to include - * @param {Array|string} [groups.exclude] Tools to exclude - * @param {Array|string} [groups.promote] Tools to promote to the beginning - * @param {Array|string} [groups.demote] Tools to demote to the end + * @constructor + * @param {OO.ui.ToolGroup} toolGroup + * @param {Object} [config] Configuration options + * @cfg {string|Function} [title] Title text or a function that returns text */ -OO.ui.Toolbar.prototype.setup = function ( groups ) { - var i, len, type, group, - items = [], - defaultType = 'bar'; +OO.ui.Tool = function OoUiTool( toolGroup, config ) { + // Config intialization + config = config || {}; - // Cleanup previous groups - this.reset(); + // Parent constructor + OO.ui.Tool.super.call( this, config ); - // Build out new groups - for ( i = 0, len = groups.length; i < len; i++ ) { - group = groups[i]; - if ( group.include === '*' ) { - // Apply defaults to catch-all groups - if ( group.type === undefined ) { - group.type = 'list'; - } - if ( group.label === undefined ) { - group.label = 'ooui-toolbar-more'; - } - } - // Check type has been registered - type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType; - items.push( - this.getToolGroupFactory().create( type, this, $.extend( { '$': this.$ }, group ) ) - ); - } - this.addItems( items ); + // Mixin constructors + OO.ui.IconedElement.call( this, this.$( '' ), config ); + + // Properties + this.toolGroup = toolGroup; + this.toolbar = this.toolGroup.getToolbar(); + this.active = false; + this.$title = this.$( '' ); + this.$link = this.$( '
' ); + this.title = null; + + // Events + this.toolbar.connect( this, { 'updateState': 'onUpdateState' } ); + + // Initialization + this.$title.addClass( 'oo-ui-tool-title' ); + this.$link + .addClass( 'oo-ui-tool-link' ) + .append( this.$icon, this.$title ) + .prop( 'tabIndex', 0 ) + .attr( 'role', 'button' ); + this.$element + .data( 'oo-ui-tool', this ) + .addClass( + 'oo-ui-tool ' + 'oo-ui-tool-name-' + + this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' ) + ) + .append( this.$link ); + this.setTitle( config.title || this.constructor.static.title ); }; +/* Setup */ + +OO.inheritClass( OO.ui.Tool, OO.ui.Widget ); +OO.mixinClass( OO.ui.Tool, OO.ui.IconedElement ); + +/* Events */ + /** - * Remove all tools and groups from the toolbar. + * @event select */ -OO.ui.Toolbar.prototype.reset = function () { - var i, len; - this.groups = []; - this.tools = {}; - for ( i = 0, len = this.items.length; i < len; i++ ) { - this.items[i].destroy(); - } - this.clearItems(); -}; +/* Static Properties */ /** - * Destroys toolbar, removing event handlers and DOM elements. - * - * Call this whenever you are done using a toolbar. + * @static + * @inheritdoc */ -OO.ui.Toolbar.prototype.destroy = function () { - this.reset(); - this.$element.remove(); -}; +OO.ui.Tool.static.tagName = 'span'; /** - * Check if tool has not been used yet. + * Symbolic name of tool. * - * @param {string} name Symbolic name of tool - * @return {boolean} Tool is available + * @abstract + * @static + * @inheritable + * @property {string} */ -OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) { - return !this.tools[name]; -}; +OO.ui.Tool.static.name = ''; /** - * Prevent tool from being used again. + * Tool group. * - * @param {OO.ui.Tool} tool Tool to reserve + * @abstract + * @static + * @inheritable + * @property {string} */ -OO.ui.Toolbar.prototype.reserveTool = function ( tool ) { - this.tools[tool.getName()] = tool; -}; +OO.ui.Tool.static.group = ''; /** - * Allow tool to be used again. + * Tool title. * - * @param {OO.ui.Tool} tool Tool to release + * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool + * is part of a list or menu tool group. If a trigger is associated with an action by the same name + * as the tool, a description of its keyboard shortcut for the appropriate platform will be + * appended to the title if the tool is part of a bar tool group. + * + * @abstract + * @static + * @inheritable + * @property {string|Function} Title text or a function that returns text */ -OO.ui.Toolbar.prototype.releaseTool = function ( tool ) { - delete this.tools[tool.getName()]; -}; +OO.ui.Tool.static.title = ''; /** - * Get accelerator label for tool. + * Tool can be automatically added to catch-all groups. * - * This is a stub that should be overridden to provide access to accelerator information. + * @static + * @inheritable + * @property {boolean} + */ +OO.ui.Tool.static.autoAddToCatchall = true; + +/** + * Tool can be automatically added to named groups. * - * @param {string} name Symbolic name of tool - * @return {string|undefined} Tool accelerator label if available + * @static + * @property {boolean} + * @inheritable */ -OO.ui.Toolbar.prototype.getToolAccelerator = function () { - return undefined; -}; +OO.ui.Tool.static.autoAddToGroup = true; /** - * Factory for tools. + * Check if this tool is compatible with given data. * - * @class - * @extends OO.Factory - * @constructor + * @static + * @inheritable + * @param {Mixed} data Data to check + * @return {boolean} Tool can be used with data */ -OO.ui.ToolFactory = function OoUiToolFactory() { - // Parent constructor - OO.ui.ToolFactory.super.call( this ); +OO.ui.Tool.static.isCompatibleWith = function () { + return false; }; -/* Setup */ - -OO.inheritClass( OO.ui.ToolFactory, OO.Factory ); - /* Methods */ -/** */ -OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) { - var i, len, included, promoted, demoted, - auto = [], - used = {}; +/** + * Handle the toolbar state being updated. + * + * This is an abstract method that must be overridden in a concrete subclass. + * + * @abstract + */ +OO.ui.Tool.prototype.onUpdateState = function () { + throw new Error( + 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor + ); +}; - // Collect included and not excluded tools - included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) ); +/** + * Handle the tool being selected. + * + * This is an abstract method that must be overridden in a concrete subclass. + * + * @abstract + */ +OO.ui.Tool.prototype.onSelect = function () { + throw new Error( + 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor + ); +}; - // Promotion - promoted = this.extract( promote, used ); - demoted = this.extract( demote, used ); +/** + * Check if the button is active. + * + * @param {boolean} Button is active + */ +OO.ui.Tool.prototype.isActive = function () { + return this.active; +}; - // Auto - for ( i = 0, len = included.length; i < len; i++ ) { - if ( !used[included[i]] ) { - auto.push( included[i] ); - } +/** + * Make the button appear active or inactive. + * + * @param {boolean} state Make button appear active + */ +OO.ui.Tool.prototype.setActive = function ( state ) { + this.active = !!state; + if ( this.active ) { + this.$element.addClass( 'oo-ui-tool-active' ); + } else { + this.$element.removeClass( 'oo-ui-tool-active' ); } - - return promoted.concat( auto ).concat( demoted ); }; /** - * Get a flat list of names from a list of names or groups. - * - * Tools can be specified in the following ways: - * - * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'` - * - All tools in a group: `{ 'group': 'group-name' }` - * - All tools: `'*'` + * Get the tool title. * - * @private - * @param {Array|string} collection List of tools - * @param {Object} [used] Object with names that should be skipped as properties; extracted - * names will be added as properties - * @return {string[]} List of extracted names + * @param {string|Function} title Title text or a function that returns text + * @chainable */ -OO.ui.ToolFactory.prototype.extract = function ( collection, used ) { - var i, len, item, name, tool, - names = []; - - if ( collection === '*' ) { - for ( name in this.registry ) { - tool = this.registry[name]; - if ( - // Only add tools by group name when auto-add is enabled - tool.static.autoAddToCatchall && - // Exclude already used tools - ( !used || !used[name] ) - ) { - names.push( name ); - if ( used ) { - used[name] = true; - } - } - } - } else if ( $.isArray( collection ) ) { - for ( i = 0, len = collection.length; i < len; i++ ) { - item = collection[i]; - // Allow plain strings as shorthand for named tools - if ( typeof item === 'string' ) { - item = { 'name': item }; - } - if ( OO.isPlainObject( item ) ) { - if ( item.group ) { - for ( name in this.registry ) { - tool = this.registry[name]; - if ( - // Include tools with matching group - tool.static.group === item.group && - // Only add tools by group name when auto-add is enabled - tool.static.autoAddToGroup && - // Exclude already used tools - ( !used || !used[name] ) - ) { - names.push( name ); - if ( used ) { - used[name] = true; - } - } - } - // Include tools with matching name and exclude already used tools - } else if ( item.name && ( !used || !used[item.name] ) ) { - names.push( item.name ); - if ( used ) { - used[item.name] = true; - } - } - } - } - } - return names; +OO.ui.Tool.prototype.setTitle = function ( title ) { + this.title = OO.ui.resolveMsg( title ); + this.updateTitle(); + return this; }; /** - * Collection of tools. + * Get the tool title. * - * Tools can be specified in the following ways: + * @return {string} Title text + */ +OO.ui.Tool.prototype.getTitle = function () { + return this.title; +}; + +/** + * Get the tool's symbolic name. * - * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'` - * - All tools in a group: `{ 'group': 'group-name' }` - * - All tools: `'*'` + * @return {string} Symbolic name of tool + */ +OO.ui.Tool.prototype.getName = function () { + return this.constructor.static.name; +}; + +/** + * Update the title. + */ +OO.ui.Tool.prototype.updateTitle = function () { + var titleTooltips = this.toolGroup.constructor.static.titleTooltips, + accelTooltips = this.toolGroup.constructor.static.accelTooltips, + accel = this.toolbar.getToolAccelerator( this.constructor.static.name ), + tooltipParts = []; + + this.$title.empty() + .text( this.title ) + .append( + this.$( '' ) + .addClass( 'oo-ui-tool-accel' ) + .text( accel ) + ); + + if ( titleTooltips && typeof this.title === 'string' && this.title.length ) { + tooltipParts.push( this.title ); + } + if ( accelTooltips && typeof accel === 'string' && accel.length ) { + tooltipParts.push( accel ); + } + if ( tooltipParts.length ) { + this.$link.attr( 'title', tooltipParts.join( ' ' ) ); + } else { + this.$link.removeAttr( 'title' ); + } +}; + +/** + * Destroy tool. + */ +OO.ui.Tool.prototype.destroy = function () { + this.toolbar.disconnect( this ); + this.$element.remove(); +}; + +/** + * Collection of tool groups. * - * @abstract * @class - * @extends OO.ui.Widget + * @extends OO.ui.Element + * @mixins OO.EventEmitter * @mixins OO.ui.GroupElement * * @constructor - * @param {OO.ui.Toolbar} toolbar + * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools + * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups * @param {Object} [config] Configuration options - * @cfg {Array|string} [include=[]] List of tools to include - * @cfg {Array|string} [exclude=[]] List of tools to exclude - * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning - * @cfg {Array|string} [demote=[]] List of tools to demote to the end + * @cfg {boolean} [actions] Add an actions section opposite to the tools + * @cfg {boolean} [shadow] Add a shadow below the toolbar */ -OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) { +OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) { // Configuration initialization config = config || {}; // Parent constructor - OO.ui.ToolGroup.super.call( this, config ); + OO.ui.Toolbar.super.call( this, config ); // Mixin constructors + OO.EventEmitter.call( this ); OO.ui.GroupElement.call( this, this.$( '
' ), config ); // Properties - this.toolbar = toolbar; + this.toolFactory = toolFactory; + this.toolGroupFactory = toolGroupFactory; + this.groups = []; this.tools = {}; - this.pressed = null; - this.autoDisabled = false; - this.include = config.include || []; - this.exclude = config.exclude || []; - this.promote = config.promote || []; - this.demote = config.demote || []; - this.onCapturedMouseUpHandler = OO.ui.bind( this.onCapturedMouseUp, this ); + this.$bar = this.$( '
' ); + this.$actions = this.$( '
' ); + this.initialized = false; // Events - this.$element.on( { - 'mousedown': OO.ui.bind( this.onMouseDown, this ), - 'mouseup': OO.ui.bind( this.onMouseUp, this ), - 'mouseover': OO.ui.bind( this.onMouseOver, this ), - 'mouseout': OO.ui.bind( this.onMouseOut, this ) - } ); - this.toolbar.getToolFactory().connect( this, { 'register': 'onToolFactoryRegister' } ); - this.aggregate( { 'disable': 'itemDisable' } ); - this.connect( this, { 'itemDisable': 'updateDisabled' } ); + this.$element + .add( this.$bar ).add( this.$group ).add( this.$actions ) + .on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) ); // Initialization - this.$group.addClass( 'oo-ui-toolGroup-tools' ); - this.$element - .addClass( 'oo-ui-toolGroup' ) - .append( this.$group ); - this.populate(); + this.$group.addClass( 'oo-ui-toolbar-tools' ); + this.$bar.addClass( 'oo-ui-toolbar-bar' ).append( this.$group ); + if ( config.actions ) { + this.$actions.addClass( 'oo-ui-toolbar-actions' ); + this.$bar.append( this.$actions ); + } + this.$bar.append( '
' ); + if ( config.shadow ) { + this.$bar.append( '
' ); + } + this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar ); }; /* Setup */ -OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget ); -OO.mixinClass( OO.ui.ToolGroup, OO.ui.GroupElement ); - -/* Events */ - -/** - * @event update - */ +OO.inheritClass( OO.ui.Toolbar, OO.ui.Element ); +OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter ); +OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement ); -/* Static Properties */ +/* Methods */ /** - * Show labels in tooltips. + * Get the tool factory. * - * @static - * @inheritable - * @property {boolean} + * @return {OO.ui.ToolFactory} Tool factory */ -OO.ui.ToolGroup.static.titleTooltips = false; +OO.ui.Toolbar.prototype.getToolFactory = function () { + return this.toolFactory; +}; /** - * Show acceleration labels in tooltips. + * Get the tool group factory. * - * @static - * @inheritable - * @property {boolean} + * @return {OO.Factory} Tool group factory */ -OO.ui.ToolGroup.static.accelTooltips = false; +OO.ui.Toolbar.prototype.getToolGroupFactory = function () { + return this.toolGroupFactory; +}; /** - * Automatically disable the toolgroup when all tools are disabled + * Handles mouse down events. * - * @static - * @inheritable - * @property {boolean} - */ -OO.ui.ToolGroup.static.autoDisable = true; - -/* Methods */ - -/** - * @inheritdoc + * @param {jQuery.Event} e Mouse down event */ -OO.ui.ToolGroup.prototype.isDisabled = function () { - return this.autoDisabled || OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments ); +OO.ui.Toolbar.prototype.onMouseDown = function ( e ) { + var $closestWidgetToEvent = this.$( e.target ).closest( '.oo-ui-widget' ), + $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' ); + if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[0] === $closestWidgetToToolbar[0] ) { + return false; + } }; /** - * @inheritdoc + * Sets up handles and preloads required information for the toolbar to work. + * This must be called immediately after it is attached to a visible document. */ -OO.ui.ToolGroup.prototype.updateDisabled = function () { - var i, item, allDisabled = true; - - if ( this.constructor.static.autoDisable ) { - for ( i = this.items.length - 1; i >= 0; i-- ) { - item = this.items[i]; - if ( !item.isDisabled() ) { - allDisabled = false; - break; - } - } - this.autoDisabled = allDisabled; - } - OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments ); +OO.ui.Toolbar.prototype.initialize = function () { + this.initialized = true; }; /** - * Handle mouse down events. + * Setup toolbar. * - * @param {jQuery.Event} e Mouse down event - */ -OO.ui.ToolGroup.prototype.onMouseDown = function ( e ) { - if ( !this.isDisabled() && e.which === 1 ) { - this.pressed = this.getTargetTool( e ); + * Tools can be specified in the following ways: + * + * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'` + * - All tools in a group: `{ 'group': 'group-name' }` + * - All tools: `'*'` - Using this will make the group a list with a "More" label by default + * + * @param {Object.} groups List of tool group configurations + * @param {Array|string} [groups.include] Tools to include + * @param {Array|string} [groups.exclude] Tools to exclude + * @param {Array|string} [groups.promote] Tools to promote to the beginning + * @param {Array|string} [groups.demote] Tools to demote to the end + */ +OO.ui.Toolbar.prototype.setup = function ( groups ) { + var i, len, type, group, + items = [], + defaultType = 'bar'; + + // Cleanup previous groups + this.reset(); + + // Build out new groups + for ( i = 0, len = groups.length; i < len; i++ ) { + group = groups[i]; + if ( group.include === '*' ) { + // Apply defaults to catch-all groups + if ( group.type === undefined ) { + group.type = 'list'; + } + if ( group.label === undefined ) { + group.label = 'ooui-toolbar-more'; + } + } + // Check type has been registered + type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType; + items.push( + this.getToolGroupFactory().create( type, this, $.extend( { '$': this.$ }, group ) ) + ); + } + this.addItems( items ); +}; + +/** + * Remove all tools and groups from the toolbar. + */ +OO.ui.Toolbar.prototype.reset = function () { + var i, len; + + this.groups = []; + this.tools = {}; + for ( i = 0, len = this.items.length; i < len; i++ ) { + this.items[i].destroy(); + } + this.clearItems(); +}; + +/** + * Destroys toolbar, removing event handlers and DOM elements. + * + * Call this whenever you are done using a toolbar. + */ +OO.ui.Toolbar.prototype.destroy = function () { + this.reset(); + this.$element.remove(); +}; + +/** + * Check if tool has not been used yet. + * + * @param {string} name Symbolic name of tool + * @return {boolean} Tool is available + */ +OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) { + return !this.tools[name]; +}; + +/** + * Prevent tool from being used again. + * + * @param {OO.ui.Tool} tool Tool to reserve + */ +OO.ui.Toolbar.prototype.reserveTool = function ( tool ) { + this.tools[tool.getName()] = tool; +}; + +/** + * Allow tool to be used again. + * + * @param {OO.ui.Tool} tool Tool to release + */ +OO.ui.Toolbar.prototype.releaseTool = function ( tool ) { + delete this.tools[tool.getName()]; +}; + +/** + * Get accelerator label for tool. + * + * This is a stub that should be overridden to provide access to accelerator information. + * + * @param {string} name Symbolic name of tool + * @return {string|undefined} Tool accelerator label if available + */ +OO.ui.Toolbar.prototype.getToolAccelerator = function () { + return undefined; +}; + +/** + * Collection of tools. + * + * Tools can be specified in the following ways: + * + * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'` + * - All tools in a group: `{ 'group': 'group-name' }` + * - All tools: `'*'` + * + * @abstract + * @class + * @extends OO.ui.Widget + * @mixins OO.ui.GroupElement + * + * @constructor + * @param {OO.ui.Toolbar} toolbar + * @param {Object} [config] Configuration options + * @cfg {Array|string} [include=[]] List of tools to include + * @cfg {Array|string} [exclude=[]] List of tools to exclude + * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning + * @cfg {Array|string} [demote=[]] List of tools to demote to the end + */ +OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.ToolGroup.super.call( this, config ); + + // Mixin constructors + OO.ui.GroupElement.call( this, this.$( '
' ), config ); + + // Properties + this.toolbar = toolbar; + this.tools = {}; + this.pressed = null; + this.autoDisabled = false; + this.include = config.include || []; + this.exclude = config.exclude || []; + this.promote = config.promote || []; + this.demote = config.demote || []; + this.onCapturedMouseUpHandler = OO.ui.bind( this.onCapturedMouseUp, this ); + + // Events + this.$element.on( { + 'mousedown': OO.ui.bind( this.onMouseDown, this ), + 'mouseup': OO.ui.bind( this.onMouseUp, this ), + 'mouseover': OO.ui.bind( this.onMouseOver, this ), + 'mouseout': OO.ui.bind( this.onMouseOut, this ) + } ); + this.toolbar.getToolFactory().connect( this, { 'register': 'onToolFactoryRegister' } ); + this.aggregate( { 'disable': 'itemDisable' } ); + this.connect( this, { 'itemDisable': 'updateDisabled' } ); + + // Initialization + this.$group.addClass( 'oo-ui-toolGroup-tools' ); + this.$element + .addClass( 'oo-ui-toolGroup' ) + .append( this.$group ); + this.populate(); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget ); +OO.mixinClass( OO.ui.ToolGroup, OO.ui.GroupElement ); + +/* Events */ + +/** + * @event update + */ + +/* Static Properties */ + +/** + * Show labels in tooltips. + * + * @static + * @inheritable + * @property {boolean} + */ +OO.ui.ToolGroup.static.titleTooltips = false; + +/** + * Show acceleration labels in tooltips. + * + * @static + * @inheritable + * @property {boolean} + */ +OO.ui.ToolGroup.static.accelTooltips = false; + +/** + * Automatically disable the toolgroup when all tools are disabled + * + * @static + * @inheritable + * @property {boolean} + */ +OO.ui.ToolGroup.static.autoDisable = true; + +/* Methods */ + +/** + * @inheritdoc + */ +OO.ui.ToolGroup.prototype.isDisabled = function () { + return this.autoDisabled || OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments ); +}; + +/** + * @inheritdoc + */ +OO.ui.ToolGroup.prototype.updateDisabled = function () { + var i, item, allDisabled = true; + + if ( this.constructor.static.autoDisable ) { + for ( i = this.items.length - 1; i >= 0; i-- ) { + item = this.items[i]; + if ( !item.isDisabled() ) { + allDisabled = false; + break; + } + } + this.autoDisabled = allDisabled; + } + OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments ); +}; + +/** + * Handle mouse down events. + * + * @param {jQuery.Event} e Mouse down event + */ +OO.ui.ToolGroup.prototype.onMouseDown = function ( e ) { + if ( !this.isDisabled() && e.which === 1 ) { + this.pressed = this.getTargetTool( e ); if ( this.pressed ) { this.pressed.setActive( true ); this.getElementDocument().addEventListener( @@ -3980,367 +5084,440 @@ OO.ui.ToolGroup.prototype.destroy = function () { }; /** - * Factory for tool groups. + * Dialog for showing a message. + * + * User interface: + * - Registers two actions by default (safe and primary). + * - Renders action widgets in the footer. * * @class - * @extends OO.Factory + * @extends OO.ui.Dialog + * * @constructor + * @param {Object} [config] Configuration options */ -OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() { +OO.ui.MessageDialog = function OoUiMessageDialog( manager, config ) { // Parent constructor - OO.Factory.call( this ); + OO.ui.MessageDialog.super.call( this, manager, config ); - var i, l, - defaultClasses = this.constructor.static.getDefaultClasses(); + // Properties + this.verticalActionLayout = null; - // Register default toolgroups - for ( i = 0, l = defaultClasses.length; i < l; i++ ) { - this.register( defaultClasses[i] ); - } + // Initialization + this.$element.addClass( 'oo-ui-messageDialog' ); }; -/* Setup */ +/* Inheritance */ -OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory ); +OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog ); -/* Static Methods */ +/* Static Properties */ + +OO.ui.MessageDialog.static.name = 'message'; + +OO.ui.MessageDialog.static.size = 'small'; + +OO.ui.MessageDialog.static.verbose = false; /** - * Get a default set of classes to be registered on construction + * Dialog title. * - * @return {Function[]} Default classes + * A confirmation dialog's title should describe what the progressive action will do. An alert + * dialog's title should describe what event occured. + * + * @static + * inheritable + * @property {jQuery|string|Function|null} */ -OO.ui.ToolGroupFactory.static.getDefaultClasses = function () { - return [ - OO.ui.BarToolGroup, - OO.ui.ListToolGroup, - OO.ui.MenuToolGroup - ]; -}; +OO.ui.MessageDialog.static.title = null; /** - * Layout made of a fieldset and optional legend. - * - * Just add OO.ui.FieldLayout items. - * - * @class - * @extends OO.ui.Layout - * @mixins OO.ui.LabeledElement - * @mixins OO.ui.IconedElement - * @mixins OO.ui.GroupElement + * A confirmation dialog's message should describe the consequences of the progressive action. An + * alert dialog's message should describe why the event occured. * - * @constructor - * @param {Object} [config] Configuration options - * @cfg {string} [icon] Symbolic icon name - * @cfg {OO.ui.FieldLayout[]} [items] Items to add + * @static + * inheritable + * @property {jQuery|string|Function|null} */ -OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) { - // Config initialization - config = config || {}; +OO.ui.MessageDialog.static.message = null; - // Parent constructor - OO.ui.FieldsetLayout.super.call( this, config ); +OO.ui.MessageDialog.static.actions = [ + { 'label': OO.ui.deferMsg( 'ooui-dialog-message-accept' ), 'flags': 'primary' }, + { 'label': OO.ui.deferMsg( 'ooui-dialog-message-reject' ), 'flags': 'safe' } +]; - // Mixin constructors - OO.ui.IconedElement.call( this, this.$( '
' ), config ); - OO.ui.LabeledElement.call( this, this.$( '
' ), config ); - OO.ui.GroupElement.call( this, this.$( '
' ), config ); +/* Methods */ - // Initialization - this.$element - .addClass( 'oo-ui-fieldsetLayout' ) - .prepend( this.$icon, this.$label, this.$group ); - if ( $.isArray( config.items ) ) { - this.addItems( config.items ); - } +/** + * @inheritdoc + */ +OO.ui.MessageDialog.prototype.onActionResize = function ( action ) { + this.fitActions(); + return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action ); }; -/* Setup */ - -OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout ); -OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconedElement ); -OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabeledElement ); -OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement ); - -/* Static Properties */ - -OO.ui.FieldsetLayout.static.tagName = 'div'; - /** - * Layout made of a field and optional label. - * - * @class - * @extends OO.ui.Layout - * @mixins OO.ui.LabeledElement - * - * Available label alignment modes include: - * - 'left': Label is before the field and aligned away from it, best for when the user will be - * scanning for a specific label in a form with many fields - * - 'right': Label is before the field and aligned toward it, best for forms the user is very - * familiar with and will tab through field checking quickly to verify which field they are in - * - 'top': Label is before the field and above it, best for when the use will need to fill out all - * fields from top to bottom in a form with few fields - * - 'inline': Label is after the field and aligned toward it, best for small boolean fields like - * checkboxes or radio buttons + * Toggle action layout between vertical and horizontal. * - * @constructor - * @param {OO.ui.Widget} field Field widget - * @param {Object} [config] Configuration options - * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline' + * @param {boolean} [value] Layout actions vertically, omit to toggle + * @chainable */ -OO.ui.FieldLayout = function OoUiFieldLayout( field, config ) { - // Config initialization - config = $.extend( { 'align': 'left' }, config ); - - // Parent constructor - OO.ui.FieldLayout.super.call( this, config ); - - // Mixin constructors - OO.ui.LabeledElement.call( this, this.$( '