Adding mapbox-gl branch

This commit is contained in:
Andreas Hocevar
2015-03-16 18:50:27 +01:00
parent 7985f030fa
commit 57ee7f52fd
3109 changed files with 943365 additions and 0 deletions
@@ -0,0 +1,444 @@
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Wrapper around {@link goog.ui.Dialog}, to provide
* dialogs that are smarter about interacting with a rich text editor.
*
* @author nicksantos@google.com (Nick Santos)
*/
goog.provide('goog.ui.editor.AbstractDialog');
goog.provide('goog.ui.editor.AbstractDialog.Builder');
goog.provide('goog.ui.editor.AbstractDialog.EventType');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.classlist');
goog.require('goog.events.EventTarget');
goog.require('goog.string');
goog.require('goog.ui.Dialog');
goog.require('goog.ui.PopupBase');
// *** Public interface ***************************************************** //
/**
* Creates an object that represents a dialog box.
* @param {goog.dom.DomHelper} domHelper DomHelper to be used to create the
* dialog's dom structure.
* @constructor
* @extends {goog.events.EventTarget}
*/
goog.ui.editor.AbstractDialog = function(domHelper) {
goog.events.EventTarget.call(this);
this.dom = domHelper;
};
goog.inherits(goog.ui.editor.AbstractDialog, goog.events.EventTarget);
/**
* Causes the dialog box to appear, centered on the screen. Lazily creates the
* dialog if needed.
*/
goog.ui.editor.AbstractDialog.prototype.show = function() {
// Lazily create the wrapped dialog to be shown.
if (!this.dialogInternal_) {
this.dialogInternal_ = this.createDialogControl();
this.dialogInternal_.listen(goog.ui.PopupBase.EventType.HIDE,
this.handleAfterHide_, false, this);
}
this.dialogInternal_.setVisible(true);
};
/**
* Hides the dialog, causing AFTER_HIDE to fire.
*/
goog.ui.editor.AbstractDialog.prototype.hide = function() {
if (this.dialogInternal_) {
// This eventually fires the wrapped dialog's AFTER_HIDE event, calling our
// handleAfterHide_().
this.dialogInternal_.setVisible(false);
}
};
/**
* @return {boolean} Whether the dialog is open.
*/
goog.ui.editor.AbstractDialog.prototype.isOpen = function() {
return !!this.dialogInternal_ && this.dialogInternal_.isVisible();
};
/**
* Runs the handler registered on the OK button event and closes the dialog if
* that handler succeeds.
* This is useful in cases such as double-clicking an item in the dialog is
* equivalent to selecting it and clicking the default button.
* @protected
*/
goog.ui.editor.AbstractDialog.prototype.processOkAndClose = function() {
// Fake an OK event from the wrapped dialog control.
var evt = new goog.ui.Dialog.Event(goog.ui.Dialog.DefaultButtonKeys.OK, null);
if (this.handleOk(evt)) {
// handleOk calls dispatchEvent, so if any listener calls preventDefault it
// will return false and we won't hide the dialog.
this.hide();
}
};
// *** Dialog events ******************************************************** //
/**
* Event type constants for events the dialog fires.
* @enum {string}
*/
goog.ui.editor.AbstractDialog.EventType = {
// This event is fired after the dialog is hidden, no matter if it was closed
// via OK or Cancel or is being disposed without being hidden first.
AFTER_HIDE: 'afterhide',
// Either the cancel or OK events can be canceled via preventDefault or by
// returning false from their handlers to stop the dialog from closing.
CANCEL: 'cancel',
OK: 'ok'
};
// *** Inner helper class *************************************************** //
/**
* A builder class for the dialog control. All methods except build return this.
* @param {goog.ui.editor.AbstractDialog} editorDialog Editor dialog object
* that will wrap the wrapped dialog object this builder will create.
* @constructor
*/
goog.ui.editor.AbstractDialog.Builder = function(editorDialog) {
// We require the editor dialog to be passed in so that the builder can set up
// ok/cancel listeners by default, making it easier for most dialogs.
this.editorDialog_ = editorDialog;
this.wrappedDialog_ = new goog.ui.Dialog('', true, this.editorDialog_.dom);
this.buttonSet_ = new goog.ui.Dialog.ButtonSet(this.editorDialog_.dom);
this.buttonHandlers_ = {};
this.addClassName(goog.getCssName('tr-dialog'));
};
/**
* Sets the title of the dialog.
* @param {string} title Title HTML (escaped).
* @return {!goog.ui.editor.AbstractDialog.Builder} This.
*/
goog.ui.editor.AbstractDialog.Builder.prototype.setTitle = function(title) {
this.wrappedDialog_.setTitle(title);
return this;
};
/**
* Adds an OK button to the dialog. Clicking this button will cause {@link
* handleOk} to run, subsequently dispatching an OK event.
* @param {string=} opt_label The caption for the button, if not "OK".
* @return {!goog.ui.editor.AbstractDialog.Builder} This.
*/
goog.ui.editor.AbstractDialog.Builder.prototype.addOkButton =
function(opt_label) {
var key = goog.ui.Dialog.DefaultButtonKeys.OK;
/** @desc Label for an OK button in an editor dialog. */
var MSG_TR_DIALOG_OK = goog.getMsg('OK');
// True means this is the default/OK button.
this.buttonSet_.set(key, opt_label || MSG_TR_DIALOG_OK, true);
this.buttonHandlers_[key] = goog.bind(this.editorDialog_.handleOk,
this.editorDialog_);
return this;
};
/**
* Adds a Cancel button to the dialog. Clicking this button will cause {@link
* handleCancel} to run, subsequently dispatching a CANCEL event.
* @param {string=} opt_label The caption for the button, if not "Cancel".
* @return {!goog.ui.editor.AbstractDialog.Builder} This.
*/
goog.ui.editor.AbstractDialog.Builder.prototype.addCancelButton =
function(opt_label) {
var key = goog.ui.Dialog.DefaultButtonKeys.CANCEL;
/** @desc Label for a cancel button in an editor dialog. */
var MSG_TR_DIALOG_CANCEL = goog.getMsg('Cancel');
// False means it's not the OK button, true means it's the Cancel button.
this.buttonSet_.set(key, opt_label || MSG_TR_DIALOG_CANCEL, false, true);
this.buttonHandlers_[key] = goog.bind(this.editorDialog_.handleCancel,
this.editorDialog_);
return this;
};
/**
* Adds a custom button to the dialog.
* @param {string} label The caption for the button.
* @param {function(goog.ui.Dialog.EventType):*} handler Function called when
* the button is clicked. It is recommended that this function be a method
* in the concrete subclass of AbstractDialog using this Builder, and that
* it dispatch an event (see {@link handleOk}).
* @param {string=} opt_buttonId Identifier to be used to access the button when
* calling AbstractDialog.getButtonElement().
* @return {!goog.ui.editor.AbstractDialog.Builder} This.
*/
goog.ui.editor.AbstractDialog.Builder.prototype.addButton =
function(label, handler, opt_buttonId) {
// We don't care what the key is, just that we can match the button with the
// handler function later.
var key = opt_buttonId || goog.string.createUniqueString();
this.buttonSet_.set(key, label);
this.buttonHandlers_[key] = handler;
return this;
};
/**
* Puts a CSS class on the dialog's main element.
* @param {string} className The class to add.
* @return {!goog.ui.editor.AbstractDialog.Builder} This.
*/
goog.ui.editor.AbstractDialog.Builder.prototype.addClassName =
function(className) {
goog.dom.classlist.add(
goog.asserts.assert(this.wrappedDialog_.getDialogElement()), className);
return this;
};
/**
* Sets the content element of the dialog.
* @param {Element} contentElem An element for the main body.
* @return {!goog.ui.editor.AbstractDialog.Builder} This.
*/
goog.ui.editor.AbstractDialog.Builder.prototype.setContent =
function(contentElem) {
goog.dom.appendChild(this.wrappedDialog_.getContentElement(), contentElem);
return this;
};
/**
* Builds the wrapped dialog control. May only be called once, after which
* no more methods may be called on this builder.
* @return {!goog.ui.Dialog} The wrapped dialog control.
*/
goog.ui.editor.AbstractDialog.Builder.prototype.build = function() {
if (this.buttonSet_.isEmpty()) {
// If caller didn't set any buttons, add an OK and Cancel button by default.
this.addOkButton();
this.addCancelButton();
}
this.wrappedDialog_.setButtonSet(this.buttonSet_);
var handlers = this.buttonHandlers_;
this.buttonHandlers_ = null;
this.wrappedDialog_.listen(goog.ui.Dialog.EventType.SELECT,
// Listen for the SELECT event, which means a button was clicked, and
// call the handler associated with that button via the key property.
function(e) {
if (handlers[e.key]) {
return handlers[e.key](e);
}
});
// All editor dialogs are modal.
this.wrappedDialog_.setModal(true);
var dialog = this.wrappedDialog_;
this.wrappedDialog_ = null;
return dialog;
};
/**
* Editor dialog that will wrap the wrapped dialog this builder will create.
* @type {goog.ui.editor.AbstractDialog}
* @private
*/
goog.ui.editor.AbstractDialog.Builder.prototype.editorDialog_;
/**
* wrapped dialog control being built by this builder.
* @type {goog.ui.Dialog}
* @private
*/
goog.ui.editor.AbstractDialog.Builder.prototype.wrappedDialog_;
/**
* Set of buttons to be added to the wrapped dialog control.
* @type {goog.ui.Dialog.ButtonSet}
* @private
*/
goog.ui.editor.AbstractDialog.Builder.prototype.buttonSet_;
/**
* Map from keys that will be returned in the wrapped dialog SELECT events to
* handler functions to be called to handle those events.
* @type {Object}
* @private
*/
goog.ui.editor.AbstractDialog.Builder.prototype.buttonHandlers_;
// *** Protected interface ************************************************** //
/**
* The DOM helper for the parent document.
* @type {goog.dom.DomHelper}
* @protected
*/
goog.ui.editor.AbstractDialog.prototype.dom;
/**
* Creates and returns the goog.ui.Dialog control that is being wrapped
* by this object.
* @return {!goog.ui.Dialog} Created Dialog control.
* @protected
*/
goog.ui.editor.AbstractDialog.prototype.createDialogControl =
goog.abstractMethod;
/**
* Returns the HTML Button element for the OK button in this dialog.
* @return {Element} The button element if found, else null.
* @protected
*/
goog.ui.editor.AbstractDialog.prototype.getOkButtonElement = function() {
return this.getButtonElement(goog.ui.Dialog.DefaultButtonKeys.OK);
};
/**
* Returns the HTML Button element for the Cancel button in this dialog.
* @return {Element} The button element if found, else null.
* @protected
*/
goog.ui.editor.AbstractDialog.prototype.getCancelButtonElement = function() {
return this.getButtonElement(goog.ui.Dialog.DefaultButtonKeys.CANCEL);
};
/**
* Returns the HTML Button element for the button added to this dialog with
* the given button id.
* @param {string} buttonId The id of the button to get.
* @return {Element} The button element if found, else null.
* @protected
*/
goog.ui.editor.AbstractDialog.prototype.getButtonElement = function(buttonId) {
return this.dialogInternal_.getButtonSet().getButton(buttonId);
};
/**
* Creates and returns the event object to be used when dispatching the OK
* event to listeners, or returns null to prevent the dialog from closing.
* Subclasses should override this to return their own subclass of
* goog.events.Event that includes all data a plugin would need from the dialog.
* @param {goog.events.Event} e The event object dispatched by the wrapped
* dialog.
* @return {goog.events.Event} The event object to be used when dispatching the
* OK event to listeners.
* @protected
*/
goog.ui.editor.AbstractDialog.prototype.createOkEvent = goog.abstractMethod;
/**
* Handles the event dispatched by the wrapped dialog control when the user
* clicks the OK button. Attempts to create the OK event object and dispatches
* it if successful.
* @param {goog.ui.Dialog.Event} e wrapped dialog OK event object.
* @return {boolean} Whether the default action (closing the dialog) should
* still be executed. This will be false if the OK event could not be
* created to be dispatched, or if any listener to that event returs false
* or calls preventDefault.
* @protected
*/
goog.ui.editor.AbstractDialog.prototype.handleOk = function(e) {
var eventObj = this.createOkEvent(e);
if (eventObj) {
return this.dispatchEvent(eventObj);
} else {
return false;
}
};
/**
* Handles the event dispatched by the wrapped dialog control when the user
* clicks the Cancel button. Simply dispatches a CANCEL event.
* @return {boolean} Returns false if any of the handlers called prefentDefault
* on the event or returned false themselves.
* @protected
*/
goog.ui.editor.AbstractDialog.prototype.handleCancel = function() {
return this.dispatchEvent(goog.ui.editor.AbstractDialog.EventType.CANCEL);
};
/**
* Disposes of the dialog. If the dialog is open, it will be hidden and
* AFTER_HIDE will be dispatched.
* @override
* @protected
*/
goog.ui.editor.AbstractDialog.prototype.disposeInternal = function() {
if (this.dialogInternal_) {
this.hide();
this.dialogInternal_.dispose();
this.dialogInternal_ = null;
}
goog.ui.editor.AbstractDialog.superClass_.disposeInternal.call(this);
};
// *** Private implementation *********************************************** //
/**
* The wrapped dialog widget.
* @type {goog.ui.Dialog}
* @private
*/
goog.ui.editor.AbstractDialog.prototype.dialogInternal_;
/**
* Cleans up after the dialog is hidden and fires the AFTER_HIDE event. Should
* be a listener for the wrapped dialog's AFTER_HIDE event.
* @private
*/
goog.ui.editor.AbstractDialog.prototype.handleAfterHide_ = function() {
this.dispatchEvent(goog.ui.editor.AbstractDialog.EventType.AFTER_HIDE);
};
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<!--
All Rights Reserved.
@author nicksantos@google.com (Nick Santos)
@author marcosalmeida@google.com (Marcos Almeida)
-->
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.ui.editor.AbstractDialog
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.ui.editor.AbstractDialogTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,483 @@
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.ui.editor.AbstractDialogTest');
goog.setTestOnly('goog.ui.editor.AbstractDialogTest');
goog.require('goog.dom');
goog.require('goog.dom.DomHelper');
goog.require('goog.dom.classlist');
goog.require('goog.events.Event');
goog.require('goog.events.EventHandler');
goog.require('goog.events.KeyCodes');
goog.require('goog.testing.MockControl');
goog.require('goog.testing.events');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.mockmatchers.ArgumentMatcher');
goog.require('goog.ui.editor.AbstractDialog');
goog.require('goog.userAgent');
function shouldRunTests() {
// Test disabled in IE7 due to flakiness. See b/4269021.
return !(goog.userAgent.IE && goog.userAgent.isVersionOrHigher('7'));
}
var dialog;
var builder;
var mockCtrl;
var mockAfterHideHandler;
var mockOkHandler;
var mockCancelHandler;
var mockCustomButtonHandler;
var CUSTOM_EVENT = 'customEvent';
var CUSTOM_BUTTON_ID = 'customButton';
function setUp() {
mockCtrl = new goog.testing.MockControl();
mockAfterHideHandler = mockCtrl.createLooseMock(goog.events.EventHandler);
mockOkHandler = mockCtrl.createLooseMock(goog.events.EventHandler);
mockCancelHandler = mockCtrl.createLooseMock(goog.events.EventHandler);
mockCustomButtonHandler = mockCtrl.createLooseMock(goog.events.EventHandler);
}
function tearDown() {
if (dialog) {
mockAfterHideHandler.$setIgnoreUnexpectedCalls(true);
dialog.dispose();
}
}
/**
* Sets up the mock event handler to expect an AFTER_HIDE event.
*/
function expectAfterHide() {
mockAfterHideHandler.handleEvent(
new goog.testing.mockmatchers.ArgumentMatcher(function(arg) {
return arg.type ==
goog.ui.editor.AbstractDialog.EventType.AFTER_HIDE;
}));
}
/**
* Sets up the mock event handler to expect an OK event.
*/
function expectOk() {
mockOkHandler.handleEvent(new goog.testing.mockmatchers.ArgumentMatcher(
function(arg) {
return arg.type == goog.ui.editor.AbstractDialog.EventType.OK;
}));
}
/**
* Sets up the mock event handler to expect an OK event and to call
* preventDefault when handling it.
*/
function expectOkPreventDefault() {
expectOk();
mockOkHandler.$does(function(e) {
e.preventDefault();
});
}
/**
* Sets up the mock event handler to expect an OK event and to return false
* when handling it.
*/
function expectOkReturnFalse() {
expectOk();
mockOkHandler.$returns(false);
}
/**
* Sets up the mock event handler to expect a CANCEL event.
*/
function expectCancel() {
mockCancelHandler.handleEvent(new goog.testing.mockmatchers.ArgumentMatcher(
function(arg) {
return arg.type == goog.ui.editor.AbstractDialog.EventType.CANCEL;
}));
}
/**
* Sets up the mock event handler to expect a custom button event.
*/
function expectCustomButton() {
mockCustomButtonHandler.handleEvent(
new goog.testing.mockmatchers.ArgumentMatcher(function(arg) {
return arg.type == CUSTOM_EVENT;
}));
}
/**
* Helper to create the dialog being tested in each test. Since NewDialog is
* abstract, needs to add a concrete version of any abstract methods. Also
* creates up the global builder variable which should be set up after the call
* to this method.
* @return {goog.ui.editor.AbstractDialog} The dialog.
*/
function createTestDialog() {
var dialog = new goog.ui.editor.AbstractDialog(new goog.dom.DomHelper());
builder = new goog.ui.editor.AbstractDialog.Builder(dialog);
dialog.createDialogControl = function() {
return builder.build();
};
dialog.createOkEvent = function(e) {
return new goog.events.Event(goog.ui.editor.AbstractDialog.EventType.OK);
};
dialog.addEventListener(goog.ui.editor.AbstractDialog.EventType.AFTER_HIDE,
mockAfterHideHandler);
dialog.addEventListener(goog.ui.editor.AbstractDialog.EventType.OK,
mockOkHandler);
dialog.addEventListener(goog.ui.editor.AbstractDialog.EventType.CANCEL,
mockCancelHandler);
dialog.addEventListener(CUSTOM_EVENT, mockCustomButtonHandler);
return dialog;
}
/**
* Asserts that the given dialog is open.
* @param {string} msg Message to be printed in case of failure.
* @param {goog.ui.editor.AbstractDialog} dialog Dialog to be tested.
*/
function assertOpen(msg, dialog) {
assertTrue(msg + ' [AbstractDialog.isOpen()]', dialog && dialog.isOpen());
}
/**
* Asserts that the given dialog is closed.
* @param {string} msg Message to be printed in case of failure.
* @param {goog.ui.editor.AbstractDialog} dialog Dialog to be tested.
*/
function assertNotOpen(msg, dialog) {
assertFalse(msg + ' [AbstractDialog.isOpen()]', dialog && dialog.isOpen());
}
/**
* Tests that if you create a dialog and hide it without having shown it, no
* errors occur.
*/
function testCreateAndHide() {
dialog = createTestDialog();
mockCtrl.$replayAll();
assertNotOpen('Dialog should not be open after creation', dialog);
dialog.hide();
assertNotOpen('Dialog should not be open after hide()', dialog);
mockCtrl.$verifyAll(); // Verifies AFTER_HIDE was not dispatched.
}
/**
* Tests that when you show and hide a dialog the flags indicating open are
* correct and the AFTER_HIDE event is dispatched (and no errors happen).
*/
function testShowAndHide() {
dialog = createTestDialog();
expectAfterHide(dialog);
mockCtrl.$replayAll();
assertNotOpen('Dialog should not be open before show()', dialog);
dialog.show();
assertOpen('Dialog should be open after show()', dialog);
dialog.hide();
assertNotOpen('Dialog should not be open after hide()', dialog);
mockCtrl.$verifyAll(); // Verifies AFTER_HIDE was dispatched.
}
/**
* Tests that when you show and dispose a dialog (without hiding it first) the
* flags indicating open are correct and the AFTER_HIDE event is dispatched (and
* no errors happen).
*/
function testShowAndDispose() {
dialog = createTestDialog();
expectAfterHide(dialog);
mockCtrl.$replayAll();
assertNotOpen('Dialog should not be open before show()', dialog);
dialog.show();
assertOpen('Dialog should be open after show()', dialog);
dialog.dispose();
assertNotOpen('Dialog should not be open after dispose()', dialog);
mockCtrl.$verifyAll(); // Verifies AFTER_HIDE was dispatched.
}
/**
* Tests that when you dispose a dialog (without ever showing it first) the
* flags indicating open are correct and the AFTER_HIDE event is never
* dispatched (and no errors happen).
*/
function testDisposeWithoutShow() {
dialog = createTestDialog();
mockCtrl.$replayAll();
assertNotOpen('Dialog should not be open before dispose()', dialog);
dialog.dispose();
assertNotOpen('Dialog should not be open after dispose()', dialog);
mockCtrl.$verifyAll(); // Verifies AFTER_HIDE was NOT dispatched.
}
/**
* Tests that labels set in the builder can be found in the resulting dialog's
* HTML.
*/
function testBasicLayout() {
dialog = createTestDialog();
mockCtrl.$replayAll();
// create some dialog content
var content = goog.dom.createDom('div', null, 'The Content');
builder.setTitle('The Title')
.setContent(content)
.addOkButton('The OK Button')
.addCancelButton()
.addButton('The Apply Button', goog.nullFunction)
.addClassName('myClassName');
dialog.show();
var dialogElem = dialog.dialogInternal_.getElement();
var html = dialogElem.innerHTML;
// TODO(user): This is really insufficient. If the title and content
// were swapped this test would still pass!
assertContains('Dialog html should contain title', '>The Title<', html);
assertContains('Dialog html should contain content', '>The Content<', html);
assertContains('Dialog html should contain custom OK button label',
'>The OK Button<',
html);
assertContains('Dialog html should contain default Cancel button label',
'>Cancel<',
html);
assertContains('Dialog html should contain custom button label',
'>The Apply Button<',
html);
assertTrue('Dialog should have default Closure class',
goog.dom.classlist.contains(dialogElem, 'modal-dialog'));
assertTrue('Dialog should have our custom class',
goog.dom.classlist.contains(dialogElem, 'myClassName'));
mockCtrl.$verifyAll();
}
/**
* Tests that clicking the OK button dispatches the OK event and closes the
* dialog (dispatching the AFTER_HIDE event too).
*/
function testOk() {
dialog = createTestDialog();
expectOk(dialog);
expectAfterHide(dialog);
mockCtrl.$replayAll();
dialog.show();
goog.testing.events.fireClickSequence(dialog.getOkButtonElement());
assertNotOpen('Dialog should not be open after clicking OK', dialog);
mockCtrl.$verifyAll();
}
/**
* Tests that hitting the enter key dispatches the OK event and closes the
* dialog (dispatching the AFTER_HIDE event too).
*/
function testEnter() {
dialog = createTestDialog();
expectOk(dialog);
expectAfterHide(dialog);
mockCtrl.$replayAll();
dialog.show();
goog.testing.events.fireKeySequence(dialog.dialogInternal_.getElement(),
goog.events.KeyCodes.ENTER);
assertNotOpen('Dialog should not be open after hitting enter', dialog);
mockCtrl.$verifyAll();
}
/**
* Tests that clicking the Cancel button dispatches the CANCEL event and closes
* the dialog (dispatching the AFTER_HIDE event too).
*/
function testCancel() {
dialog = createTestDialog();
expectCancel(dialog);
expectAfterHide(dialog);
mockCtrl.$replayAll();
builder.addCancelButton('My Cancel Button');
dialog.show();
goog.testing.events.fireClickSequence(dialog.getCancelButtonElement());
assertNotOpen('Dialog should not be open after clicking Cancel', dialog);
mockCtrl.$verifyAll();
}
/**
* Tests that hitting the escape key dispatches the CANCEL event and closes
* the dialog (dispatching the AFTER_HIDE event too).
*/
function testEscape() {
dialog = createTestDialog();
expectCancel(dialog);
expectAfterHide(dialog);
mockCtrl.$replayAll();
dialog.show();
goog.testing.events.fireKeySequence(dialog.dialogInternal_.getElement(),
goog.events.KeyCodes.ESC);
assertNotOpen('Dialog should not be open after hitting escape', dialog);
mockCtrl.$verifyAll();
}
/**
* Tests that clicking the custom button dispatches the custom event and closes
* the dialog (dispatching the AFTER_HIDE event too).
*/
function testCustomButton() {
dialog = createTestDialog();
expectCustomButton(dialog);
expectAfterHide(dialog);
mockCtrl.$replayAll();
builder.addButton('My Custom Button',
function() {
dialog.dispatchEvent(CUSTOM_EVENT);
},
CUSTOM_BUTTON_ID);
dialog.show();
goog.testing.events.fireClickSequence(
dialog.getButtonElement(CUSTOM_BUTTON_ID));
assertNotOpen('Dialog should not be open after clicking custom button',
dialog);
mockCtrl.$verifyAll();
}
/**
* Tests that if the OK handler calls preventDefault, the dialog doesn't close.
*/
function testOkPreventDefault() {
dialog = createTestDialog();
expectOkPreventDefault(dialog);
mockCtrl.$replayAll();
dialog.show();
goog.testing.events.fireClickSequence(dialog.getOkButtonElement());
assertOpen('Dialog should not be closed because preventDefault was called',
dialog);
mockCtrl.$verifyAll();
}
/**
* Tests that if the OK handler returns false, the dialog doesn't close.
*/
function testOkReturnFalse() {
dialog = createTestDialog();
expectOkReturnFalse(dialog);
mockCtrl.$replayAll();
dialog.show();
goog.testing.events.fireClickSequence(dialog.getOkButtonElement());
assertOpen('Dialog should not be closed because handler returned false',
dialog);
mockCtrl.$verifyAll();
}
/**
* Tests that if creating the OK event fails, no event is dispatched and the
* dialog doesn't close.
*/
function testCreateOkEventFail() {
dialog = createTestDialog();
dialog.createOkEvent = function() { // Override our mock createOkEvent.
return null;
};
mockCtrl.$replayAll();
dialog.show();
goog.testing.events.fireClickSequence(dialog.getOkButtonElement());
assertOpen('Dialog should not be closed because OK event creation failed',
dialog);
mockCtrl.$verifyAll(); // Verifies that no event was dispatched.
}
/**
* Tests that processOkAndClose() dispatches the OK event and closes the
* dialog (dispatching the AFTER_HIDE event too).
*/
function testProcessOkAndClose() {
dialog = createTestDialog();
expectOk(dialog);
expectAfterHide(dialog);
mockCtrl.$replayAll();
dialog.show();
dialog.processOkAndClose();
assertNotOpen('Dialog should not be open after processOkAndClose()', dialog);
mockCtrl.$verifyAll();
}
/**
* Tests that if the OK handler triggered by processOkAndClose calls
* preventDefault, the dialog doesn't close (in the old implementation this
* failed due to not great design, so this is sort of a regression test).
*/
function testProcessOkAndClosePreventDefault() {
dialog = createTestDialog();
expectOkPreventDefault(dialog);
mockCtrl.$replayAll();
dialog.show();
dialog.processOkAndClose();
assertOpen('Dialog should not be closed because preventDefault was called',
dialog);
mockCtrl.$verifyAll();
}
@@ -0,0 +1,559 @@
// Copyright 2005 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Bubble component - handles display, hiding, etc. of the
* actual bubble UI.
*
* This is used exclusively by code within the editor package, and should not
* be used directly.
*
* @author robbyw@google.com (Robby Walker)
*/
goog.provide('goog.ui.editor.Bubble');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.dom.ViewportSizeMonitor');
goog.require('goog.dom.classlist');
goog.require('goog.editor.style');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.functions');
goog.require('goog.log');
goog.require('goog.math.Box');
goog.require('goog.object');
goog.require('goog.positioning');
goog.require('goog.positioning.Corner');
goog.require('goog.positioning.Overflow');
goog.require('goog.positioning.OverflowStatus');
goog.require('goog.string');
goog.require('goog.style');
goog.require('goog.ui.Component');
goog.require('goog.ui.PopupBase');
goog.require('goog.userAgent');
/**
* Property bubble UI element.
* @param {Element} parent The parent element for this bubble.
* @param {number} zIndex The z index to draw the bubble at.
* @constructor
* @extends {goog.events.EventTarget}
*/
goog.ui.editor.Bubble = function(parent, zIndex) {
goog.ui.editor.Bubble.base(this, 'constructor');
/**
* Dom helper for the document the bubble should be shown in.
* @type {!goog.dom.DomHelper}
* @private
*/
this.dom_ = goog.dom.getDomHelper(parent);
/**
* Event handler for this bubble.
* @type {goog.events.EventHandler<!goog.ui.editor.Bubble>}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
/**
* Object that monitors the application window for size changes.
* @type {goog.dom.ViewportSizeMonitor}
* @private
*/
this.viewPortSizeMonitor_ = new goog.dom.ViewportSizeMonitor(
this.dom_.getWindow());
/**
* Maps panel ids to panels.
* @type {Object<goog.ui.editor.Bubble.Panel_>}
* @private
*/
this.panels_ = {};
/**
* Container element for the entire bubble. This may contain elements related
* to look and feel or styling of the bubble.
* @type {Element}
* @private
*/
this.bubbleContainer_ =
this.dom_.createDom(goog.dom.TagName.DIV,
{'className': goog.ui.editor.Bubble.BUBBLE_CLASSNAME});
goog.style.setElementShown(this.bubbleContainer_, false);
goog.dom.appendChild(parent, this.bubbleContainer_);
goog.style.setStyle(this.bubbleContainer_, 'zIndex', zIndex);
/**
* Container element for the bubble panels - this should be some inner element
* within (or equal to) bubbleContainer.
* @type {Element}
* @private
*/
this.bubbleContents_ = this.createBubbleDom(this.dom_, this.bubbleContainer_);
/**
* Element showing the close box.
* @type {!Element}
* @private
*/
this.closeBox_ = this.dom_.createDom(goog.dom.TagName.DIV, {
'className': goog.getCssName('tr_bubble_closebox'),
'innerHTML': '&nbsp;'
});
this.bubbleContents_.appendChild(this.closeBox_);
// We make bubbles unselectable so that clicking on them does not steal focus
// or move the cursor away from the element the bubble is attached to.
goog.editor.style.makeUnselectable(this.bubbleContainer_, this.eventHandler_);
/**
* Popup that controls showing and hiding the bubble at the appropriate
* position.
* @type {goog.ui.PopupBase}
* @private
*/
this.popup_ = new goog.ui.PopupBase(this.bubbleContainer_);
};
goog.inherits(goog.ui.editor.Bubble, goog.events.EventTarget);
/**
* The css class name of the bubble container element.
* @type {string}
*/
goog.ui.editor.Bubble.BUBBLE_CLASSNAME = goog.getCssName('tr_bubble');
/**
* Creates and adds DOM for the bubble UI to the given container. This default
* implementation just returns the container itself.
* @param {!goog.dom.DomHelper} dom DOM helper to use.
* @param {!Element} container Element to add the new elements to.
* @return {!Element} The element where bubble content should be added.
* @protected
*/
goog.ui.editor.Bubble.prototype.createBubbleDom = function(dom, container) {
return container;
};
/**
* A logger for goog.ui.editor.Bubble.
* @type {goog.log.Logger}
* @protected
*/
goog.ui.editor.Bubble.prototype.logger =
goog.log.getLogger('goog.ui.editor.Bubble');
/** @override */
goog.ui.editor.Bubble.prototype.disposeInternal = function() {
goog.ui.editor.Bubble.base(this, 'disposeInternal');
goog.dom.removeNode(this.bubbleContainer_);
this.bubbleContainer_ = null;
this.eventHandler_.dispose();
this.eventHandler_ = null;
this.viewPortSizeMonitor_.dispose();
this.viewPortSizeMonitor_ = null;
};
/**
* @return {Element} The element that where the bubble's contents go.
*/
goog.ui.editor.Bubble.prototype.getContentElement = function() {
return this.bubbleContents_;
};
/**
* @return {Element} The element that contains the bubble.
* @protected
*/
goog.ui.editor.Bubble.prototype.getContainerElement = function() {
return this.bubbleContainer_;
};
/**
* @return {goog.events.EventHandler<T>} The event handler.
* @protected
* @this T
* @template T
*/
goog.ui.editor.Bubble.prototype.getEventHandler = function() {
return this.eventHandler_;
};
/**
* Handles user resizing of window.
* @private
*/
goog.ui.editor.Bubble.prototype.handleWindowResize_ = function() {
if (this.isVisible()) {
this.reposition();
}
};
/**
* Sets whether the bubble dismisses itself when the user clicks outside of it.
* @param {boolean} autoHide Whether to autohide on an external click.
*/
goog.ui.editor.Bubble.prototype.setAutoHide = function(autoHide) {
this.popup_.setAutoHide(autoHide);
};
/**
* Returns whether there is already a panel of the given type.
* @param {string} type Type of panel to check.
* @return {boolean} Whether there is already a panel of the given type.
*/
goog.ui.editor.Bubble.prototype.hasPanelOfType = function(type) {
return goog.object.some(this.panels_, function(panel) {
return panel.type == type;
});
};
/**
* Adds a panel to the bubble.
* @param {string} type The type of bubble panel this is. Should usually be
* the same as the tagName of the targetElement. This ensures multiple
* bubble panels don't appear for the same element.
* @param {string} title The title of the panel.
* @param {Element} targetElement The target element of the bubble.
* @param {function(Element): void} contentFn Function that when called with
* a container element, will add relevant panel content to it.
* @param {boolean=} opt_preferTopPosition Whether to prefer placing the bubble
* above the element instead of below it. Defaults to preferring below.
* If any panel prefers the top position, the top position is used.
* @return {string} The id of the panel.
*/
goog.ui.editor.Bubble.prototype.addPanel = function(type, title, targetElement,
contentFn, opt_preferTopPosition) {
var id = goog.string.createUniqueString();
var panel = new goog.ui.editor.Bubble.Panel_(this.dom_, id, type, title,
targetElement, !opt_preferTopPosition);
this.panels_[id] = panel;
// Insert the panel in string order of type. Technically we could use binary
// search here but n is really small (probably 0 - 2) so it's not worth it.
// The last child of bubbleContents_ is the close box so we take care not
// to treat it as a panel element, and we also ensure it stays as the last
// element. The intention here is not to create any artificial order, but
// just to ensure that it is always consistent.
var nextElement;
for (var i = 0, len = this.bubbleContents_.childNodes.length - 1; i < len;
i++) {
var otherChild = this.bubbleContents_.childNodes[i];
var otherPanel = this.panels_[otherChild.id];
if (otherPanel.type > type) {
nextElement = otherChild;
break;
}
}
goog.dom.insertSiblingBefore(panel.element,
nextElement || this.bubbleContents_.lastChild);
contentFn(panel.getContentElement());
goog.editor.style.makeUnselectable(panel.element, this.eventHandler_);
var numPanels = goog.object.getCount(this.panels_);
if (numPanels == 1) {
this.openBubble_();
} else if (numPanels == 2) {
goog.dom.classlist.add(
goog.asserts.assert(this.bubbleContainer_),
goog.getCssName('tr_multi_bubble'));
}
this.reposition();
return id;
};
/**
* Removes the panel with the given id.
* @param {string} id The id of the panel.
*/
goog.ui.editor.Bubble.prototype.removePanel = function(id) {
var panel = this.panels_[id];
goog.dom.removeNode(panel.element);
delete this.panels_[id];
var numPanels = goog.object.getCount(this.panels_);
if (numPanels <= 1) {
goog.dom.classlist.remove(
goog.asserts.assert(this.bubbleContainer_),
goog.getCssName('tr_multi_bubble'));
}
if (numPanels == 0) {
this.closeBubble_();
} else {
this.reposition();
}
};
/**
* Opens the bubble.
* @private
*/
goog.ui.editor.Bubble.prototype.openBubble_ = function() {
this.eventHandler_.
listen(this.closeBox_, goog.events.EventType.CLICK,
this.closeBubble_).
listen(this.viewPortSizeMonitor_,
goog.events.EventType.RESIZE, this.handleWindowResize_).
listen(this.popup_, goog.ui.PopupBase.EventType.HIDE,
this.handlePopupHide);
this.popup_.setVisible(true);
this.reposition();
};
/**
* Closes the bubble.
* @private
*/
goog.ui.editor.Bubble.prototype.closeBubble_ = function() {
this.popup_.setVisible(false);
};
/**
* Handles the popup's hide event by removing all panels and dispatching a
* HIDE event.
* @protected
*/
goog.ui.editor.Bubble.prototype.handlePopupHide = function() {
// Remove the panel elements.
for (var panelId in this.panels_) {
goog.dom.removeNode(this.panels_[panelId].element);
}
// Update the state to reflect no panels.
this.panels_ = {};
goog.dom.classlist.remove(
goog.asserts.assert(this.bubbleContainer_),
goog.getCssName('tr_multi_bubble'));
this.eventHandler_.removeAll();
this.dispatchEvent(goog.ui.Component.EventType.HIDE);
};
/**
* Returns the visibility of the bubble.
* @return {boolean} True if visible false if not.
*/
goog.ui.editor.Bubble.prototype.isVisible = function() {
return this.popup_.isVisible();
};
/**
* The vertical clearance in pixels between the bottom of the targetElement
* and the edge of the bubble.
* @type {number}
* @private
*/
goog.ui.editor.Bubble.VERTICAL_CLEARANCE_ = goog.userAgent.IE ? 4 : 2;
/**
* Bubble's margin box to be passed to goog.positioning.
* @type {goog.math.Box}
* @private
*/
goog.ui.editor.Bubble.MARGIN_BOX_ = new goog.math.Box(
goog.ui.editor.Bubble.VERTICAL_CLEARANCE_, 0,
goog.ui.editor.Bubble.VERTICAL_CLEARANCE_, 0);
/**
* Returns the margin box.
* @return {goog.math.Box}
* @protected
*/
goog.ui.editor.Bubble.prototype.getMarginBox = function() {
return goog.ui.editor.Bubble.MARGIN_BOX_;
};
/**
* Positions and displays this bubble below its targetElement. Assumes that
* the bubbleContainer is already contained in the document object it applies
* to.
*/
goog.ui.editor.Bubble.prototype.reposition = function() {
var targetElement = null;
var preferBottomPosition = true;
for (var panelId in this.panels_) {
var panel = this.panels_[panelId];
// We don't care which targetElement we get, so we just take the last one.
targetElement = panel.targetElement;
preferBottomPosition = preferBottomPosition && panel.preferBottomPosition;
}
var status = goog.positioning.OverflowStatus.FAILED;
// Fix for bug when bubbleContainer and targetElement have
// opposite directionality, the bubble should anchor to the END of
// the targetElement instead of START.
var reverseLayout = (goog.style.isRightToLeft(this.bubbleContainer_) !=
goog.style.isRightToLeft(targetElement));
// Try to put the bubble at the bottom of the target unless the plugin has
// requested otherwise.
if (preferBottomPosition) {
status = this.positionAtAnchor_(reverseLayout ?
goog.positioning.Corner.BOTTOM_END :
goog.positioning.Corner.BOTTOM_START,
goog.positioning.Corner.TOP_START,
goog.positioning.Overflow.ADJUST_X | goog.positioning.Overflow.FAIL_Y);
}
if (status & goog.positioning.OverflowStatus.FAILED) {
// Try to put it at the top of the target if there is not enough
// space at the bottom.
status = this.positionAtAnchor_(reverseLayout ?
goog.positioning.Corner.TOP_END : goog.positioning.Corner.TOP_START,
goog.positioning.Corner.BOTTOM_START,
goog.positioning.Overflow.ADJUST_X | goog.positioning.Overflow.FAIL_Y);
}
if (status & goog.positioning.OverflowStatus.FAILED) {
// Put it at the bottom again with adjustment if there is no
// enough space at the top.
status = this.positionAtAnchor_(reverseLayout ?
goog.positioning.Corner.BOTTOM_END :
goog.positioning.Corner.BOTTOM_START,
goog.positioning.Corner.TOP_START,
goog.positioning.Overflow.ADJUST_X |
goog.positioning.Overflow.ADJUST_Y);
if (status & goog.positioning.OverflowStatus.FAILED) {
goog.log.warning(this.logger,
'reposition(): positionAtAnchor() failed with ' + status);
}
}
};
/**
* A helper for reposition() - positions the bubble in regards to the position
* of the elements the bubble is attached to.
* @param {goog.positioning.Corner} targetCorner The corner of
* the target element.
* @param {goog.positioning.Corner} bubbleCorner The corner of the bubble.
* @param {number} overflow Overflow handling mode bitmap,
* {@see goog.positioning.Overflow}.
* @return {number} Status bitmap, {@see goog.positioning.OverflowStatus}.
* @private
*/
goog.ui.editor.Bubble.prototype.positionAtAnchor_ = function(
targetCorner, bubbleCorner, overflow) {
var targetElement = null;
for (var panelId in this.panels_) {
// For now, we use the outermost element. This assumes the multiple
// elements this panel is showing for contain each other - in the event
// that is not generally the case this may need to be updated to pick
// the lowest or highest element depending on targetCorner.
var candidate = this.panels_[panelId].targetElement;
if (!targetElement || goog.dom.contains(candidate, targetElement)) {
targetElement = this.panels_[panelId].targetElement;
}
}
return goog.positioning.positionAtAnchor(
targetElement, targetCorner, this.bubbleContainer_,
bubbleCorner, null, this.getMarginBox(), overflow,
null, this.getViewportBox());
};
/**
* Returns the viewport box to use when positioning the bubble.
* @return {goog.math.Box}
* @protected
*/
goog.ui.editor.Bubble.prototype.getViewportBox = goog.functions.NULL;
/**
* Private class used to describe a bubble panel.
* @param {goog.dom.DomHelper} dom DOM helper used to create the panel.
* @param {string} id ID of the panel.
* @param {string} type Type of the panel.
* @param {string} title Title of the panel.
* @param {Element} targetElement Element the panel is showing for.
* @param {boolean} preferBottomPosition Whether this panel prefers to show
* below the target element.
* @constructor
* @private
*/
goog.ui.editor.Bubble.Panel_ = function(dom, id, type, title, targetElement,
preferBottomPosition) {
/**
* The type of bubble panel.
* @type {string}
*/
this.type = type;
/**
* The target element of this bubble panel.
* @type {Element}
*/
this.targetElement = targetElement;
/**
* Whether the panel prefers to be placed below the target element.
* @type {boolean}
*/
this.preferBottomPosition = preferBottomPosition;
/**
* The element containing this panel.
*/
this.element = dom.createDom(goog.dom.TagName.DIV,
{className: goog.getCssName('tr_bubble_panel'), id: id},
dom.createDom(goog.dom.TagName.DIV,
{className: goog.getCssName('tr_bubble_panel_title')},
title ? title + ':' : ''), // TODO(robbyw): Does this work in bidi?
dom.createDom(goog.dom.TagName.DIV,
{className: goog.getCssName('tr_bubble_panel_content')}));
};
/**
* @return {Element} The element in the panel where content should go.
*/
goog.ui.editor.Bubble.Panel_.prototype.getContentElement = function() {
return /** @type {Element} */ (this.element.lastChild);
};
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<!--
All Rights Reserved.
@author tildahl@google.com (Michael Tildahl)
@author robbyw@google.com (Robby Walker)
-->
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.ui.editor.Bubble
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.ui.editor.BubbleTest');
</script>
<link rel="stylesheet" type="text/css" href="../../css/editor/bubble.css" />
</head>
<body>
<div id="field"></div>
</body>
</html>
@@ -0,0 +1,255 @@
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.ui.editor.BubbleTest');
goog.setTestOnly('goog.ui.editor.BubbleTest');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.positioning.Corner');
goog.require('goog.positioning.OverflowStatus');
goog.require('goog.string');
goog.require('goog.style');
goog.require('goog.testing.editor.TestHelper');
goog.require('goog.testing.events');
goog.require('goog.testing.jsunit');
goog.require('goog.ui.Component');
goog.require('goog.ui.editor.Bubble');
var testHelper;
var fieldDiv;
var bubble;
var link;
var link2;
var panelId;
function setUpPage() {
fieldDiv = goog.dom.getElement('field');
var viewportSize = goog.dom.getViewportSize();
// Some tests depends on enough size of viewport.
if (viewportSize.width < 600 || viewportSize.height < 440) {
window.moveTo(0, 0);
window.resizeTo(640, 480);
}
}
function setUp() {
testHelper = new goog.testing.editor.TestHelper(fieldDiv);
testHelper.setUpEditableElement();
bubble = new goog.ui.editor.Bubble(document.body, 999);
fieldDiv.innerHTML = '<a href="http://www.google.com">Google</a>' +
'<a href="http://www.google.com">Google2</a>';
link = fieldDiv.firstChild;
link2 = fieldDiv.lastChild;
window.scrollTo(0, 0);
goog.style.setStyle(document.body, 'direction', 'ltr');
goog.style.setStyle(document.getElementById('field'), 'position', 'static');
}
function tearDown() {
if (panelId) {
bubble.removePanel(panelId);
}
testHelper.tearDownEditableElement();
}
/**
* This is a helper function for setting up the target element with a
* given direction.
*
* @param {string} dir The direction of the target element, 'ltr' or 'rtl'.
* @param {boolean=} opt_preferTopPosition Whether to prefer placing the bubble
* above the element instead of below it. Defaults to preferring below.
*/
function prepareTargetWithGivenDirection(dir, opt_preferTopPosition) {
goog.style.setStyle(document.body, 'direction', dir);
fieldDiv.style.direction = dir;
fieldDiv.innerHTML = '<a href="http://www.google.com">Google</a>';
link = fieldDiv.firstChild;
panelId = bubble.addPanel('A', 'Link', link, function(el) {
el.innerHTML = '<div style="border:1px solid blue;">B</div>';
}, opt_preferTopPosition);
}
/**
* This is a helper function for getting the expected position of the bubble.
* (align to the right or the left of the target element). Align left by
* default and align right if opt_alignRight is true. The expected Y is
* unaffected by alignment.
*
* @param {boolean=} opt_alignRight Sets the expected alignment to be right.
*/
function getExpectedBubblePositionWithGivenAlignment(opt_alignRight) {
var targetPosition = goog.style.getFramedPageOffset(link, window);
var targetWidth = link.offsetWidth;
var bubbleSize = goog.style.getSize(bubble.bubbleContainer_);
var expectedBubbleX = opt_alignRight ?
targetPosition.x + targetWidth - bubbleSize.width : targetPosition.x;
var expectedBubbleY = link.offsetHeight + targetPosition.y +
goog.ui.editor.Bubble.VERTICAL_CLEARANCE_;
return {
x: expectedBubbleX,
y: expectedBubbleY
};
}
function testCreateBubbleWithLinkPanel() {
var id = goog.string.createUniqueString();
panelId = bubble.addPanel('A', 'Link', link, function(container) {
container.innerHTML = '<span id="' + id + '">Test</span>';
});
assertNotNull('Bubble should be created', bubble.bubbleContents_);
assertNotNull('Added element should be present', goog.dom.getElement(id));
assertTrue('Bubble should be visible', bubble.isVisible());
}
function testCloseBubble() {
testCreateBubbleWithLinkPanel();
var count = 0;
goog.events.listen(bubble, goog.ui.Component.EventType.HIDE, function() {
count++;
});
bubble.removePanel(panelId);
panelId = null;
assertFalse('Bubble should not be visible', bubble.isVisible());
assertEquals('Hide event should be dispatched', 1, count);
}
function testCloseBox() {
testCreateBubbleWithLinkPanel();
var count = 0;
goog.events.listen(bubble, goog.ui.Component.EventType.HIDE, function() {
count++;
});
var closeBox = goog.dom.getElementsByTagNameAndClass(
goog.dom.TagName.DIV, 'tr_bubble_closebox', bubble.bubbleContainer_)[0];
goog.testing.events.fireClickSequence(closeBox);
panelId = null;
assertFalse('Bubble should not be visible', bubble.isVisible());
assertEquals('Hide event should be dispatched', 1, count);
}
function testViewPortSizeMonitorEvent() {
testCreateBubbleWithLinkPanel();
var numCalled = 0;
bubble.reposition = function() {
numCalled++;
};
assertNotUndefined('viewPortSizeMonitor_ should not be undefined',
bubble.viewPortSizeMonitor_);
bubble.viewPortSizeMonitor_.dispatchEvent(goog.events.EventType.RESIZE);
assertEquals('reposition not called', 1, numCalled);
}
function testBubblePositionPreferTop() {
called = false;
bubble.positionAtAnchor_ = function(targetCorner, bubbleCorner, overflow) {
called = true;
// Assert that the bubble is positioned below the target.
assertEquals(goog.positioning.Corner.TOP_START, targetCorner);
assertEquals(goog.positioning.Corner.BOTTOM_START, bubbleCorner);
return goog.positioning.OverflowStatus.NONE;
};
prepareTargetWithGivenDirection('ltr', true);
assertTrue(called);
}
function testBubblePosition() {
panelId = bubble.addPanel('A', 'Link', link, goog.nullFunction);
var CLEARANCE = goog.ui.editor.Bubble.VERTICAL_CLEARANCE_;
var bubbleContainer = bubble.bubbleContainer_;
// The field is at a normal place, alomost the top of the viewport, and
// there is enough space at the bottom of the field.
var targetPos = goog.style.getFramedPageOffset(link, window);
var targetSize = goog.style.getSize(link);
var pos = goog.style.getFramedPageOffset(bubbleContainer);
assertEquals(targetPos.y + targetSize.height + CLEARANCE, pos.y);
assertEquals(targetPos.x, pos.x);
// Move the target to the bottom of the viewport.
var field = document.getElementById('field');
var fieldPos = goog.style.getFramedPageOffset(field, window);
fieldPos.y += bubble.dom_.getViewportSize().height -
(targetPos.y + targetSize.height);
goog.style.setStyle(field, 'position', 'absolute');
goog.style.setPosition(field, fieldPos);
bubble.reposition();
var bubbleSize = goog.style.getSize(bubbleContainer);
targetPosition = goog.style.getFramedPageOffset(link, window);
pos = goog.style.getFramedPageOffset(bubbleContainer);
assertEquals(targetPosition.y - CLEARANCE - bubbleSize.height, pos.y);
}
function testBubblePositionRightAligned() {
prepareTargetWithGivenDirection('rtl');
var expectedPos = getExpectedBubblePositionWithGivenAlignment(true);
var pos = goog.style.getFramedPageOffset(bubble.bubbleContainer_);
assertRoughlyEquals(expectedPos.x, pos.x, 0.1);
assertRoughlyEquals(expectedPos.y, pos.y, 0.1);
}
/**
* Test for bug 1955511, the bubble should align to the right side
* of the target element when the bubble is RTL, regardless of the
* target element's directionality.
*/
function testBubblePositionLeftToRight() {
goog.style.setStyle(bubble.bubbleContainer_, 'direction', 'ltr');
prepareTargetWithGivenDirection('rtl');
var expectedPos = getExpectedBubblePositionWithGivenAlignment();
var pos = goog.style.getFramedPageOffset(bubble.bubbleContainer_);
assertRoughlyEquals(expectedPos.x, pos.x, 0.1);
assertRoughlyEquals(expectedPos.y, pos.y, 0.1);
}
/**
* Test for bug 1955511, the bubble should align to the left side
* of the target element when the bubble is LTR, regardless of the
* target element's directionality.
*/
function testBubblePositionRightToLeft() {
goog.style.setStyle(bubble.bubbleContainer_, 'direction', 'rtl');
prepareTargetWithGivenDirection('ltr');
var expectedPos = getExpectedBubblePositionWithGivenAlignment(true);
var pos = goog.style.getFramedPageOffset(bubble.bubbleContainer_);
assertEquals(expectedPos.x, pos.x);
assertEquals(expectedPos.y, pos.y);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2010 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
goog.ui.editor.LinkDialog Tests
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.ui.editor.LinkDialogTest');
</script>
<link rel="stylesheet" href="../../css/dialog.css" />
<link rel="stylesheet" href="../../css/linkbutton.css" />
<link rel="stylesheet" href="../../css/editor/dialog.css" />
<link rel="stylesheet" href="../../css/editor/linkdialog.css" />
</head>
<body>
<iframe id="appWindowIframe" src="javascript:''">
</iframe>
</body>
</html>
@@ -0,0 +1,666 @@
// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.ui.editor.LinkDialogTest');
goog.setTestOnly('goog.ui.editor.LinkDialogTest');
goog.require('goog.dom');
goog.require('goog.dom.DomHelper');
goog.require('goog.dom.TagName');
goog.require('goog.editor.BrowserFeature');
goog.require('goog.editor.Link');
goog.require('goog.events');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyCodes');
goog.require('goog.style');
goog.require('goog.testing.MockControl');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.dom');
goog.require('goog.testing.events');
goog.require('goog.testing.events.Event');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.mockmatchers');
goog.require('goog.testing.mockmatchers.ArgumentMatcher');
goog.require('goog.ui.editor.AbstractDialog');
goog.require('goog.ui.editor.LinkDialog');
goog.require('goog.ui.editor.messages');
goog.require('goog.userAgent');
var dialog;
var mockCtrl;
var mockLink;
var mockOkHandler;
var mockGetViewportSize;
var mockWindowOpen;
var isNew;
var anchorElem;
var stubs = new goog.testing.PropertyReplacer();
var ANCHOR_TEXT = 'anchor text';
var ANCHOR_URL = 'http://www.google.com/';
var ANCHOR_EMAIL = 'm@r.cos';
var ANCHOR_MAILTO = 'mailto:' + ANCHOR_EMAIL;
function setUp() {
anchorElem = goog.dom.createElement(goog.dom.TagName.A);
goog.dom.appendChild(goog.dom.getDocument().body, anchorElem);
mockCtrl = new goog.testing.MockControl();
mockLink = mockCtrl.createLooseMock(goog.editor.Link);
mockOkHandler = mockCtrl.createLooseMock(goog.events.EventHandler);
isNew = false;
mockLink.isNew();
mockLink.$anyTimes();
mockLink.$does(function() {
return isNew;
});
mockLink.getCurrentText();
mockLink.$anyTimes();
mockLink.$does(function() {
return anchorElem.innerHTML;
});
mockLink.setTextAndUrl(goog.testing.mockmatchers.isString,
goog.testing.mockmatchers.isString);
mockLink.$anyTimes();
mockLink.$does(function(text, url) {
anchorElem.innerHTML = text;
anchorElem.href = url;
});
mockLink.$registerArgumentListVerifier('placeCursorRightOf', function() {
return true;
});
mockLink.placeCursorRightOf(goog.testing.mockmatchers.iBoolean);
mockLink.$anyTimes();
mockLink.getAnchor();
mockLink.$anyTimes();
mockLink.$returns(anchorElem);
mockWindowOpen = mockCtrl.createFunctionMock('open');
stubs.set(window, 'open', mockWindowOpen);
}
function tearDown() {
dialog.dispose();
goog.dom.removeNode(anchorElem);
stubs.reset();
}
function setUpAnchor(href, text, opt_isNew, opt_target, opt_rel) {
anchorElem.href = href;
anchorElem.innerHTML = text;
isNew = !!opt_isNew;
if (opt_target) {
anchorElem.target = opt_target;
}
if (opt_rel) {
anchorElem.rel = opt_rel;
}
}
/**
* Creates and shows the dialog to be tested.
* @param {Document=} opt_document Document to render the dialog into.
* Defaults to the main window's document.
* @param {boolean=} opt_openInNewWindow Whether the open in new window
* checkbox should be shown.
* @param {boolean=} opt_noFollow Whether rel=nofollow checkbox should be
* shown.
*/
function createAndShow(opt_document, opt_openInNewWindow, opt_noFollow) {
dialog = new goog.ui.editor.LinkDialog(new goog.dom.DomHelper(opt_document),
mockLink);
if (opt_openInNewWindow) {
dialog.showOpenLinkInNewWindow(false);
}
if (opt_noFollow) {
dialog.showRelNoFollow();
}
dialog.addEventListener(goog.ui.editor.AbstractDialog.EventType.OK,
mockOkHandler);
dialog.show();
}
/**
* Sets up the mock event handler to expect an OK event with the given text
* and url.
*/
function expectOk(linkText, linkUrl, opt_openInNewWindow, opt_noFollow) {
mockOkHandler.handleEvent(new goog.testing.mockmatchers.ArgumentMatcher(
function(arg) {
return arg.type == goog.ui.editor.AbstractDialog.EventType.OK &&
arg.linkText == linkText &&
arg.linkUrl == linkUrl &&
arg.openInNewWindow == !!opt_openInNewWindow &&
arg.noFollow == !!opt_noFollow;
},
'{linkText: ' + linkText + ', linkUrl: ' + linkUrl +
', openInNewWindow: ' + opt_openInNewWindow +
', noFollow: ' + opt_noFollow + '}'));
}
/**
* Return true if we should use active element in our tests.
* @return {boolean} .
*/
function useActiveElement() {
return goog.editor.BrowserFeature.HAS_ACTIVE_ELEMENT ||
goog.userAgent.WEBKIT && goog.userAgent.isVersionOrHigher(9);
}
/**
* Tests that when you show the dialog for a new link, you can switch
* to the URL view.
* @param {Document=} opt_document Document to render the dialog into.
* Defaults to the main window's document.
*/
function testShowNewLinkSwitchToUrl(opt_document) {
mockCtrl.$replayAll();
setUpAnchor('', '', true); // Must be done before creating the dialog.
createAndShow(opt_document);
var webRadio = dialog.dom.getElement(
goog.ui.editor.LinkDialog.Id_.ON_WEB_TAB).firstChild;
var emailRadio = dialog.dom.getElement(
goog.ui.editor.LinkDialog.Id_.EMAIL_ADDRESS_TAB).firstChild;
assertTrue('Web Radio Button selected', webRadio.checked);
assertFalse('Email Radio Button selected', emailRadio.checked);
if (useActiveElement()) {
assertEquals('Focus should be on url input',
getUrlInput(),
dialog.dom.getActiveElement());
}
emailRadio.click();
assertFalse('Web Radio Button selected', webRadio.checked);
assertTrue('Email Radio Button selected', emailRadio.checked);
if (useActiveElement()) {
assertEquals('Focus should be on url input',
getEmailInput(),
dialog.dom.getActiveElement());
}
mockCtrl.$verifyAll();
}
/**
* Tests that when you show the dialog for a new link, the input fields are
* empty, the web tab is selected and focus is in the url input field.
* @param {Document=} opt_document Document to render the dialog into.
* Defaults to the main window's document.
*/
function testShowForNewLink(opt_document) {
mockCtrl.$replayAll();
setUpAnchor('', '', true); // Must be done before creating the dialog.
createAndShow(opt_document);
assertEquals('Display text input field should be empty',
'',
getDisplayInputText());
assertEquals('Url input field should be empty',
'',
getUrlInputText());
assertEquals('On the web tab should be selected',
goog.ui.editor.LinkDialog.Id_.ON_WEB,
dialog.curTabId_);
if (useActiveElement()) {
assertEquals('Focus should be on url input',
getUrlInput(),
dialog.dom.getActiveElement());
}
mockCtrl.$verifyAll();
}
/**
* Fakes that the mock field is using an iframe and does the same test as
* testShowForNewLink().
*/
function testShowForNewLinkWithDiffAppWindow() {
testShowForNewLink(goog.dom.getElement('appWindowIframe').contentDocument);
}
/**
* Tests that when you show the dialog for a url link, the input fields are
* filled in, the web tab is selected and focus is in the url input field.
*/
function testShowForUrlLink() {
mockCtrl.$replayAll();
setUpAnchor(ANCHOR_URL, ANCHOR_TEXT);
createAndShow();
assertEquals('Display text input field should be filled in',
ANCHOR_TEXT,
getDisplayInputText());
assertEquals('Url input field should be filled in',
ANCHOR_URL,
getUrlInputText());
assertEquals('On the web tab should be selected',
goog.ui.editor.LinkDialog.Id_.ON_WEB,
dialog.curTabId_);
if (useActiveElement()) {
assertEquals('Focus should be on url input',
getUrlInput(),
dialog.dom.getActiveElement());
}
mockCtrl.$verifyAll();
}
/**
* Tests that when you show the dialog for a mailto link, the input fields are
* filled in, the email tab is selected and focus is in the email input field.
*/
function testShowForMailtoLink() {
mockCtrl.$replayAll();
setUpAnchor(ANCHOR_MAILTO, ANCHOR_TEXT);
createAndShow();
assertEquals('Display text input field should be filled in',
ANCHOR_TEXT,
getDisplayInputText());
assertEquals('Email input field should be filled in',
ANCHOR_EMAIL, // The 'mailto:' is not in the input!
getEmailInputText());
assertEquals('Email tab should be selected',
goog.ui.editor.LinkDialog.Id_.EMAIL_ADDRESS,
dialog.curTabId_);
if (useActiveElement()) {
assertEquals('Focus should be on email input',
getEmailInput(),
dialog.dom.getActiveElement());
}
mockCtrl.$verifyAll();
}
/**
* Tests that the display text is autogenerated from the url input in the
* right situations (and not generated when appropriate too).
*/
function testAutogeneration() {
mockCtrl.$replayAll();
setUpAnchor('', '', true);
createAndShow();
// Simulate typing a url when everything is empty, should autogen.
setUrlInputText(ANCHOR_URL);
assertEquals('Display text should have been autogenerated',
ANCHOR_URL,
getDisplayInputText());
// Simulate typing text when url is set, afterwards should not autogen.
setDisplayInputText(ANCHOR_TEXT);
setUrlInputText(ANCHOR_MAILTO);
assertNotEquals('Display text should not have been autogenerated',
ANCHOR_MAILTO,
getDisplayInputText());
assertEquals('Display text should have remained the same',
ANCHOR_TEXT,
getDisplayInputText());
// Simulate typing text equal to existing url, afterwards should autogen.
setDisplayInputText(ANCHOR_MAILTO);
setUrlInputText(ANCHOR_URL);
assertEquals('Display text should have been autogenerated',
ANCHOR_URL,
getDisplayInputText());
mockCtrl.$verifyAll();
}
/**
* Tests that the display text is not autogenerated from the url input in all
* situations when the autogeneration feature is turned off.
*/
function testAutogenerationOff() {
mockCtrl.$replayAll();
setUpAnchor('', '', true);
createAndShow();
// Disable the autogen feature
dialog.setAutogenFeatureEnabled(false);
// Simulate typing a url when everything is empty, should not autogen.
setUrlInputText(ANCHOR_URL);
assertEquals('Display text should not have been autogenerated',
'',
getDisplayInputText());
// Simulate typing text when url is set, afterwards should not autogen.
setDisplayInputText(ANCHOR_TEXT);
setUrlInputText(ANCHOR_MAILTO);
assertNotEquals('Display text should not have been autogenerated',
ANCHOR_MAILTO,
getDisplayInputText());
assertEquals('Display text should have remained the same',
ANCHOR_TEXT,
getDisplayInputText());
// Simulate typing text equal to existing url, afterwards should not
// autogen.
setDisplayInputText(ANCHOR_MAILTO);
setUrlInputText(ANCHOR_URL);
assertEquals('Display text should not have been autogenerated',
ANCHOR_MAILTO,
getDisplayInputText());
mockCtrl.$verifyAll();
}
/**
* Tests that clicking OK with the url tab selected dispatches an event with
* the proper link data.
*/
function testOkForUrl() {
expectOk(ANCHOR_TEXT, ANCHOR_URL);
mockCtrl.$replayAll();
setUpAnchor('', '', true);
createAndShow();
dialog.tabPane_.setSelectedTabId(goog.ui.editor.LinkDialog.Id_.ON_WEB_TAB);
setDisplayInputText(ANCHOR_TEXT);
setUrlInputText(ANCHOR_URL);
goog.testing.events.fireClickSequence(dialog.getOkButtonElement());
mockCtrl.$verifyAll();
}
/**
* Tests that clicking OK with the url tab selected but with an email address
* in the url field dispatches an event with the proper link data.
*/
function testOkForUrlWithEmail() {
expectOk(ANCHOR_TEXT, ANCHOR_MAILTO);
mockCtrl.$replayAll();
setUpAnchor('', '', true);
createAndShow();
dialog.tabPane_.setSelectedTabId(goog.ui.editor.LinkDialog.Id_.ON_WEB_TAB);
setDisplayInputText(ANCHOR_TEXT);
setUrlInputText(ANCHOR_EMAIL);
goog.testing.events.fireClickSequence(dialog.getOkButtonElement());
mockCtrl.$verifyAll();
}
/**
* Tests that clicking OK with the email tab selected dispatches an event with
* the proper link data.
*/
function testOkForEmail() {
expectOk(ANCHOR_TEXT, ANCHOR_MAILTO);
mockCtrl.$replayAll();
setUpAnchor('', '', true);
createAndShow();
dialog.tabPane_.setSelectedTabId(
goog.ui.editor.LinkDialog.Id_.EMAIL_ADDRESS_TAB);
setDisplayInputText(ANCHOR_TEXT);
setEmailInputText(ANCHOR_EMAIL);
goog.testing.events.fireClickSequence(dialog.getOkButtonElement());
mockCtrl.$verifyAll();
}
function testOpenLinkInNewWindowNewLink() {
expectOk(ANCHOR_TEXT, ANCHOR_URL, true);
expectOk(ANCHOR_TEXT, ANCHOR_URL, false);
mockCtrl.$replayAll();
setUpAnchor('', '', true);
createAndShow(undefined, true);
dialog.tabPane_.setSelectedTabId(goog.ui.editor.LinkDialog.Id_.ON_WEB_TAB);
setDisplayInputText(ANCHOR_TEXT);
setUrlInputText(ANCHOR_URL);
assertFalse('"Open in new window" should start unchecked',
getOpenInNewWindowCheckboxChecked());
setOpenInNewWindowCheckboxChecked(true);
assertTrue('"Open in new window" should have gotten checked',
getOpenInNewWindowCheckboxChecked());
goog.testing.events.fireClickSequence(dialog.getOkButtonElement());
// Reopen same dialog
dialog.show();
dialog.tabPane_.setSelectedTabId(goog.ui.editor.LinkDialog.Id_.ON_WEB_TAB);
setDisplayInputText(ANCHOR_TEXT);
setUrlInputText(ANCHOR_URL);
assertTrue('"Open in new window" should remember it was checked',
getOpenInNewWindowCheckboxChecked());
setOpenInNewWindowCheckboxChecked(false);
assertFalse('"Open in new window" should have gotten unchecked',
getOpenInNewWindowCheckboxChecked());
goog.testing.events.fireClickSequence(dialog.getOkButtonElement());
}
function testOpenLinkInNewWindowExistingLink() {
mockCtrl.$replayAll();
// Edit an existing link that already opens in a new window.
setUpAnchor('', '', false, '_blank');
createAndShow(undefined, true);
dialog.tabPane_.setSelectedTabId(goog.ui.editor.LinkDialog.Id_.ON_WEB_TAB);
setDisplayInputText(ANCHOR_TEXT);
setUrlInputText(ANCHOR_URL);
assertTrue('"Open in new window" should start checked for existing link',
getOpenInNewWindowCheckboxChecked());
mockCtrl.$verifyAll();
}
function testRelNoFollowNewLink() {
expectOk(ANCHOR_TEXT, ANCHOR_URL, null, true);
expectOk(ANCHOR_TEXT, ANCHOR_URL, null, false);
mockCtrl.$replayAll();
setUpAnchor('', '', true, true);
createAndShow(null, null, true);
dialog.tabPane_.setSelectedTabId(goog.ui.editor.LinkDialog.Id_.ON_WEB_TAB);
setDisplayInputText(ANCHOR_TEXT);
setUrlInputText(ANCHOR_URL);
assertFalse('rel=nofollow should start unchecked',
dialog.relNoFollowCheckbox_.checked);
// Check rel=nofollow and close the dialog.
dialog.relNoFollowCheckbox_.checked = true;
goog.testing.events.fireClickSequence(dialog.getOkButtonElement());
// Reopen the same dialog.
anchorElem.rel = 'foo nofollow bar';
dialog.show();
dialog.tabPane_.setSelectedTabId(goog.ui.editor.LinkDialog.Id_.ON_WEB_TAB);
setDisplayInputText(ANCHOR_TEXT);
setUrlInputText(ANCHOR_URL);
assertTrue('rel=nofollow should start checked when reopening the dialog',
dialog.relNoFollowCheckbox_.checked);
}
function testRelNoFollowExistingLink() {
mockCtrl.$replayAll();
setUpAnchor('', '', null, null, 'foo nofollow bar');
createAndShow(null, null, true);
assertTrue('rel=nofollow should start checked for existing link',
dialog.relNoFollowCheckbox_.checked);
mockCtrl.$verifyAll();
}
/**
* Test that clicking on the test button opens a new window with the correct
* options.
*/
function testWebTestButton() {
if (goog.userAgent.GECKO) {
// TODO(robbyw): Figure out why this is flaky and fix it.
return;
}
var width, height;
mockWindowOpen(ANCHOR_URL, '_blank',
new goog.testing.mockmatchers.ArgumentMatcher(function(str) {
return str == 'width=' + width + ',height=' + height +
',toolbar=1,scrollbars=1,location=1,statusbar=0,' +
'menubar=1,resizable=1';
}, '3rd arg: (string) window.open() options'));
mockCtrl.$replayAll();
setUpAnchor(ANCHOR_URL, ANCHOR_TEXT);
createAndShow();
// Measure viewport after opening dialog because that might cause scrollbars
// to appear and reduce the viewport size.
var size = goog.dom.getViewportSize(window);
width = Math.max(size.width - 50, 50);
height = Math.max(size.height - 50, 50);
var testLink = goog.testing.dom.findTextNode(
goog.ui.editor.messages.MSG_TEST_THIS_LINK,
dialog.dialogInternal_.getElement());
goog.testing.events.fireClickSequence(testLink.parentNode);
mockCtrl.$verifyAll();
}
/**
* Test that clicking on the test button does not open a new window when
* the event is canceled.
*/
function testWebTestButtonPreventDefault() {
mockCtrl.$replayAll();
setUpAnchor(ANCHOR_URL, ANCHOR_TEXT);
createAndShow();
goog.events.listen(dialog,
goog.ui.editor.LinkDialog.EventType.BEFORE_TEST_LINK,
function(e) {
assertEquals(e.url, ANCHOR_URL);
e.preventDefault();
});
var testLink = goog.testing.dom.findTextNode(
goog.ui.editor.messages.MSG_TEST_THIS_LINK,
dialog.dialogInternal_.getElement());
goog.testing.events.fireClickSequence(testLink.parentNode);
mockCtrl.$verifyAll();
}
/**
* Test that the setTextToDisplayVisible() correctly works.
* options.
*/
function testSetTextToDisplayVisible() {
mockCtrl.$replayAll();
setUpAnchor('', '', true);
createAndShow();
assertNotEquals('none',
goog.style.getStyle(dialog.textToDisplayDiv_, 'display'));
dialog.setTextToDisplayVisible(false);
assertEquals('none',
goog.style.getStyle(dialog.textToDisplayDiv_, 'display'));
dialog.setTextToDisplayVisible(true);
assertNotEquals('none',
goog.style.getStyle(dialog.textToDisplayDiv_, 'display'));
mockCtrl.$verifyAll();
}
function getDisplayInput() {
return dialog.dom.getElement(goog.ui.editor.LinkDialog.Id_.TEXT_TO_DISPLAY);
}
function getDisplayInputText() {
return getDisplayInput().value;
}
function setDisplayInputText(text) {
var textInput = getDisplayInput();
textInput.value = text;
// Fire event so that dialog behaves like when user types.
fireInputEvent(textInput, goog.events.KeyCodes.M);
}
function getUrlInput() {
var elt = dialog.dom.getElement(goog.ui.editor.LinkDialog.Id_.ON_WEB_INPUT);
assertNotNullNorUndefined('UrlInput must be found', elt);
return elt;
}
function getUrlInputText() {
return getUrlInput().value;
}
function setUrlInputText(text) {
var urlInput = getUrlInput();
urlInput.value = text;
// Fire event so that dialog behaves like when user types.
fireInputEvent(dialog.urlInputHandler_, goog.events.KeyCodes.M);
}
function getEmailInput() {
var elt = dialog.dom.getElement(
goog.ui.editor.LinkDialog.Id_.EMAIL_ADDRESS_INPUT);
assertNotNullNorUndefined('EmailInput must be found', elt);
return elt;
}
function getEmailInputText() {
return getEmailInput().value;
}
function setEmailInputText(text) {
var emailInput = getEmailInput();
emailInput.value = text;
// Fire event so that dialog behaves like when user types.
fireInputEvent(dialog.emailInputHandler_, goog.events.KeyCodes.M);
}
function getOpenInNewWindowCheckboxChecked() {
return dialog.openInNewWindowCheckbox_.checked;
}
function setOpenInNewWindowCheckboxChecked(checked) {
dialog.openInNewWindowCheckbox_.checked = checked;
}
function fireInputEvent(input, keyCode) {
var inputEvent = new goog.testing.events.Event(goog.events.EventType.INPUT,
input);
inputEvent.keyCode = keyCode;
inputEvent.charCode = keyCode;
goog.testing.events.fireBrowserEvent(inputEvent);
}
@@ -0,0 +1,148 @@
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Messages common to Editor UI components.
*
* @author robbyw@google.com (Robby Walker)
*/
goog.provide('goog.ui.editor.messages');
goog.require('goog.html.uncheckedconversions');
goog.require('goog.string.Const');
/** @desc Link button / bubble caption. */
goog.ui.editor.messages.MSG_LINK_CAPTION = goog.getMsg('Link');
/** @desc Title for the dialog that edits a link. */
goog.ui.editor.messages.MSG_EDIT_LINK = goog.getMsg('Edit Link');
/** @desc Prompt the user for the text of the link they've written. */
goog.ui.editor.messages.MSG_TEXT_TO_DISPLAY = goog.getMsg('Text to display:');
/** @desc Prompt the user for the URL of the link they've created. */
goog.ui.editor.messages.MSG_LINK_TO = goog.getMsg('Link to:');
/** @desc Prompt the user to type a web address for their link. */
goog.ui.editor.messages.MSG_ON_THE_WEB = goog.getMsg('Web address');
/** @desc More details on what linking to a web address involves.. */
goog.ui.editor.messages.MSG_ON_THE_WEB_TIP = goog.getMsg(
'Link to a page or file somewhere else on the web');
/**
* @desc Text for a button that allows the user to test the link that
* they created.
*/
goog.ui.editor.messages.MSG_TEST_THIS_LINK = goog.getMsg('Test this link');
/**
* @desc Explanation for how to create a link with the link-editing dialog.
*/
goog.ui.editor.messages.MSG_TR_LINK_EXPLANATION = goog.getMsg(
'{$startBold}Not sure what to put in the box?{$endBold} ' +
'First, find the page on the web that you want to ' +
'link to. (A {$searchEngineLink}search engine{$endLink} ' +
'might be useful.) Then, copy the web address from ' +
"the box in your browser's address bar, and paste it into " +
'the box above.',
{'startBold': '<b>',
'endBold': '</b>',
'searchEngineLink': "<a href='http://www.google.com/' target='_new'>",
'endLink': '</a>'});
/**
* @return {!goog.html.SafeHtml} SafeHtml version of MSG_TR_LINK_EXPLANATION.
*/
goog.ui.editor.messages.getTrLinkExplanationSafeHtml = function() {
return goog.html.uncheckedconversions.
safeHtmlFromStringKnownToSatisfyTypeContract(
goog.string.Const.from('Parameterless translation'),
goog.ui.editor.messages.MSG_TR_LINK_EXPLANATION);
};
/** @desc Prompt for the URL of a link that the user is creating. */
goog.ui.editor.messages.MSG_WHAT_URL = goog.getMsg(
'To what URL should this link go?');
/**
* @desc Prompt for an email address, so that the user can create a link
* that sends an email.
*/
goog.ui.editor.messages.MSG_EMAIL_ADDRESS = goog.getMsg('Email address');
/**
* @desc Explanation of the prompt for an email address in a link.
*/
goog.ui.editor.messages.MSG_EMAIL_ADDRESS_TIP = goog.getMsg(
'Link to an email address');
/** @desc Error message when the user enters an invalid email address. */
goog.ui.editor.messages.MSG_INVALID_EMAIL = goog.getMsg(
'Invalid email address');
/**
* @desc When the user creates a mailto link, asks them what email
* address clicking on this link will send mail to.
*/
goog.ui.editor.messages.MSG_WHAT_EMAIL = goog.getMsg(
'To what email address should this link?');
/**
* @desc Warning about the dangers of creating links with email
* addresses in them.
*/
goog.ui.editor.messages.MSG_EMAIL_EXPLANATION = goog.getMsg(
'{$preb}Be careful.{$postb} ' +
'Remember that any time you include an email address on a web page, ' +
'nasty spammers can find it too.', {'preb': '<b>', 'postb': '</b>'});
/**
* @return {!goog.html.SafeHtml} SafeHtml version of MSG_EMAIL_EXPLANATION.
*/
goog.ui.editor.messages.getEmailExplanationSafeHtml = function() {
return goog.html.uncheckedconversions.
safeHtmlFromStringKnownToSatisfyTypeContract(
goog.string.Const.from('Parameterless translation'),
goog.ui.editor.messages.MSG_EMAIL_EXPLANATION);
};
/**
* @desc Label for the checkbox that allows the user to specify what when this
* link is clicked, it should be opened in a new window.
*/
goog.ui.editor.messages.MSG_OPEN_IN_NEW_WINDOW = goog.getMsg(
'Open this link in a new window');
/** @desc Image bubble caption. */
goog.ui.editor.messages.MSG_IMAGE_CAPTION = goog.getMsg('Image');
@@ -0,0 +1,201 @@
// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Tabbed pane with style and functionality specific to
* Editor dialogs.
*
* @author robbyw@google.com (Robby Walker)
*/
goog.provide('goog.ui.editor.TabPane');
goog.require('goog.asserts');
goog.require('goog.dom.TagName');
goog.require('goog.dom.classlist');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventType');
goog.require('goog.style');
goog.require('goog.ui.Component');
goog.require('goog.ui.Control');
goog.require('goog.ui.Tab');
goog.require('goog.ui.TabBar');
/**
* Creates a new Editor-style tab pane.
* @param {goog.dom.DomHelper} dom The dom helper for the window to create this
* tab pane in.
* @param {string=} opt_caption Optional caption of the tab pane.
* @constructor
* @extends {goog.ui.Component}
* @final
*/
goog.ui.editor.TabPane = function(dom, opt_caption) {
goog.ui.editor.TabPane.base(this, 'constructor', dom);
/**
* The event handler used to register events.
* @type {goog.events.EventHandler<!goog.ui.editor.TabPane>}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
this.registerDisposable(this.eventHandler_);
/**
* The tab bar used to render the tabs.
* @type {goog.ui.TabBar}
* @private
*/
this.tabBar_ = new goog.ui.TabBar(goog.ui.TabBar.Location.START,
undefined, this.dom_);
this.tabBar_.setFocusable(false);
/**
* The content element.
* @private
*/
this.tabContent_ = this.dom_.createDom(goog.dom.TagName.DIV,
{className: goog.getCssName('goog-tab-content')});
/**
* The currently selected radio button.
* @type {Element}
* @private
*/
this.selectedRadio_ = null;
/**
* The currently visible tab content.
* @type {Element}
* @private
*/
this.visibleContent_ = null;
// Add the caption as the first element in the tab bar.
if (opt_caption) {
var captionControl = new goog.ui.Control(opt_caption, undefined,
this.dom_);
captionControl.addClassName(goog.getCssName('tr-tabpane-caption'));
captionControl.setEnabled(false);
this.tabBar_.addChild(captionControl, true);
}
};
goog.inherits(goog.ui.editor.TabPane, goog.ui.Component);
/**
* @return {string} The ID of the content element for the current tab.
*/
goog.ui.editor.TabPane.prototype.getCurrentTabId = function() {
return this.tabBar_.getSelectedTab().getId();
};
/**
* Selects the tab with the given id.
* @param {string} id Id of the tab to select.
*/
goog.ui.editor.TabPane.prototype.setSelectedTabId = function(id) {
this.tabBar_.setSelectedTab(this.tabBar_.getChild(id));
};
/**
* Adds a tab to the tab pane.
* @param {string} id The id of the tab to add.
* @param {string} caption The caption of the tab.
* @param {string} tooltip The tooltip for the tab.
* @param {string} groupName for the radio button group.
* @param {Element} content The content element to show when this tab is
* selected.
*/
goog.ui.editor.TabPane.prototype.addTab = function(id, caption, tooltip,
groupName, content) {
var radio = this.dom_.createDom(goog.dom.TagName.INPUT,
{
name: groupName,
type: 'radio'
});
var tab = new goog.ui.Tab([radio, this.dom_.createTextNode(caption)],
undefined, this.dom_);
tab.setId(id);
tab.setTooltip(tooltip);
this.tabBar_.addChild(tab, true);
// When you navigate the radio buttons with TAB and then the Arrow keys on
// Chrome and FF, you get a CLICK event on them, and the radio button
// is selected. You don't get a SELECT at all. We listen for SELECT
// nonetheless because it's possible that some browser will issue only
// SELECT.
this.eventHandler_.listen(radio,
[goog.events.EventType.SELECT, goog.events.EventType.CLICK],
goog.bind(this.tabBar_.setSelectedTab, this.tabBar_, tab));
content.id = id + '-tab';
this.tabContent_.appendChild(content);
goog.style.setElementShown(content, false);
};
/** @override */
goog.ui.editor.TabPane.prototype.enterDocument = function() {
goog.ui.editor.TabPane.base(this, 'enterDocument');
// Get the root element and add a class name to it.
var root = this.getElement();
goog.asserts.assert(root);
goog.dom.classlist.add(root, goog.getCssName('tr-tabpane'));
// Add the tabs.
this.addChild(this.tabBar_, true);
this.eventHandler_.listen(this.tabBar_, goog.ui.Component.EventType.SELECT,
this.handleTabSelect_);
// Add the tab content.
root.appendChild(this.tabContent_);
// Add an element to clear the tab float.
root.appendChild(
this.dom_.createDom(goog.dom.TagName.DIV,
{className: goog.getCssName('goog-tab-bar-clear')}));
};
/**
* Handles a tab change.
* @param {goog.events.Event} e The browser change event.
* @private
*/
goog.ui.editor.TabPane.prototype.handleTabSelect_ = function(e) {
var tab = /** @type {goog.ui.Tab} */ (e.target);
// Show the tab content.
if (this.visibleContent_) {
goog.style.setElementShown(this.visibleContent_, false);
}
this.visibleContent_ = this.dom_.getElement(tab.getId() + '-tab');
goog.style.setElementShown(this.visibleContent_, true);
// Select the appropriate radio button (and deselect the current one).
if (this.selectedRadio_) {
this.selectedRadio_.checked = false;
}
this.selectedRadio_ = tab.getElement().getElementsByTagName(
goog.dom.TagName.INPUT)[0];
this.selectedRadio_.checked = true;
};
@@ -0,0 +1,296 @@
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview A class for managing the editor toolbar.
*
* @author attila@google.com (Attila Bodis)
* @see ../../demos/editor/editor.html
*/
goog.provide('goog.ui.editor.ToolbarController');
goog.require('goog.editor.Field');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.ui.Component');
/**
* A class for managing the editor toolbar. Acts as a bridge between
* a {@link goog.editor.Field} and a {@link goog.ui.Toolbar}.
*
* The {@code toolbar} argument must be an instance of {@link goog.ui.Toolbar}
* or a subclass. This class doesn't care how the toolbar was created. As
* long as one or more controls hosted in the toolbar have IDs that match
* built-in {@link goog.editor.Command}s, they will function as expected. It is
* the caller's responsibility to ensure that the toolbar is already rendered
* or that it decorates an existing element.
*
*
* @param {!goog.editor.Field} field Editable field to be controlled by the
* toolbar.
* @param {!goog.ui.Toolbar} toolbar Toolbar to control the editable field.
* @constructor
* @extends {goog.events.EventTarget}
*/
goog.ui.editor.ToolbarController = function(field, toolbar) {
goog.events.EventTarget.call(this);
/**
* Event handler to listen for field events and user actions.
* @type {!goog.events.EventHandler<!goog.ui.editor.ToolbarController>}
* @private
*/
this.handler_ = new goog.events.EventHandler(this);
/**
* The field instance controlled by the toolbar.
* @type {!goog.editor.Field}
* @private
*/
this.field_ = field;
/**
* The toolbar that controls the field.
* @type {!goog.ui.Toolbar}
* @private
*/
this.toolbar_ = toolbar;
/**
* Editing commands whose state is to be queried when updating the toolbar.
* @type {!Array<string>}
* @private
*/
this.queryCommands_ = [];
// Iterate over all buttons, and find those which correspond to
// queryable commands. Add them to the list of commands to query on
// each COMMAND_VALUE_CHANGE event.
this.toolbar_.forEachChild(function(button) {
if (button.queryable) {
this.queryCommands_.push(this.getComponentId(button.getId()));
}
}, this);
// Make sure the toolbar doesn't steal keyboard focus.
this.toolbar_.setFocusable(false);
// Hook up handlers that update the toolbar in response to field events,
// and to execute editor commands in response to toolbar events.
this.handler_.
listen(this.field_, goog.editor.Field.EventType.COMMAND_VALUE_CHANGE,
this.updateToolbar).
listen(this.toolbar_, goog.ui.Component.EventType.ACTION,
this.handleAction);
};
goog.inherits(goog.ui.editor.ToolbarController, goog.events.EventTarget);
/**
* Returns the Closure component ID of the control that corresponds to the
* given {@link goog.editor.Command} constant.
* Subclasses may override this method if they want to use a custom mapping
* scheme from commands to controls.
* @param {string} command Editor command.
* @return {string} Closure component ID of the corresponding toolbar
* control, if any.
* @protected
*/
goog.ui.editor.ToolbarController.prototype.getComponentId = function(command) {
// The default implementation assumes that the component ID is the same as
// the command constant.
return command;
};
/**
* Returns the {@link goog.editor.Command} constant
* that corresponds to the given Closure component ID. Subclasses may override
* this method if they want to use a custom mapping scheme from controls to
* commands.
* @param {string} id Closure component ID of a toolbar control.
* @return {string} Editor command or dialog constant corresponding to the
* toolbar control, if any.
* @protected
*/
goog.ui.editor.ToolbarController.prototype.getCommand = function(id) {
// The default implementation assumes that the component ID is the same as
// the command constant.
return id;
};
/**
* Returns the event handler object for the editor toolbar. Useful for classes
* that extend {@code goog.ui.editor.ToolbarController}.
* @return {!goog.events.EventHandler<T>} The event handler object.
* @protected
* @this T
* @template T
*/
goog.ui.editor.ToolbarController.prototype.getHandler = function() {
return this.handler_;
};
/**
* Returns the field instance managed by the toolbar. Useful for
* classes that extend {@code goog.ui.editor.ToolbarController}.
* @return {!goog.editor.Field} The field managed by the toolbar.
* @protected
*/
goog.ui.editor.ToolbarController.prototype.getField = function() {
return this.field_;
};
/**
* Returns the toolbar UI component that manages the editor. Useful for
* classes that extend {@code goog.ui.editor.ToolbarController}.
* @return {!goog.ui.Toolbar} The toolbar UI component.
*/
goog.ui.editor.ToolbarController.prototype.getToolbar = function() {
return this.toolbar_;
};
/**
* @return {boolean} Whether the toolbar is visible.
*/
goog.ui.editor.ToolbarController.prototype.isVisible = function() {
return this.toolbar_.isVisible();
};
/**
* Shows or hides the toolbar.
* @param {boolean} visible Whether to show or hide the toolbar.
*/
goog.ui.editor.ToolbarController.prototype.setVisible = function(visible) {
this.toolbar_.setVisible(visible);
};
/**
* @return {boolean} Whether the toolbar is enabled.
*/
goog.ui.editor.ToolbarController.prototype.isEnabled = function() {
return this.toolbar_.isEnabled();
};
/**
* Enables or disables the toolbar.
* @param {boolean} enabled Whether to enable or disable the toolbar.
*/
goog.ui.editor.ToolbarController.prototype.setEnabled = function(enabled) {
this.toolbar_.setEnabled(enabled);
};
/**
* Programmatically blurs the editor toolbar, un-highlighting the currently
* highlighted item, and closing the currently open menu (if any).
*/
goog.ui.editor.ToolbarController.prototype.blur = function() {
// We can't just call this.toolbar_.getElement().blur(), because the toolbar
// element itself isn't focusable, so goog.ui.Container#handleBlur isn't
// registered to handle blur events.
this.toolbar_.handleBlur(null);
};
/** @override */
goog.ui.editor.ToolbarController.prototype.disposeInternal = function() {
goog.ui.editor.ToolbarController.superClass_.disposeInternal.call(this);
if (this.handler_) {
this.handler_.dispose();
delete this.handler_;
}
if (this.toolbar_) {
this.toolbar_.dispose();
delete this.toolbar_;
}
delete this.field_;
delete this.queryCommands_;
};
/**
* Updates the toolbar in response to editor events. Specifically, updates
* button states based on {@code COMMAND_VALUE_CHANGE} events, reflecting the
* effective formatting of the selection.
* @param {goog.events.Event} e Editor event to handle.
* @protected
*/
goog.ui.editor.ToolbarController.prototype.updateToolbar = function(e) {
if (!this.toolbar_.isEnabled() ||
!this.dispatchEvent(goog.ui.Component.EventType.CHANGE)) {
return;
}
var state;
/** @preserveTry */
try {
/** @type {Array<string>} */
e.commands; // Added by dispatchEvent.
// If the COMMAND_VALUE_CHANGE event specifies which commands changed
// state, then we only need to update those ones, otherwise update all
// commands.
state = /** @type {Object} */ (
this.field_.queryCommandValue(e.commands || this.queryCommands_));
} catch (ex) {
// TODO(attila): Find out when/why this happens.
state = {};
}
this.updateToolbarFromState(state);
};
/**
* Updates the toolbar to reflect a given state.
* @param {Object} state Object mapping editor commands to values.
*/
goog.ui.editor.ToolbarController.prototype.updateToolbarFromState =
function(state) {
for (var command in state) {
var button = this.toolbar_.getChild(this.getComponentId(command));
if (button) {
var value = state[command];
if (button.updateFromValue) {
button.updateFromValue(value);
} else {
button.setChecked(!!value);
}
}
}
};
/**
* Handles {@code ACTION} events dispatched by toolbar buttons in response to
* user actions by executing the corresponding field command.
* @param {goog.events.Event} e Action event to handle.
* @protected
*/
goog.ui.editor.ToolbarController.prototype.handleAction = function(e) {
var command = this.getCommand(e.target.getId());
this.field_.execCommand(command, e.target.getValue());
};
@@ -0,0 +1,439 @@
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Generic factory functions for creating the building blocks for
* an editor toolbar.
*
* @author attila@google.com (Attila Bodis)
*/
goog.provide('goog.ui.editor.ToolbarFactory');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.string');
goog.require('goog.string.Unicode');
goog.require('goog.style');
goog.require('goog.ui.Component');
goog.require('goog.ui.Container');
goog.require('goog.ui.Option');
goog.require('goog.ui.Toolbar');
goog.require('goog.ui.ToolbarButton');
goog.require('goog.ui.ToolbarColorMenuButton');
goog.require('goog.ui.ToolbarMenuButton');
goog.require('goog.ui.ToolbarRenderer');
goog.require('goog.ui.ToolbarSelect');
goog.require('goog.userAgent');
/**
* Takes a font spec (e.g. "Arial, Helvetica, sans-serif") and returns the
* primary font name, normalized to lowercase (e.g. "arial").
* @param {string} fontSpec Font specification.
* @return {string} The primary font name, in lowercase.
*/
goog.ui.editor.ToolbarFactory.getPrimaryFont = function(fontSpec) {
var i = fontSpec.indexOf(',');
var fontName = (i != -1 ? fontSpec.substring(0, i) : fontSpec).toLowerCase();
// Strip leading/trailing quotes from the font name (bug 1050118).
return goog.string.stripQuotes(fontName, '"\'');
};
/**
* Bulk-adds fonts to the given font menu button. The argument must be an
* array of font descriptor objects, each of which must have the following
* attributes:
* <ul>
* <li>{@code caption} - Caption to show in the font menu (e.g. 'Tahoma')
* <li>{@code value} - Value for the corresponding 'font-family' CSS style
* (e.g. 'Tahoma, Arial, sans-serif')
* </ul>
* @param {!goog.ui.Select} button Font menu button.
* @param {!Array<{caption: string, value: string}>} fonts Array of
* font descriptors.
*/
goog.ui.editor.ToolbarFactory.addFonts = function(button, fonts) {
goog.array.forEach(fonts, function(font) {
goog.ui.editor.ToolbarFactory.addFont(button, font.caption, font.value);
});
};
/**
* Adds a menu item to the given font menu button. The first font listed in
* the {@code value} argument is considered the font ID, so adding two items
* whose CSS style starts with the same font may lead to unpredictable results.
* @param {!goog.ui.Select} button Font menu button.
* @param {string} caption Caption to show for the font menu.
* @param {string} value Value for the corresponding 'font-family' CSS style.
*/
goog.ui.editor.ToolbarFactory.addFont = function(button, caption, value) {
// The font ID is the first font listed in the CSS style, normalized to
// lowercase.
var id = goog.ui.editor.ToolbarFactory.getPrimaryFont(value);
// Construct the option, and add it to the button.
var option = new goog.ui.Option(caption, value, button.getDomHelper());
option.setId(id);
button.addItem(option);
// Captions are shown in their own font.
option.getContentElement().style.fontFamily = value;
};
/**
* Bulk-adds font sizes to the given font size menu button. The argument must
* be an array of font size descriptor objects, each of which must have the
* following attributes:
* <ul>
* <li>{@code caption} - Caption to show in the font size menu (e.g. 'Huge')
* <li>{@code value} - Value for the corresponding HTML font size (e.g. 6)
* </ul>
* @param {!goog.ui.Select} button Font size menu button.
* @param {!Array<{caption: string, value:number}>} sizes Array of font
* size descriptors.
*/
goog.ui.editor.ToolbarFactory.addFontSizes = function(button, sizes) {
goog.array.forEach(sizes, function(size) {
goog.ui.editor.ToolbarFactory.addFontSize(button, size.caption, size.value);
});
};
/**
* Adds a menu item to the given font size menu button. The {@code value}
* argument must be a legacy HTML font size in the 0-7 range.
* @param {!goog.ui.Select} button Font size menu button.
* @param {string} caption Caption to show in the font size menu.
* @param {number} value Value for the corresponding HTML font size.
*/
goog.ui.editor.ToolbarFactory.addFontSize = function(button, caption, value) {
// Construct the option, and add it to the button.
var option = new goog.ui.Option(caption, value, button.getDomHelper());
button.addItem(option);
// Adjust the font size of the menu item and the height of the checkbox
// element after they've been rendered by addItem(). Captions are shown in
// the corresponding font size, and lining up the checkbox is tricky.
var content = option.getContentElement();
content.style.fontSize =
goog.ui.editor.ToolbarFactory.getPxFromLegacySize(value) + 'px';
content.firstChild.style.height = '1.1em';
};
/**
* Converts a legacy font size specification into an equivalent pixel size.
* For example, {@code &lt;font size="6"&gt;} is {@code font-size: 32px;}, etc.
* @param {number} fontSize Legacy font size spec in the 0-7 range.
* @return {number} Equivalent pixel size.
*/
goog.ui.editor.ToolbarFactory.getPxFromLegacySize = function(fontSize) {
return goog.ui.editor.ToolbarFactory.LEGACY_SIZE_TO_PX_MAP_[fontSize] || 10;
};
/**
* Converts a pixel font size specification into an equivalent legacy size.
* For example, {@code font-size: 32px;} is {@code &lt;font size="6"&gt;}, etc.
* If the given pixel size doesn't exactly match one of the legacy sizes, -1 is
* returned.
* @param {number} px Pixel font size.
* @return {number} Equivalent legacy size spec in the 0-7 range, or -1 if none
* exists.
*/
goog.ui.editor.ToolbarFactory.getLegacySizeFromPx = function(px) {
// Use lastIndexOf to get the largest legacy size matching the pixel size
// (most notably returning 1 instead of 0 for 10px).
return goog.array.lastIndexOf(
goog.ui.editor.ToolbarFactory.LEGACY_SIZE_TO_PX_MAP_, px);
};
/**
* Map of legacy font sizes (0-7) to equivalent pixel sizes.
* @type {!Array<number>}
* @private
*/
goog.ui.editor.ToolbarFactory.LEGACY_SIZE_TO_PX_MAP_ =
[10, 10, 13, 16, 18, 24, 32, 48];
/**
* Bulk-adds format options to the given "Format block" menu button. The
* argument must be an array of format option descriptor objects, each of
* which must have the following attributes:
* <ul>
* <li>{@code caption} - Caption to show in the menu (e.g. 'Minor heading')
* <li>{@code command} - Corresponding {@link goog.dom.TagName} (e.g.
* 'H4')
* </ul>
* @param {!goog.ui.Select} button "Format block" menu button.
* @param {!Array<{caption: string, command: goog.dom.TagName}>} formats Array
* of format option descriptors.
*/
goog.ui.editor.ToolbarFactory.addFormatOptions = function(button, formats) {
goog.array.forEach(formats, function(format) {
goog.ui.editor.ToolbarFactory.addFormatOption(button, format.caption,
format.command);
});
};
/**
* Adds a menu item to the given "Format block" menu button.
* @param {!goog.ui.Select} button "Format block" menu button.
* @param {string} caption Caption to show in the menu.
* @param {goog.dom.TagName} tag Corresponding block format tag.
*/
goog.ui.editor.ToolbarFactory.addFormatOption = function(button, caption, tag) {
// Construct the option, and add it to the button.
// TODO(attila): Create boring but functional menu item for now...
var buttonDom = button.getDomHelper();
var option = new goog.ui.Option(buttonDom.createDom(goog.dom.TagName.DIV,
null, caption), tag, buttonDom);
option.setId(tag);
button.addItem(option);
};
/**
* Creates a {@link goog.ui.Toolbar} containing the specified set of
* toolbar buttons, and renders it into the given parent element. Each
* item in the {@code items} array must a {@link goog.ui.Control}.
* @param {!Array<goog.ui.Control>} items Toolbar items; each must
* be a {@link goog.ui.Control}.
* @param {!Element} elem Toolbar parent element.
* @param {boolean=} opt_isRightToLeft Whether the editor chrome is
* right-to-left; defaults to the directionality of the toolbar parent
* element.
* @return {!goog.ui.Toolbar} Editor toolbar, rendered into the given parent
* element.
*/
goog.ui.editor.ToolbarFactory.makeToolbar = function(items, elem,
opt_isRightToLeft) {
var domHelper = goog.dom.getDomHelper(elem);
// Create an empty horizontal toolbar using the default renderer.
var toolbar = new goog.ui.Toolbar(goog.ui.ToolbarRenderer.getInstance(),
goog.ui.Container.Orientation.HORIZONTAL, domHelper);
// Optimization: Explicitly test for the directionality of the parent
// element here, so we can set it for both the toolbar and its children,
// saving a lot of expensive calls to goog.style.isRightToLeft() during
// rendering.
var isRightToLeft = opt_isRightToLeft || goog.style.isRightToLeft(elem);
toolbar.setRightToLeft(isRightToLeft);
// Optimization: Set the toolbar to non-focusable before it is rendered,
// to avoid creating unnecessary keyboard event handler objects.
toolbar.setFocusable(false);
for (var i = 0, button; button = items[i]; i++) {
// Optimization: Set the button to non-focusable before it is rendered,
// to avoid creating unnecessary keyboard event handler objects. Also set
// the directionality of the button explicitly, to avoid expensive calls
// to goog.style.isRightToLeft() during rendering.
button.setSupportedState(goog.ui.Component.State.FOCUSED, false);
button.setRightToLeft(isRightToLeft);
toolbar.addChild(button, true);
}
toolbar.render(elem);
return toolbar;
};
/**
* Creates a toolbar button with the given ID, tooltip, and caption. Applies
* any custom CSS class names to the button's caption element.
* @param {string} id Button ID; must equal a {@link goog.editor.Command} for
* built-in buttons, anything else for custom buttons.
* @param {string} tooltip Tooltip to be shown on hover.
* @param {goog.ui.ControlContent} caption Button caption.
* @param {string=} opt_classNames CSS class name(s) to apply to the caption
* element.
* @param {goog.ui.ButtonRenderer=} opt_renderer Button renderer; defaults to
* {@link goog.ui.ToolbarButtonRenderer} if unspecified.
* @param {goog.dom.DomHelper=} opt_domHelper DOM helper, used for DOM
* creation; defaults to the current document if unspecified.
* @return {!goog.ui.Button} A toolbar button.
*/
goog.ui.editor.ToolbarFactory.makeButton = function(id, tooltip, caption,
opt_classNames, opt_renderer, opt_domHelper) {
var button = new goog.ui.ToolbarButton(
goog.ui.editor.ToolbarFactory.createContent_(caption, opt_classNames,
opt_domHelper),
opt_renderer,
opt_domHelper);
button.setId(id);
button.setTooltip(tooltip);
return button;
};
/**
* Creates a toggle button with the given ID, tooltip, and caption. Applies
* any custom CSS class names to the button's caption element. The button
* returned has checkbox-like toggle semantics.
* @param {string} id Button ID; must equal a {@link goog.editor.Command} for
* built-in buttons, anything else for custom buttons.
* @param {string} tooltip Tooltip to be shown on hover.
* @param {goog.ui.ControlContent} caption Button caption.
* @param {string=} opt_classNames CSS class name(s) to apply to the caption
* element.
* @param {goog.ui.ButtonRenderer=} opt_renderer Button renderer; defaults to
* {@link goog.ui.ToolbarButtonRenderer} if unspecified.
* @param {goog.dom.DomHelper=} opt_domHelper DOM helper, used for DOM
* creation; defaults to the current document if unspecified.
* @return {!goog.ui.Button} A toggle button.
*/
goog.ui.editor.ToolbarFactory.makeToggleButton = function(id, tooltip, caption,
opt_classNames, opt_renderer, opt_domHelper) {
var button = goog.ui.editor.ToolbarFactory.makeButton(id, tooltip, caption,
opt_classNames, opt_renderer, opt_domHelper);
button.setSupportedState(goog.ui.Component.State.CHECKED, true);
return button;
};
/**
* Creates a menu button with the given ID, tooltip, and caption. Applies
* any custom CSS class names to the button's caption element. The button
* returned doesn't have an actual menu attached; use {@link
* goog.ui.MenuButton#setMenu} to attach a {@link goog.ui.Menu} to the
* button.
* @param {string} id Button ID; must equal a {@link goog.editor.Command} for
* built-in buttons, anything else for custom buttons.
* @param {string} tooltip Tooltip to be shown on hover.
* @param {goog.ui.ControlContent} caption Button caption.
* @param {string=} opt_classNames CSS class name(s) to apply to the caption
* element.
* @param {goog.ui.ButtonRenderer=} opt_renderer Button renderer; defaults to
* {@link goog.ui.ToolbarMenuButtonRenderer} if unspecified.
* @param {goog.dom.DomHelper=} opt_domHelper DOM helper, used for DOM
* creation; defaults to the current document if unspecified.
* @return {!goog.ui.MenuButton} A menu button.
*/
goog.ui.editor.ToolbarFactory.makeMenuButton = function(id, tooltip, caption,
opt_classNames, opt_renderer, opt_domHelper) {
var button = new goog.ui.ToolbarMenuButton(
goog.ui.editor.ToolbarFactory.createContent_(caption, opt_classNames,
opt_domHelper),
null,
opt_renderer,
opt_domHelper);
button.setId(id);
button.setTooltip(tooltip);
return button;
};
/**
* Creates a select button with the given ID, tooltip, and caption. Applies
* any custom CSS class names to the button's root element. The button
* returned doesn't have an actual menu attached; use {@link
* goog.ui.Select#setMenu} to attach a {@link goog.ui.Menu} containing
* {@link goog.ui.Option}s to the select button.
* @param {string} id Button ID; must equal a {@link goog.editor.Command} for
* built-in buttons, anything else for custom buttons.
* @param {string} tooltip Tooltip to be shown on hover.
* @param {goog.ui.ControlContent} caption Button caption; used as the
* default caption when nothing is selected.
* @param {string=} opt_classNames CSS class name(s) to apply to the button's
* root element.
* @param {goog.ui.MenuButtonRenderer=} opt_renderer Button renderer;
* defaults to {@link goog.ui.ToolbarMenuButtonRenderer} if unspecified.
* @param {goog.dom.DomHelper=} opt_domHelper DOM helper, used for DOM
* creation; defaults to the current document if unspecified.
* @return {!goog.ui.Select} A select button.
*/
goog.ui.editor.ToolbarFactory.makeSelectButton = function(id, tooltip, caption,
opt_classNames, opt_renderer, opt_domHelper) {
var button = new goog.ui.ToolbarSelect(null, null,
opt_renderer,
opt_domHelper);
if (opt_classNames) {
// Unlike the other button types, for goog.ui.Select buttons we apply the
// extra class names to the root element, because for select buttons the
// caption isn't stable (as it changes each time the selection changes).
goog.array.forEach(opt_classNames.split(/\s+/), button.addClassName,
button);
}
button.addClassName(goog.getCssName('goog-toolbar-select'));
button.setDefaultCaption(caption);
button.setId(id);
button.setTooltip(tooltip);
return button;
};
/**
* Creates a color menu button with the given ID, tooltip, and caption.
* Applies any custom CSS class names to the button's caption element. The
* button is created with a default color menu containing standard color
* palettes.
* @param {string} id Button ID; must equal a {@link goog.editor.Command} for
* built-in toolbar buttons, but can be anything else for custom buttons.
* @param {string} tooltip Tooltip to be shown on hover.
* @param {goog.ui.ControlContent} caption Button caption.
* @param {string=} opt_classNames CSS class name(s) to apply to the caption
* element.
* @param {goog.ui.ColorMenuButtonRenderer=} opt_renderer Button renderer;
* defaults to {@link goog.ui.ToolbarColorMenuButtonRenderer}
* if unspecified.
* @param {goog.dom.DomHelper=} opt_domHelper DOM helper, used for DOM
* creation; defaults to the current document if unspecified.
* @return {!goog.ui.ColorMenuButton} A color menu button.
*/
goog.ui.editor.ToolbarFactory.makeColorMenuButton = function(id, tooltip,
caption, opt_classNames, opt_renderer, opt_domHelper) {
var button = new goog.ui.ToolbarColorMenuButton(
goog.ui.editor.ToolbarFactory.createContent_(caption, opt_classNames,
opt_domHelper),
null,
opt_renderer,
opt_domHelper);
button.setId(id);
button.setTooltip(tooltip);
return button;
};
/**
* Creates a new DIV that wraps a button caption, optionally applying CSS
* class names to it. Used as a helper function in button factory methods.
* @param {goog.ui.ControlContent} caption Button caption.
* @param {string=} opt_classNames CSS class name(s) to apply to the DIV that
* wraps the caption (if any).
* @param {goog.dom.DomHelper=} opt_domHelper DOM helper, used for DOM
* creation; defaults to the current document if unspecified.
* @return {!Element} DIV that wraps the caption.
* @private
*/
goog.ui.editor.ToolbarFactory.createContent_ = function(caption, opt_classNames,
opt_domHelper) {
// FF2 doesn't like empty DIVs, especially when rendered right-to-left.
if ((!caption || caption == '') && goog.userAgent.GECKO &&
!goog.userAgent.isVersionOrHigher('1.9a')) {
caption = goog.string.Unicode.NBSP;
}
return (opt_domHelper || goog.dom.getDomHelper()).createDom(
goog.dom.TagName.DIV,
opt_classNames ? {'class' : opt_classNames} : null, caption);
};
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<!--
All Rights Reserved.
@author jparent@google.com (Julie Parent)
-->
<html>
<!--
Copyright 2010 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.ui.editor.ToolbarFactory
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.ui.editor.ToolbarFactoryTest');
</script>
</head>
<body>
<div id="myField">foo</div>
</body>
</html>
@@ -0,0 +1,73 @@
// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.ui.editor.ToolbarFactoryTest');
goog.setTestOnly('goog.ui.editor.ToolbarFactoryTest');
goog.require('goog.dom');
goog.require('goog.testing.ExpectedFailures');
goog.require('goog.testing.editor.TestHelper');
goog.require('goog.testing.jsunit');
goog.require('goog.ui.editor.ToolbarFactory');
goog.require('goog.userAgent');
var helper;
var expectedFailures;
function setUpPage() {
helper = new goog.testing.editor.TestHelper(goog.dom.getElement('myField'));
expectedFailures = new goog.testing.ExpectedFailures();
}
function setUp() {
helper.setUpEditableElement();
}
function tearDown() {
helper.tearDownEditableElement();
expectedFailures.handleTearDown();
}
/**
* Makes sure we have the correct conversion table in
* goog.ui.editor.ToolbarFactory.LEGACY_SIZE_TO_PX_MAP_. Can only be tested in
* a browser that takes legacy size values as input to execCommand but returns
* pixel size values from queryCommandValue. That's OK because that's the only
* situation where this conversion table's precision is critical. (When it's
* used to size the labels of the font size menu options it's ok if it's a few
* pixels off.)
*/
function testGetLegacySizeFromPx() {
// We will be warned if other browsers start behaving like webkit pre-534.7.
expectedFailures.expectFailureFor(
!goog.userAgent.WEBKIT ||
(goog.userAgent.WEBKIT && goog.userAgent.isVersionOrHigher('534.7')));
try {
var fieldElem = goog.dom.getElement('myField');
// Start from 1 because size 0 is bogus (becomes 16px, legacy size 3).
for (var i = 1; i <
goog.ui.editor.ToolbarFactory.LEGACY_SIZE_TO_PX_MAP_.length; i++) {
helper.select(fieldElem, 0, fieldElem, 1);
document.execCommand('fontSize', false, i);
helper.select('foo', 1);
var value = document.queryCommandValue('fontSize');
assertEquals('Px size ' + value + ' should convert to legacy size ' + i,
i, goog.ui.editor.ToolbarFactory.getLegacySizeFromPx(
parseInt(value, 10)));
}
} catch (e) {
expectedFailures.handleException(e);
}
}