Update wmts-hidpi, add nicer-api-docs

This commit is contained in:
Andreas Hocevar
2014-05-06 13:02:46 -05:00
parent b3ac1afd00
commit 1e25fc5585
2239 changed files with 3726515 additions and 37010 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
// Copyright 2012 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 Utility methods supporting the autocomplete package.
*
* @see ../../demos/autocomplete-basic.html
*/
goog.provide('goog.ui.ac');
goog.require('goog.ui.ac.ArrayMatcher');
goog.require('goog.ui.ac.AutoComplete');
goog.require('goog.ui.ac.InputHandler');
goog.require('goog.ui.ac.Renderer');
/**
* Factory function for building a basic autocomplete widget that autocompletes
* an inputbox or text area from a data array.
* @param {Array} data Data array.
* @param {Element} input Input element or text area.
* @param {boolean=} opt_multi Whether to allow multiple entries separated with
* semi-colons or commas.
* @param {boolean=} opt_useSimilar use similar matches. e.g. "gost" => "ghost".
* @return {!goog.ui.ac.AutoComplete} A new autocomplete object.
*/
goog.ui.ac.createSimpleAutoComplete =
function(data, input, opt_multi, opt_useSimilar) {
var matcher = new goog.ui.ac.ArrayMatcher(data, !opt_useSimilar);
var renderer = new goog.ui.ac.Renderer();
var inputHandler = new goog.ui.ac.InputHandler(null, null, !!opt_multi);
var autoComplete = new goog.ui.ac.AutoComplete(
matcher, renderer, inputHandler);
inputHandler.attachAutoComplete(autoComplete);
inputHandler.attachInputs(input);
return autoComplete;
};

View File

@@ -0,0 +1,162 @@
// Copyright 2006 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 Basic class for matching words in an array.
*
*/
goog.provide('goog.ui.ac.ArrayMatcher');
goog.require('goog.string');
/**
* Basic class for matching words in an array
* @constructor
* @param {Array} rows Dictionary of items to match. Can be objects if they
* have a toString method that returns the value to match against.
* @param {boolean=} opt_noSimilar if true, do not do similarity matches for the
* input token against the dictionary.
*/
goog.ui.ac.ArrayMatcher = function(rows, opt_noSimilar) {
this.rows_ = rows;
this.useSimilar_ = !opt_noSimilar;
};
/**
* Replaces the rows that this object searches over.
* @param {Array} rows Dictionary of items to match.
*/
goog.ui.ac.ArrayMatcher.prototype.setRows = function(rows) {
this.rows_ = rows;
};
/**
* Function used to pass matches to the autocomplete
* @param {string} token Token to match.
* @param {number} maxMatches Max number of matches to return.
* @param {Function} matchHandler callback to execute after matching.
* @param {string=} opt_fullString The full string from the input box.
*/
goog.ui.ac.ArrayMatcher.prototype.requestMatchingRows =
function(token, maxMatches, matchHandler, opt_fullString) {
var matches = this.getPrefixMatches(token, maxMatches);
if (matches.length == 0 && this.useSimilar_) {
matches = this.getSimilarRows(token, maxMatches);
}
matchHandler(token, matches);
};
/**
* Matches the token against the start of words in the row.
* @param {string} token Token to match.
* @param {number} maxMatches Max number of matches to return.
* @return {Array} Rows that match.
*/
goog.ui.ac.ArrayMatcher.prototype.getPrefixMatches =
function(token, maxMatches) {
var matches = [];
if (token != '') {
var escapedToken = goog.string.regExpEscape(token);
var matcher = new RegExp('(^|\\W+)' + escapedToken, 'i');
for (var i = 0; i < this.rows_.length && matches.length < maxMatches; i++) {
var row = this.rows_[i];
if (String(row).match(matcher)) {
matches.push(row);
}
}
}
return matches;
};
/**
* Matches the token against similar rows, by calculating "distance" between the
* terms.
* @param {string} token Token to match.
* @param {number} maxMatches Max number of matches to return.
* @return {Array} The best maxMatches rows.
*/
goog.ui.ac.ArrayMatcher.prototype.getSimilarRows = function(token, maxMatches) {
var results = [];
for (var index = 0; index < this.rows_.length; index++) {
var row = this.rows_[index];
var str = token.toLowerCase();
var txt = String(row).toLowerCase();
var score = 0;
if (txt.indexOf(str) != -1) {
score = parseInt((txt.indexOf(str) / 4).toString(), 10);
} else {
var arr = str.split('');
var lastPos = -1;
var penalty = 10;
for (var i = 0, c; c = arr[i]; i++) {
var pos = txt.indexOf(c);
if (pos > lastPos) {
var diff = pos - lastPos - 1;
if (diff > penalty - 5) {
diff = penalty - 5;
}
score += diff;
lastPos = pos;
} else {
score += penalty;
penalty += 5;
}
}
}
if (score < str.length * 6) {
results.push({
str: row,
score: score,
index: index
});
}
}
results.sort(function(a, b) {
var diff = a.score - b.score;
if (diff != 0) {
return diff;
}
return a.index - b.index;
});
var matches = [];
for (var i = 0; i < maxMatches && i < results.length; i++) {
matches.push(results[i].str);
}
return matches;
};

View File

@@ -0,0 +1,919 @@
// Copyright 2006 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 Gmail-like AutoComplete logic.
*
* @see ../../demos/autocomplete-basic.html
*/
goog.provide('goog.ui.ac.AutoComplete');
goog.provide('goog.ui.ac.AutoComplete.EventType');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.object');
/**
* This is the central manager class for an AutoComplete instance. The matcher
* can specify disabled rows that should not be hilited or selected by
* implementing <code>isRowDisabled(row):boolean</code> for each autocomplete
* row. No row will not be considered disabled if this method is not
* implemented.
*
* @param {Object} matcher A data source and row matcher, implements
* <code>requestMatchingRows(token, maxMatches, matchCallback)</code>.
* @param {goog.events.EventTarget} renderer An object that implements
* <code>
* isVisible():boolean<br>
* renderRows(rows:Array, token:string, target:Element);<br>
* hiliteId(row-id:number);<br>
* dismiss();<br>
* dispose():
* </code>.
* @param {Object} selectionHandler An object that implements
* <code>
* selectRow(row);<br>
* update(opt_force);
* </code>.
*
* @constructor
* @extends {goog.events.EventTarget}
*/
goog.ui.ac.AutoComplete = function(matcher, renderer, selectionHandler) {
goog.events.EventTarget.call(this);
/**
* A data-source which provides autocomplete suggestions.
*
* TODO(user): Tighten the type to !Object.
*
* @type {Object}
* @protected
* @suppress {underscore}
*/
this.matcher_ = matcher;
/**
* A handler which interacts with the input DOM element (textfield, textarea,
* or richedit).
*
* TODO(user): Tighten the type to !Object.
*
* @type {Object}
* @protected
* @suppress {underscore}
*/
this.selectionHandler_ = selectionHandler;
/**
* A renderer to render/show/highlight/hide the autocomplete menu.
* @type {goog.events.EventTarget}
* @protected
* @suppress {underscore}
*/
this.renderer_ = renderer;
goog.events.listen(
renderer,
[
goog.ui.ac.AutoComplete.EventType.HILITE,
goog.ui.ac.AutoComplete.EventType.SELECT,
goog.ui.ac.AutoComplete.EventType.CANCEL_DISMISS,
goog.ui.ac.AutoComplete.EventType.DISMISS
],
this.handleEvent, false, this);
/**
* Currently typed token which will be used for completion.
* @type {?string}
* @protected
* @suppress {underscore}
*/
this.token_ = null;
/**
* Autocomplete suggestion items.
* @type {Array}
* @protected
* @suppress {underscore}
*/
this.rows_ = [];
/**
* Id of the currently highlighted row.
* @type {number}
* @protected
* @suppress {underscore}
*/
this.hiliteId_ = -1;
/**
* Id of the first row in autocomplete menu. Note that new ids are assigned
* everytime new suggestions are fetched.
*
* TODO(user): Figure out what subclass does with this value
* and whether we should expose a more proper API.
*
* @type {number}
* @protected
* @suppress {underscore}
*/
this.firstRowId_ = 0;
/**
* The target HTML node for displaying.
* @type {Element}
* @protected
* @suppress {underscore}
*/
this.target_ = null;
/**
* The timer id for dismissing autocomplete menu with a delay.
* @type {?number}
* @private
*/
this.dismissTimer_ = null;
/**
* Mapping from text input element to the anchor element. If the
* mapping does not exist, the input element will act as the anchor
* element.
* @type {Object.<Element>}
* @private
*/
this.inputToAnchorMap_ = {};
};
goog.inherits(goog.ui.ac.AutoComplete, goog.events.EventTarget);
/**
* The maximum number of matches that should be returned
* @type {number}
* @private
*/
goog.ui.ac.AutoComplete.prototype.maxMatches_ = 10;
/**
* True iff the first row should automatically be highlighted
* @type {boolean}
* @private
*/
goog.ui.ac.AutoComplete.prototype.autoHilite_ = true;
/**
* True iff the user can unhilight all rows by pressing the up arrow.
* @type {boolean}
* @private
*/
goog.ui.ac.AutoComplete.prototype.allowFreeSelect_ = false;
/**
* True iff item selection should wrap around from last to first. If
* allowFreeSelect_ is on in conjunction, there is a step of free selection
* before wrapping.
* @type {boolean}
* @private
*/
goog.ui.ac.AutoComplete.prototype.wrap_ = false;
/**
* Whether completion from suggestion triggers fetching new suggestion.
* @type {boolean}
* @private
*/
goog.ui.ac.AutoComplete.prototype.triggerSuggestionsOnUpdate_ = false;
/**
* Events associated with the autocomplete
* @enum {string}
*/
goog.ui.ac.AutoComplete.EventType = {
/** A row has been highlighted by the renderer */
ROW_HILITE: 'rowhilite',
// Note: The events below are used for internal autocomplete events only and
// should not be used in non-autocomplete code.
/** A row has been mouseovered and should be highlighted by the renderer. */
HILITE: 'hilite',
/** A row has been selected by the renderer */
SELECT: 'select',
/** A dismiss event has occurred */
DISMISS: 'dismiss',
/** Event that cancels a dismiss event */
CANCEL_DISMISS: 'canceldismiss',
/**
* Field value was updated. A row field is included and is non-null when a
* row has been selected. The value of the row typically includes fields:
* contactData and formattedValue as well as a toString function (though none
* of these fields are guaranteed to exist). The row field may be used to
* return custom-type row data.
*/
UPDATE: 'update',
/**
* The list of suggestions has been updated, usually because either the list
* has opened, or because the user has typed another character and the
* suggestions have been updated, or the user has dismissed the autocomplete.
*/
SUGGESTIONS_UPDATE: 'suggestionsupdate'
};
/**
* @return {!Object} The data source providing the `autocomplete
* suggestions.
*/
goog.ui.ac.AutoComplete.prototype.getMatcher = function() {
return goog.asserts.assert(this.matcher_);
};
/**
* Sets the data source providing the autocomplete suggestions.
*
* See constructor documentation for the interface.
*
* @param {!Object} matcher The matcher.
* @protected
*/
goog.ui.ac.AutoComplete.prototype.setMatcher = function(matcher) {
this.matcher_ = matcher;
};
/**
* @return {!Object} The handler used to interact with the input DOM
* element (textfield, textarea, or richedit), e.g. to update the
* input DOM element with selected value.
* @protected
*/
goog.ui.ac.AutoComplete.prototype.getSelectionHandler = function() {
return goog.asserts.assert(this.selectionHandler_);
};
/**
* @return {goog.events.EventTarget} The renderer that
* renders/shows/highlights/hides the autocomplete menu.
* See constructor documentation for the expected renderer API.
*/
goog.ui.ac.AutoComplete.prototype.getRenderer = function() {
return this.renderer_;
};
/**
* Sets the renderer that renders/shows/highlights/hides the autocomplete
* menu.
*
* See constructor documentation for the expected renderer API.
*
* @param {goog.events.EventTarget} renderer The renderer.
* @protected
*/
goog.ui.ac.AutoComplete.prototype.setRenderer = function(renderer) {
this.renderer_ = renderer;
};
/**
* @return {?string} The currently typed token used for completion.
* @protected
*/
goog.ui.ac.AutoComplete.prototype.getToken = function() {
return this.token_;
};
/**
* Sets the current token (without changing the rendered autocompletion).
*
* NOTE(user): This method will likely go away when we figure
* out a better API.
*
* @param {?string} token The new token.
* @protected
*/
goog.ui.ac.AutoComplete.prototype.setTokenInternal = function(token) {
this.token_ = token;
};
/**
* @param {number} index The suggestion index, must be within the
* interval [0, this.getSuggestionCount()).
* @return {Object} The currently suggested item at the given index
* (or null if there is none).
*/
goog.ui.ac.AutoComplete.prototype.getSuggestion = function(index) {
return this.rows_[index];
};
/**
* @return {!Array} The current autocomplete suggestion items.
*/
goog.ui.ac.AutoComplete.prototype.getAllSuggestions = function() {
return goog.asserts.assert(this.rows_);
};
/**
* @return {number} The number of currently suggested items.
*/
goog.ui.ac.AutoComplete.prototype.getSuggestionCount = function() {
return this.rows_.length;
};
/**
* @return {number} The id (not index!) of the currently highlighted row.
*/
goog.ui.ac.AutoComplete.prototype.getHighlightedId = function() {
return this.hiliteId_;
};
/**
* Sets the current highlighted row to the given id (not index). Note
* that this does not change any rendering.
*
* NOTE(user): This method will likely go away when we figure
* out a better API.
*
* @param {number} id The new highlighted row id.
*/
goog.ui.ac.AutoComplete.prototype.setHighlightedIdInternal = function(id) {
this.hiliteId_ = id;
};
/**
* Generic event handler that handles any events this object is listening to.
* @param {goog.events.Event} e Event Object.
*/
goog.ui.ac.AutoComplete.prototype.handleEvent = function(e) {
if (e.target == this.renderer_) {
switch (e.type) {
case goog.ui.ac.AutoComplete.EventType.HILITE:
this.hiliteId(/** @type {number} */ (e.row));
break;
case goog.ui.ac.AutoComplete.EventType.SELECT:
// e.row can be either a valid row number or empty.
var rowId = /** @type {number} */ (e.row);
var index = this.getIndexOfId(rowId);
var row = this.rows_[index];
// Make sure the row selected is not a disabled row.
var rowDisabled = !!row && this.matcher_.isRowDisabled &&
this.matcher_.isRowDisabled(row);
if (rowId && row && !rowDisabled && this.hiliteId_ != rowId) {
// Event target row not currently highlighted - fix the mismatch.
this.hiliteId(rowId);
}
if (!rowDisabled) {
// Note that rowDisabled can be false even if e.row does not
// contain a valid row ID; at least one client depends on us
// proceeding anyway.
this.selectHilited();
}
break;
case goog.ui.ac.AutoComplete.EventType.CANCEL_DISMISS:
this.cancelDelayedDismiss();
break;
case goog.ui.ac.AutoComplete.EventType.DISMISS:
this.dismissOnDelay();
break;
}
}
};
/**
* Sets the max number of matches to fetch from the Matcher.
*
* @param {number} max Max number of matches.
*/
goog.ui.ac.AutoComplete.prototype.setMaxMatches = function(max) {
this.maxMatches_ = max;
};
/**
* Sets whether or not the first row should be highlighted by default.
*
* @param {boolean} autoHilite true iff the first row should be
* highlighted by default.
*/
goog.ui.ac.AutoComplete.prototype.setAutoHilite = function(autoHilite) {
this.autoHilite_ = autoHilite;
};
/**
* Sets whether or not the up/down arrow can unhilite all rows.
*
* @param {boolean} allowFreeSelect true iff the up arrow can unhilite all rows.
*/
goog.ui.ac.AutoComplete.prototype.setAllowFreeSelect =
function(allowFreeSelect) {
this.allowFreeSelect_ = allowFreeSelect;
};
/**
* Sets whether or not selections can wrap around the edges.
*
* @param {boolean} wrap true iff sections should wrap around the edges.
*/
goog.ui.ac.AutoComplete.prototype.setWrap = function(wrap) {
this.wrap_ = wrap;
};
/**
* Sets whether or not to request new suggestions immediately after completion
* of a suggestion.
*
* @param {boolean} triggerSuggestionsOnUpdate true iff completion should fetch
* new suggestions.
*/
goog.ui.ac.AutoComplete.prototype.setTriggerSuggestionsOnUpdate = function(
triggerSuggestionsOnUpdate) {
this.triggerSuggestionsOnUpdate_ = triggerSuggestionsOnUpdate;
};
/**
* Sets the token to match against. This triggers calls to the Matcher to
* fetch the matches (up to maxMatches), and then it triggers a call to
* <code>renderer.renderRows()</code>.
*
* @param {string} token The string for which to search in the Matcher.
* @param {string=} opt_fullString Optionally, the full string in the input
* field.
*/
goog.ui.ac.AutoComplete.prototype.setToken = function(token, opt_fullString) {
if (this.token_ == token) {
return;
}
this.token_ = token;
this.matcher_.requestMatchingRows(this.token_,
this.maxMatches_, goog.bind(this.matchListener_, this), opt_fullString);
this.cancelDelayedDismiss();
};
/**
* Gets the current target HTML node for displaying autocomplete UI.
* @return {Element} The current target HTML node for displaying autocomplete
* UI.
*/
goog.ui.ac.AutoComplete.prototype.getTarget = function() {
return this.target_;
};
/**
* Sets the current target HTML node for displaying autocomplete UI.
* Can be an implementation specific definition of how to display UI in relation
* to the target node.
* This target will be passed into <code>renderer.renderRows()</code>
*
* @param {Element} target The current target HTML node for displaying
* autocomplete UI.
*/
goog.ui.ac.AutoComplete.prototype.setTarget = function(target) {
this.target_ = target;
};
/**
* @return {boolean} Whether the autocomplete's renderer is open.
*/
goog.ui.ac.AutoComplete.prototype.isOpen = function() {
return this.renderer_.isVisible();
};
/**
* @return {number} Number of rows in the autocomplete.
* @deprecated Use this.getSuggestionCount().
*/
goog.ui.ac.AutoComplete.prototype.getRowCount = function() {
return this.getSuggestionCount();
};
/**
* Moves the hilite to the next non-disabled row.
* Calls renderer.hiliteId() when there's something to do.
* @return {boolean} Returns true on a successful hilite.
*/
goog.ui.ac.AutoComplete.prototype.hiliteNext = function() {
var lastId = this.firstRowId_ + this.rows_.length - 1;
var toHilite = this.hiliteId_;
// Hilite the next row, skipping any disabled rows.
for (var i = 0; i < this.rows_.length; i++) {
// Increment to the next row.
if (toHilite >= this.firstRowId_ && toHilite < lastId) {
toHilite++;
} else if (toHilite == -1) {
toHilite = this.firstRowId_;
} else if (this.allowFreeSelect_ && toHilite == lastId) {
this.hiliteId(-1);
return false;
} else if (this.wrap_ && toHilite == lastId) {
toHilite = this.firstRowId_;
} else {
return false;
}
if (this.hiliteId(toHilite)) {
return true;
}
}
return false;
};
/**
* Moves the hilite to the previous non-disabled row. Calls
* renderer.hiliteId() when there's something to do.
* @return {boolean} Returns true on a successful hilite.
*/
goog.ui.ac.AutoComplete.prototype.hilitePrev = function() {
var lastId = this.firstRowId_ + this.rows_.length - 1;
var toHilite = this.hiliteId_;
// Hilite the previous row, skipping any disabled rows.
for (var i = 0; i < this.rows_.length; i++) {
// Decrement to the previous row.
if (toHilite > this.firstRowId_) {
toHilite--;
} else if (this.allowFreeSelect_ && toHilite == this.firstRowId_) {
this.hiliteId(-1);
return false;
} else if (this.wrap_ && (toHilite == -1 || toHilite == this.firstRowId_)) {
toHilite = lastId;
} else {
return false;
}
if (this.hiliteId(toHilite)) {
return true;
}
}
return false;
};
/**
* Hilites the id if it's valid and the row is not disabled, otherwise does
* nothing.
* @param {number} id A row id (not index).
* @return {boolean} Whether the id was hilited. Returns false if the row is
* disabled.
*/
goog.ui.ac.AutoComplete.prototype.hiliteId = function(id) {
var index = this.getIndexOfId(id);
var row = this.rows_[index];
var rowDisabled = !!row && this.matcher_.isRowDisabled &&
this.matcher_.isRowDisabled(row);
if (!rowDisabled) {
this.hiliteId_ = id;
this.renderer_.hiliteId(id);
return index != -1;
}
return false;
};
/**
* Hilites the index, if it's valid and the row is not disabled, otherwise does
* nothing.
* @param {number} index The row's index.
* @return {boolean} Whether the index was hilited.
*/
goog.ui.ac.AutoComplete.prototype.hiliteIndex = function(index) {
return this.hiliteId(this.getIdOfIndex_(index));
};
/**
* If there are any current matches, this passes the hilited row data to
* <code>selectionHandler.selectRow()</code>
* @return {boolean} Whether there are any current matches.
*/
goog.ui.ac.AutoComplete.prototype.selectHilited = function() {
var index = this.getIndexOfId(this.hiliteId_);
if (index != -1) {
var selectedRow = this.rows_[index];
var suppressUpdate = this.selectionHandler_.selectRow(selectedRow);
if (this.triggerSuggestionsOnUpdate_) {
this.token_ = null;
this.dismissOnDelay();
} else {
this.dismiss();
}
if (!suppressUpdate) {
this.dispatchEvent({
type: goog.ui.ac.AutoComplete.EventType.UPDATE,
row: selectedRow
});
if (this.triggerSuggestionsOnUpdate_) {
this.selectionHandler_.update(true);
}
}
return true;
} else {
this.dismiss();
this.dispatchEvent(
{
type: goog.ui.ac.AutoComplete.EventType.UPDATE,
row: null
});
return false;
}
};
/**
* Returns whether or not the autocomplete is open and has a highlighted row.
* @return {boolean} Whether an autocomplete row is highlighted.
*/
goog.ui.ac.AutoComplete.prototype.hasHighlight = function() {
return this.isOpen() && this.getIndexOfId(this.hiliteId_) != -1;
};
/**
* Clears out the token, rows, and hilite, and calls
* <code>renderer.dismiss()</code>
*/
goog.ui.ac.AutoComplete.prototype.dismiss = function() {
this.hiliteId_ = -1;
this.token_ = null;
this.firstRowId_ += this.rows_.length;
this.rows_ = [];
window.clearTimeout(this.dismissTimer_);
this.dismissTimer_ = null;
this.renderer_.dismiss();
this.dispatchEvent(goog.ui.ac.AutoComplete.EventType.SUGGESTIONS_UPDATE);
this.dispatchEvent(goog.ui.ac.AutoComplete.EventType.DISMISS);
};
/**
* Call a dismiss after a delay, if there's already a dismiss active, ignore.
*/
goog.ui.ac.AutoComplete.prototype.dismissOnDelay = function() {
if (!this.dismissTimer_) {
this.dismissTimer_ = window.setTimeout(goog.bind(this.dismiss, this), 100);
}
};
/**
* Cancels any delayed dismiss events immediately.
* @return {boolean} Whether a delayed dismiss was cancelled.
* @private
*/
goog.ui.ac.AutoComplete.prototype.immediatelyCancelDelayedDismiss_ =
function() {
if (this.dismissTimer_) {
window.clearTimeout(this.dismissTimer_);
this.dismissTimer_ = null;
return true;
}
return false;
};
/**
* Cancel the active delayed dismiss if there is one.
*/
goog.ui.ac.AutoComplete.prototype.cancelDelayedDismiss = function() {
// Under certain circumstances a cancel event occurs immediately prior to a
// delayedDismiss event that it should be cancelling. To handle this situation
// properly, a timer is used to stop that event.
// Using only the timer creates undesirable behavior when the cancel occurs
// less than 10ms before the delayed dismiss timout ends. If that happens the
// clearTimeout() will occur too late and have no effect.
if (!this.immediatelyCancelDelayedDismiss_()) {
window.setTimeout(goog.bind(this.immediatelyCancelDelayedDismiss_, this),
10);
}
};
/** @override */
goog.ui.ac.AutoComplete.prototype.disposeInternal = function() {
goog.ui.ac.AutoComplete.superClass_.disposeInternal.call(this);
delete this.inputToAnchorMap_;
this.renderer_.dispose();
this.selectionHandler_.dispose();
this.matcher_ = null;
};
/**
* Callback passed to Matcher when requesting matches for a token.
* This might be called synchronously, or asynchronously, or both, for
* any implementation of a Matcher.
* If the Matcher calls this back, with the same token this AutoComplete
* has set currently, then this will package the matching rows in object
* of the form
* <pre>
* {
* id: an integer ID unique to this result set and AutoComplete instance,
* data: the raw row data from Matcher
* }
* </pre>
*
* @param {string} matchedToken Token that corresponds with the rows.
* @param {!Array} rows Set of data that match the given token.
* @param {(boolean|goog.ui.ac.RenderOptions)=} opt_options If true,
* keeps the currently hilited (by index) element hilited. If false not.
* Otherwise a RenderOptions object.
* @private
*/
goog.ui.ac.AutoComplete.prototype.matchListener_ =
function(matchedToken, rows, opt_options) {
if (this.token_ != matchedToken) {
// Matcher's response token doesn't match current token.
// This is probably an async response that came in after
// the token was changed, so don't do anything.
return;
}
this.renderRows(rows, opt_options);
};
/**
* Renders the rows and adds highlighting.
* @param {!Array} rows Set of data that match the given token.
* @param {(boolean|goog.ui.ac.RenderOptions)=} opt_options If true,
* keeps the currently hilited (by index) element hilited. If false not.
* Otherwise a RenderOptions object.
*/
goog.ui.ac.AutoComplete.prototype.renderRows = function(rows, opt_options) {
// The optional argument should be a RenderOptions object. It can be a
// boolean for backwards compatibility, defaulting to false.
var optionsObj = goog.typeOf(opt_options) == 'object' && opt_options;
var preserveHilited =
optionsObj ? optionsObj.getPreserveHilited() : opt_options;
var indexToHilite = preserveHilited ? this.getIndexOfId(this.hiliteId_) : -1;
// Current token matches the matcher's response token.
this.firstRowId_ += this.rows_.length;
this.rows_ = rows;
var rendRows = [];
for (var i = 0; i < rows.length; ++i) {
rendRows.push({
id: this.getIdOfIndex_(i),
data: rows[i]
});
}
var anchor = null;
if (this.target_) {
anchor = this.inputToAnchorMap_[goog.getUid(this.target_)] || this.target_;
}
this.renderer_.setAnchorElement(anchor);
this.renderer_.renderRows(rendRows, this.token_, this.target_);
var autoHilite = this.autoHilite_;
if (optionsObj && optionsObj.getAutoHilite() !== undefined) {
autoHilite = optionsObj.getAutoHilite();
}
this.hiliteId_ = -1;
if ((autoHilite || indexToHilite >= 0) &&
rendRows.length != 0 &&
this.token_) {
if (indexToHilite >= 0) {
this.hiliteId(this.getIdOfIndex_(indexToHilite));
} else {
// Hilite the first non-disabled row.
this.hiliteNext();
}
}
this.dispatchEvent(goog.ui.ac.AutoComplete.EventType.SUGGESTIONS_UPDATE);
};
/**
* Gets the index corresponding to a particular id.
* @param {number} id A unique id for the row.
* @return {number} A valid index into rows_, or -1 if the id is invalid.
* @protected
*/
goog.ui.ac.AutoComplete.prototype.getIndexOfId = function(id) {
var index = id - this.firstRowId_;
if (index < 0 || index >= this.rows_.length) {
return -1;
}
return index;
};
/**
* Gets the id corresponding to a particular index. (Does no checking.)
* @param {number} index The index of a row in the result set.
* @return {number} The id that currently corresponds to that index.
* @private
*/
goog.ui.ac.AutoComplete.prototype.getIdOfIndex_ = function(index) {
return this.firstRowId_ + index;
};
/**
* Attach text areas or input boxes to the autocomplete by DOM reference. After
* elements are attached to the autocomplete, when a user types they will see
* the autocomplete drop down.
* @param {...Element} var_args Variable args: Input or text area elements to
* attach the autocomplete too.
*/
goog.ui.ac.AutoComplete.prototype.attachInputs = function(var_args) {
// Delegate to the input handler
var inputHandler = /** @type {goog.ui.ac.InputHandler} */
(this.selectionHandler_);
inputHandler.attachInputs.apply(inputHandler, arguments);
};
/**
* Detach text areas or input boxes to the autocomplete by DOM reference.
* @param {...Element} var_args Variable args: Input or text area elements to
* detach from the autocomplete.
*/
goog.ui.ac.AutoComplete.prototype.detachInputs = function(var_args) {
// Delegate to the input handler
var inputHandler = /** @type {goog.ui.ac.InputHandler} */
(this.selectionHandler_);
inputHandler.detachInputs.apply(inputHandler, arguments);
// Remove mapping from input to anchor if one exists.
goog.array.forEach(arguments, function(input) {
goog.object.remove(this.inputToAnchorMap_, goog.getUid(input));
}, this);
};
/**
* Attaches the autocompleter to a text area or text input element
* with an anchor element. The anchor element is the element the
* autocomplete box will be positioned against.
* @param {Element} inputElement The input element. May be 'textarea',
* text 'input' element, or any other element that exposes similar
* interface.
* @param {Element} anchorElement The anchor element.
*/
goog.ui.ac.AutoComplete.prototype.attachInputWithAnchor = function(
inputElement, anchorElement) {
this.inputToAnchorMap_[goog.getUid(inputElement)] = anchorElement;
this.attachInputs(inputElement);
};
/**
* Forces an update of the display.
* @param {boolean=} opt_force Whether to force an update.
*/
goog.ui.ac.AutoComplete.prototype.update = function(opt_force) {
var inputHandler = /** @type {goog.ui.ac.InputHandler} */
(this.selectionHandler_);
inputHandler.update(opt_force);
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,114 @@
// Copyright 2007 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 Factory class to create a simple autocomplete that will match
* from an array of data provided via ajax.
*
* @see ../../demos/autocompleteremote.html
*/
goog.provide('goog.ui.ac.Remote');
goog.require('goog.ui.ac.AutoComplete');
goog.require('goog.ui.ac.InputHandler');
goog.require('goog.ui.ac.RemoteArrayMatcher');
goog.require('goog.ui.ac.Renderer');
/**
* Factory class for building a remote autocomplete widget that autocompletes
* an inputbox or text area from a data array provided via ajax.
* @param {string} url The Uri which generates the auto complete matches.
* @param {Element} input Input element or text area.
* @param {boolean=} opt_multi Whether to allow multiple entries; defaults
* to false.
* @param {boolean=} opt_useSimilar Whether to use similar matches; e.g.
* "gost" => "ghost".
* @constructor
* @extends {goog.ui.ac.AutoComplete}
*/
goog.ui.ac.Remote = function(url, input, opt_multi, opt_useSimilar) {
var matcher = new goog.ui.ac.RemoteArrayMatcher(url, !opt_useSimilar);
this.matcher_ = matcher;
var renderer = new goog.ui.ac.Renderer();
var inputhandler = new goog.ui.ac.InputHandler(null, null, !!opt_multi, 300);
goog.ui.ac.AutoComplete.call(this, matcher, renderer, inputhandler);
inputhandler.attachAutoComplete(this);
inputhandler.attachInputs(input);
};
goog.inherits(goog.ui.ac.Remote, goog.ui.ac.AutoComplete);
/**
* Set whether or not standard highlighting should be used when rendering rows.
* @param {boolean} useStandardHighlighting true if standard highlighting used.
*/
goog.ui.ac.Remote.prototype.setUseStandardHighlighting =
function(useStandardHighlighting) {
this.renderer_.setUseStandardHighlighting(useStandardHighlighting);
};
/**
* Gets the attached InputHandler object.
* @return {goog.ui.ac.InputHandler} The input handler.
*/
goog.ui.ac.Remote.prototype.getInputHandler = function() {
return /** @type {goog.ui.ac.InputHandler} */ (
this.selectionHandler_);
};
/**
* Set the send method ("GET", "POST") for the matcher.
* @param {string} method The send method; default: GET.
*/
goog.ui.ac.Remote.prototype.setMethod = function(method) {
this.matcher_.setMethod(method);
};
/**
* Set the post data for the matcher.
* @param {string} content Post data.
*/
goog.ui.ac.Remote.prototype.setContent = function(content) {
this.matcher_.setContent(content);
};
/**
* Set the HTTP headers for the matcher.
* @param {Object|goog.structs.Map} headers Map of headers to add to the
* request.
*/
goog.ui.ac.Remote.prototype.setHeaders = function(headers) {
this.matcher_.setHeaders(headers);
};
/**
* Set the timeout interval for the matcher.
* @param {number} interval Number of milliseconds after which an
* incomplete request will be aborted; 0 means no timeout is set.
*/
goog.ui.ac.Remote.prototype.setTimeoutInterval = function(interval) {
this.matcher_.setTimeoutInterval(interval);
};

View File

@@ -0,0 +1,271 @@
// Copyright 2007 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 Class that retrieves autocomplete matches via an ajax call.
*
*/
goog.provide('goog.ui.ac.RemoteArrayMatcher');
goog.require('goog.Disposable');
goog.require('goog.Uri');
goog.require('goog.events');
goog.require('goog.json');
goog.require('goog.net.EventType');
goog.require('goog.net.XhrIo');
/**
* An array matcher that requests matches via ajax.
* @param {string} url The Uri which generates the auto complete matches. The
* search term is passed to the server as the 'token' query param.
* @param {boolean=} opt_noSimilar If true, request that the server does not do
* similarity matches for the input token against the dictionary.
* The value is sent to the server as the 'use_similar' query param which is
* either "1" (opt_noSimilar==false) or "0" (opt_noSimilar==true).
* @constructor
* @extends {goog.Disposable}
*/
goog.ui.ac.RemoteArrayMatcher = function(url, opt_noSimilar) {
goog.Disposable.call(this);
/**
* The base URL for the ajax call. The token and max_matches are added as
* query params.
* @type {string}
* @private
*/
this.url_ = url;
/**
* Whether similar matches should be found as well. This is sent as a hint
* to the server only.
* @type {boolean}
* @private
*/
this.useSimilar_ = !opt_noSimilar;
/**
* The XhrIo object used for making remote requests. When a new request
* is made, the current one is aborted and the new one sent.
* @type {goog.net.XhrIo}
* @private
*/
this.xhr_ = new goog.net.XhrIo();
};
goog.inherits(goog.ui.ac.RemoteArrayMatcher, goog.Disposable);
/**
* The HTTP send method (GET, POST) to use when making the ajax call.
* @type {string}
* @private
*/
goog.ui.ac.RemoteArrayMatcher.prototype.method_ = 'GET';
/**
* Data to submit during a POST.
* @type {string|undefined}
* @private
*/
goog.ui.ac.RemoteArrayMatcher.prototype.content_ = undefined;
/**
* Headers to send with every HTTP request.
* @type {Object|goog.structs.Map}
* @private
*/
goog.ui.ac.RemoteArrayMatcher.prototype.headers_ = null;
/**
* Key to the listener on XHR. Used to clear previous listeners.
* @type {goog.events.Key}
* @private
*/
goog.ui.ac.RemoteArrayMatcher.prototype.lastListenerKey_ = null;
/**
* Set the send method ("GET", "POST").
* @param {string} method The send method; default: GET.
*/
goog.ui.ac.RemoteArrayMatcher.prototype.setMethod = function(method) {
this.method_ = method;
};
/**
* Set the post data.
* @param {string} content Post data.
*/
goog.ui.ac.RemoteArrayMatcher.prototype.setContent = function(content) {
this.content_ = content;
};
/**
* Set the HTTP headers.
* @param {Object|goog.structs.Map} headers Map of headers to add to the
* request.
*/
goog.ui.ac.RemoteArrayMatcher.prototype.setHeaders = function(headers) {
this.headers_ = headers;
};
/**
* Set the timeout interval.
* @param {number} interval Number of milliseconds after which an
* incomplete request will be aborted; 0 means no timeout is set.
*/
goog.ui.ac.RemoteArrayMatcher.prototype.setTimeoutInterval =
function(interval) {
this.xhr_.setTimeoutInterval(interval);
};
/**
* Builds a complete GET-style URL, given the base URI and autocomplete related
* parameter values.
* <b>Override this to build any customized lookup URLs.</b>
* <b>Can be used to change request method and any post content as well.</b>
* @param {string} uri The base URI of the request target.
* @param {string} token Current token in autocomplete.
* @param {number} maxMatches Maximum number of matches required.
* @param {boolean} useSimilar A hint to the server.
* @param {string=} opt_fullString Complete text in the input element.
* @return {?string} The complete url. Return null if no request should be sent.
* @protected
*/
goog.ui.ac.RemoteArrayMatcher.prototype.buildUrl = function(uri,
token, maxMatches, useSimilar, opt_fullString) {
var url = new goog.Uri(uri);
url.setParameterValue('token', token);
url.setParameterValue('max_matches', String(maxMatches));
url.setParameterValue('use_similar', String(Number(useSimilar)));
return url.toString();
};
/**
* Returns whether the suggestions should be updated?
* <b>Override this to prevent updates eg - when token is empty.</b>
* @param {string} uri The base URI of the request target.
* @param {string} token Current token in autocomplete.
* @param {number} maxMatches Maximum number of matches required.
* @param {boolean} useSimilar A hint to the server.
* @param {string=} opt_fullString Complete text in the input element.
* @return {boolean} Whether new matches be requested.
* @protected
*/
goog.ui.ac.RemoteArrayMatcher.prototype.shouldRequestMatches =
function(uri, token, maxMatches, useSimilar, opt_fullString) {
return true;
};
/**
* Parses and retrieves the array of suggestions from XHR response.
* <b>Override this if the response is not a simple JSON array.</b>
* @param {string} responseText The XHR response text.
* @return {Array.<string>} The array of suggestions.
* @protected
*/
goog.ui.ac.RemoteArrayMatcher.prototype.parseResponseText = function(
responseText) {
var matches = [];
// If there is no response text, unsafeParse will throw a syntax error.
if (responseText) {
/** @preserveTry */
try {
matches = goog.json.unsafeParse(responseText);
} catch (exception) {
}
}
return /** @type {Array.<string>} */ (matches);
};
/**
* Handles the XHR response.
* @param {string} token The XHR autocomplete token.
* @param {Function} matchHandler The AutoComplete match handler.
* @param {goog.events.Event} event The XHR success event.
*/
goog.ui.ac.RemoteArrayMatcher.prototype.xhrCallback = function(token,
matchHandler, event) {
var text = event.target.getResponseText();
matchHandler(token, this.parseResponseText(text));
};
/**
* Retrieve a set of matching rows from the server via ajax.
* @param {string} token The text that should be matched; passed to the server
* as the 'token' query param.
* @param {number} maxMatches The maximum number of matches requested from the
* server; passed as the 'max_matches' query param. The server is
* responsible for limiting the number of matches that are returned.
* @param {Function} matchHandler Callback to execute on the result after
* matching.
* @param {string=} opt_fullString The full string from the input box.
*/
goog.ui.ac.RemoteArrayMatcher.prototype.requestMatchingRows =
function(token, maxMatches, matchHandler, opt_fullString) {
if (!this.shouldRequestMatches(this.url_, token, maxMatches, this.useSimilar_,
opt_fullString)) {
return;
}
// Set the query params on the URL.
var url = this.buildUrl(this.url_, token, maxMatches, this.useSimilar_,
opt_fullString);
if (!url) {
// Do nothing if there is no URL.
return;
}
// The callback evals the server response and calls the match handler on
// the array of matches.
var callback = goog.bind(this.xhrCallback, this, token, matchHandler);
// Abort the current request and issue the new one; prevent requests from
// being queued up by the browser with a slow server
if (this.xhr_.isActive()) {
this.xhr_.abort();
}
// This ensures if previous XHR is aborted or ends with error, the
// corresponding success-callbacks are cleared.
if (this.lastListenerKey_) {
goog.events.unlistenByKey(this.lastListenerKey_);
}
// Listen once ensures successful callback gets cleared by itself.
this.lastListenerKey_ = goog.events.listenOnce(this.xhr_,
goog.net.EventType.SUCCESS, callback);
this.xhr_.send(url, this.method_, this.content_, this.headers_);
};
/** @override */
goog.ui.ac.RemoteArrayMatcher.prototype.disposeInternal = function() {
this.xhr_.dispose();
goog.ui.ac.RemoteArrayMatcher.superClass_.disposeInternal.call(
this);
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,80 @@
// Copyright 2012 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 Options for rendering matches.
*
*/
goog.provide('goog.ui.ac.RenderOptions');
/**
* A simple class that contains options for rendering a set of autocomplete
* matches. Used as an optional argument in the callback from the matcher.
* @constructor
*/
goog.ui.ac.RenderOptions = function() {
};
/**
* Whether the current highlighting is to be preserved when displaying the new
* set of matches.
* @type {boolean}
* @private
*/
goog.ui.ac.RenderOptions.prototype.preserveHilited_ = false;
/**
* Whether the first match is to be highlighted. When undefined the autoHilite
* flag of the autocomplete is used.
* @type {boolean|undefined}
* @private
*/
goog.ui.ac.RenderOptions.prototype.autoHilite_;
/**
* @param {boolean} flag The new value for the preserveHilited_ flag.
*/
goog.ui.ac.RenderOptions.prototype.setPreserveHilited = function(flag) {
this.preserveHilited_ = flag;
};
/**
* @return {boolean} The value of the preserveHilited_ flag.
*/
goog.ui.ac.RenderOptions.prototype.getPreserveHilited = function() {
return this.preserveHilited_;
};
/**
* @param {boolean} flag The new value for the autoHilite_ flag.
*/
goog.ui.ac.RenderOptions.prototype.setAutoHilite = function(flag) {
this.autoHilite_ = flag;
};
/**
* @return {boolean|undefined} The value of the autoHilite_ flag.
*/
goog.ui.ac.RenderOptions.prototype.getAutoHilite = function() {
return this.autoHilite_;
};

View File

@@ -0,0 +1,58 @@
// Copyright 2007 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 Class for managing the interactions between a rich autocomplete
* object and a text-input or textarea.
*
*/
goog.provide('goog.ui.ac.RichInputHandler');
goog.require('goog.ui.ac.InputHandler');
/**
* Class for managing the interaction between an autocomplete object and a
* text-input or textarea.
* @param {?string=} opt_separators Seperators to split multiple entries.
* @param {?string=} opt_literals Characters used to delimit text literals.
* @param {?boolean=} opt_multi Whether to allow multiple entries
* (Default: true).
* @param {?number=} opt_throttleTime Number of milliseconds to throttle
* keyevents with (Default: 150).
* @constructor
* @extends {goog.ui.ac.InputHandler}
*/
goog.ui.ac.RichInputHandler = function(opt_separators, opt_literals,
opt_multi, opt_throttleTime) {
goog.ui.ac.InputHandler.call(this, opt_separators, opt_literals,
opt_multi, opt_throttleTime);
};
goog.inherits(goog.ui.ac.RichInputHandler, goog.ui.ac.InputHandler);
/**
* Selects the given rich row. The row's select(target) method is called.
* @param {Object} row The row to select.
* @return {boolean} Whether to suppress the update event.
* @override
*/
goog.ui.ac.RichInputHandler.prototype.selectRow = function(row) {
var suppressUpdate = goog.ui.ac.RichInputHandler.superClass_
.selectRow.call(this, row);
row.select(this.ac_.getTarget());
return suppressUpdate;
};

View File

@@ -0,0 +1,107 @@
// Copyright 2007 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 Factory class to create a rich autocomplete that will match
* from an array of data provided via ajax. The server returns a complex data
* structure that is used with client-side javascript functions to render the
* results.
*
* The server sends a list of the form:
* [["type1", {...}, {...}, ...], ["type2", {...}, {...}, ...], ...]
* The first element of each sublist is a string designating the type of the
* hashes in the sublist, each of which represents one match. The type string
* must be the name of a function(item) which converts the hash into a rich
* row that contains both a render(node, token) and a select(target) method.
* The render method is called by the renderer when rendering the rich row,
* and the select method is called by the RichInputHandler when the rich row is
* selected.
*
* @see ../../demos/autocompleterichremote.html
*/
goog.provide('goog.ui.ac.RichRemote');
goog.require('goog.ui.ac.AutoComplete');
goog.require('goog.ui.ac.Remote');
goog.require('goog.ui.ac.Renderer');
goog.require('goog.ui.ac.RichInputHandler');
goog.require('goog.ui.ac.RichRemoteArrayMatcher');
/**
* Factory class to create a rich autocomplete widget that autocompletes an
* inputbox or textarea from data provided via ajax. The server returns a
* complex data structure that is used with client-side javascript functions to
* render the results.
* @param {string} url The Uri which generates the auto complete matches.
* @param {Element} input Input element or text area.
* @param {boolean=} opt_multi Whether to allow multiple entries; defaults
* to false.
* @param {boolean=} opt_useSimilar Whether to use similar matches; e.g.
* "gost" => "ghost".
* @constructor
* @extends {goog.ui.ac.Remote}
*/
goog.ui.ac.RichRemote = function(url, input, opt_multi, opt_useSimilar) {
// Create a custom renderer that renders rich rows. The renderer calls
// row.render(node, token) for each row.
var customRenderer = {};
customRenderer.renderRow = function(row, token, node) {
return row.data.render(node, token);
};
/**
* A standard renderer that uses a custom row renderer to display the
* rich rows generated by this autocomplete widget.
* @type {goog.ui.ac.Renderer}
* @private
*/
var renderer = new goog.ui.ac.Renderer(null, customRenderer);
this.renderer_ = renderer;
/**
* A remote matcher that parses rich results returned by the server.
* @type {goog.ui.ac.RichRemoteArrayMatcher}
* @private
*/
var matcher = new goog.ui.ac.RichRemoteArrayMatcher(url,
!opt_useSimilar);
this.matcher_ = matcher;
/**
* An input handler that calls select on a row when it is selected.
* @type {goog.ui.ac.RichInputHandler}
* @private
*/
var inputhandler = new goog.ui.ac.RichInputHandler(null, null,
!!opt_multi, 300);
// Create the widget and connect it to the input handler.
goog.ui.ac.AutoComplete.call(this, matcher, renderer, inputhandler);
inputhandler.attachAutoComplete(this);
inputhandler.attachInputs(input);
};
goog.inherits(goog.ui.ac.RichRemote, goog.ui.ac.Remote);
/**
* Set the filter that is called before the array matches are returned.
* @param {Function} rowFilter A function(rows) that returns an array of rows as
* a subset of the rows input array.
*/
goog.ui.ac.RichRemote.prototype.setRowFilter = function(rowFilter) {
this.matcher_.setRowFilter(rowFilter);
};

View File

@@ -0,0 +1,125 @@
// Copyright 2007 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 Class that retrieves rich autocomplete matches, represented as
* a structured list of lists, via an ajax call. The first element of each
* sublist is the name of a client-side javascript function that converts the
* remaining sublist elements into rich rows.
*
*/
goog.provide('goog.ui.ac.RichRemoteArrayMatcher');
goog.require('goog.json');
goog.require('goog.ui.ac.RemoteArrayMatcher');
/**
* An array matcher that requests rich matches via ajax and converts them into
* rich rows.
* @param {string} url The Uri which generates the auto complete matches. The
* search term is passed to the server as the 'token' query param.
* @param {boolean=} opt_noSimilar If true, request that the server does not do
* similarity matches for the input token against the dictionary.
* The value is sent to the server as the 'use_similar' query param which is
* either "1" (opt_noSimilar==false) or "0" (opt_noSimilar==true).
* @constructor
* @extends {goog.ui.ac.RemoteArrayMatcher}
*/
goog.ui.ac.RichRemoteArrayMatcher = function(url, opt_noSimilar) {
goog.ui.ac.RemoteArrayMatcher.call(this, url, opt_noSimilar);
/**
* A function(rows) that is called before the array matches are returned.
* It runs client-side and filters the results given by the server before
* being rendered by the client.
* @type {Function}
* @private
*/
this.rowFilter_ = null;
};
goog.inherits(goog.ui.ac.RichRemoteArrayMatcher, goog.ui.ac.RemoteArrayMatcher);
/**
* Set the filter that is called before the array matches are returned.
* @param {Function} rowFilter A function(rows) that returns an array of rows as
* a subset of the rows input array.
*/
goog.ui.ac.RichRemoteArrayMatcher.prototype.setRowFilter = function(rowFilter) {
this.rowFilter_ = rowFilter;
};
/**
* Retrieve a set of matching rows from the server via ajax and convert them
* into rich rows.
* @param {string} token The text that should be matched; passed to the server
* as the 'token' query param.
* @param {number} maxMatches The maximum number of matches requested from the
* server; passed as the 'max_matches' query param. The server is
* responsible for limiting the number of matches that are returned.
* @param {Function} matchHandler Callback to execute on the result after
* matching.
* @override
*/
goog.ui.ac.RichRemoteArrayMatcher.prototype.requestMatchingRows =
function(token, maxMatches, matchHandler) {
// The RichRemoteArrayMatcher must map over the results and filter them
// before calling the request matchHandler. This is done by passing
// myMatchHandler to RemoteArrayMatcher.requestMatchingRows which maps,
// filters, and then calls matchHandler.
var myMatchHandler = goog.bind(function(token, matches) {
/** @preserveTry */
try {
var rows = [];
for (var i = 0; i < matches.length; i++) {
var func = /** @type {!Function} */
(goog.json.unsafeParse(matches[i][0]));
for (var j = 1; j < matches[i].length; j++) {
var richRow = func(matches[i][j]);
rows.push(richRow);
// If no render function was provided, set the node's innerHTML.
if (typeof richRow.render == 'undefined') {
richRow.render = function(node, token) {
node.innerHTML = richRow.toString();
};
}
// If no select function was provided, set the text of the input.
if (typeof richRow.select == 'undefined') {
richRow.select = function(target) {
target.value = richRow.toString();
};
}
}
}
if (this.rowFilter_) {
rows = this.rowFilter_(rows);
}
matchHandler(token, rows);
} catch (exception) {
// TODO(user): Is this what we want?
matchHandler(token, []);
}
}, this);
// Call the super's requestMatchingRows with myMatchHandler
goog.ui.ac.RichRemoteArrayMatcher.superClass_
.requestMatchingRows.call(this, token, maxMatches, myMatchHandler);
};

View File

@@ -0,0 +1,347 @@
// Copyright 2006 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 Activity Monitor.
*
* Fires throttled events when a user interacts with the specified document.
* This class also exposes the amount of time since the last user event.
*
* If you would prefer to get BECOME_ACTIVE and BECOME_IDLE events when the
* user changes states, then you should use the IdleTimer class instead.
*
*/
goog.provide('goog.ui.ActivityMonitor');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
/**
* Once initialized with a document, the activity monitor can be queried for
* the current idle time.
*
* @param {goog.dom.DomHelper|Array.<goog.dom.DomHelper>=} opt_domHelper
* DomHelper which contains the document(s) to listen to. If null, the
* default document is usedinstead.
* @param {boolean=} opt_useBubble Whether to use the bubble phase to listen for
* events. By default listens on the capture phase so that it won't miss
* events that get stopPropagation/cancelBubble'd. However, this can cause
* problems in IE8 if the page loads multiple scripts that include the
* closure event handling code.
*
* @constructor
* @extends {goog.events.EventTarget}
*/
goog.ui.ActivityMonitor = function(opt_domHelper, opt_useBubble) {
goog.events.EventTarget.call(this);
/**
* Array of documents that are being listened to.
* @type {Array.<Document>}
* @private
*/
this.documents_ = [];
/**
* Whether to use the bubble phase to listen for events.
* @type {boolean}
* @private
*/
this.useBubble_ = !!opt_useBubble;
/**
* The event handler.
* @type {goog.events.EventHandler}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
/**
* Whether the current window is an iframe.
* TODO(user): Move to goog.dom.
* @type {boolean}
* @private
*/
this.isIframe_ = window.parent != window;
if (!opt_domHelper) {
this.addDocument(goog.dom.getDomHelper().getDocument());
} else if (goog.isArray(opt_domHelper)) {
for (var i = 0; i < opt_domHelper.length; i++) {
this.addDocument(opt_domHelper[i].getDocument());
}
} else {
this.addDocument(opt_domHelper.getDocument());
}
/**
* The time (in milliseconds) of the last user event.
* @type {number}
* @private
*/
this.lastEventTime_ = goog.now();
};
goog.inherits(goog.ui.ActivityMonitor, goog.events.EventTarget);
/**
* The last event type that was detected.
* @type {string}
* @private
*/
goog.ui.ActivityMonitor.prototype.lastEventType_ = '';
/**
* The mouse x-position after the last user event.
* @type {number}
* @private
*/
goog.ui.ActivityMonitor.prototype.lastMouseX_;
/**
* The mouse y-position after the last user event.
* @type {number}
* @private
*/
goog.ui.ActivityMonitor.prototype.lastMouseY_;
/**
* The earliest time that another throttled ACTIVITY event will be dispatched
* @type {number}
* @private
*/
goog.ui.ActivityMonitor.prototype.minEventTime_ = 0;
/**
* Minimum amount of time in ms between throttled ACTIVITY events
* @type {number}
*/
goog.ui.ActivityMonitor.MIN_EVENT_SPACING = 3 * 1000;
/**
* If a user executes one of these events, s/he is considered not idle.
* @type {Array.<goog.events.EventType>}
* @private
*/
goog.ui.ActivityMonitor.userEventTypesBody_ = [
goog.events.EventType.CLICK,
goog.events.EventType.DBLCLICK,
goog.events.EventType.MOUSEDOWN,
goog.events.EventType.MOUSEMOVE,
goog.events.EventType.MOUSEUP
];
/**
* If a user executes one of these events, s/he is considered not idle.
* Note: monitoring touch events within iframe cause problems in iOS.
* @type {Array.<goog.events.EventType>}
* @private
*/
goog.ui.ActivityMonitor.userTouchEventTypesBody_ = [
goog.events.EventType.TOUCHEND,
goog.events.EventType.TOUCHMOVE,
goog.events.EventType.TOUCHSTART
];
/**
* If a user executes one of these events, s/he is considered not idle.
* @type {Array.<goog.events.EventType>}
* @private
*/
goog.ui.ActivityMonitor.userEventTypesDocuments_ =
[goog.events.EventType.KEYDOWN, goog.events.EventType.KEYUP];
/**
* Event constants for the activity monitor.
* @enum {string}
*/
goog.ui.ActivityMonitor.Event = {
/** Event fired when the user does something interactive */
ACTIVITY: 'activity'
};
/** @override */
goog.ui.ActivityMonitor.prototype.disposeInternal = function() {
goog.ui.ActivityMonitor.superClass_.disposeInternal.call(this);
this.eventHandler_.dispose();
this.eventHandler_ = null;
delete this.documents_;
};
/**
* Adds a document to those being monitored by this class.
*
* @param {Document} doc Document to monitor.
*/
goog.ui.ActivityMonitor.prototype.addDocument = function(doc) {
if (goog.array.contains(this.documents_, doc)) {
return;
}
this.documents_.push(doc);
var useCapture = !this.useBubble_;
var eventsToListenTo = goog.array.concat(
goog.ui.ActivityMonitor.userEventTypesDocuments_,
goog.ui.ActivityMonitor.userEventTypesBody_);
if (!this.isIframe_) {
// Monitoring touch events in iframe causes problems interacting with text
// fields in iOS (input text, textarea, contenteditable, select/copy/paste),
// so just ignore these events. This shouldn't matter much given that a
// touchstart event followed by touchend event produces a click event,
// which is being monitored correctly.
goog.array.extend(eventsToListenTo,
goog.ui.ActivityMonitor.userTouchEventTypesBody_);
}
this.eventHandler_.listen(doc, eventsToListenTo, this.handleEvent_,
useCapture);
};
/**
* Removes a document from those being monitored by this class.
*
* @param {Document} doc Document to monitor.
*/
goog.ui.ActivityMonitor.prototype.removeDocument = function(doc) {
if (this.isDisposed()) {
return;
}
goog.array.remove(this.documents_, doc);
var useCapture = !this.useBubble_;
var eventsToUnlistenTo = goog.array.concat(
goog.ui.ActivityMonitor.userEventTypesDocuments_,
goog.ui.ActivityMonitor.userEventTypesBody_);
if (!this.isIframe_) {
// See note above about monitoring touch events in iframe.
goog.array.extend(eventsToUnlistenTo,
goog.ui.ActivityMonitor.userTouchEventTypesBody_);
}
this.eventHandler_.unlisten(doc, eventsToUnlistenTo, this.handleEvent_,
useCapture);
};
/**
* Updates the last event time when a user action occurs.
* @param {goog.events.BrowserEvent} e Event object.
* @private
*/
goog.ui.ActivityMonitor.prototype.handleEvent_ = function(e) {
var update = false;
switch (e.type) {
case goog.events.EventType.MOUSEMOVE:
// In FF 1.5, we get spurious mouseover and mouseout events when the UI
// redraws. We only want to update the idle time if the mouse has moved.
if (typeof this.lastMouseX_ == 'number' &&
this.lastMouseX_ != e.clientX ||
typeof this.lastMouseY_ == 'number' &&
this.lastMouseY_ != e.clientY) {
update = true;
}
this.lastMouseX_ = e.clientX;
this.lastMouseY_ = e.clientY;
break;
default:
update = true;
}
if (update) {
var type = goog.asserts.assertString(e.type);
this.updateIdleTime(goog.now(), type);
}
};
/**
* Updates the last event time to be the present time, useful for non-DOM
* events that should update idle time.
*/
goog.ui.ActivityMonitor.prototype.resetTimer = function() {
this.updateIdleTime(goog.now(), 'manual');
};
/**
* Updates the idle time and fires an event if time has elapsed since
* the last update.
* @param {number} eventTime Time (in MS) of the event that cleared the idle
* timer.
* @param {string} eventType Type of the event, used only for debugging.
* @protected
*/
goog.ui.ActivityMonitor.prototype.updateIdleTime = function(
eventTime, eventType) {
// update internal state noting whether the user was idle
this.lastEventTime_ = eventTime;
this.lastEventType_ = eventType;
// dispatch event
if (eventTime > this.minEventTime_) {
this.dispatchEvent(goog.ui.ActivityMonitor.Event.ACTIVITY);
this.minEventTime_ = eventTime + goog.ui.ActivityMonitor.MIN_EVENT_SPACING;
}
};
/**
* Returns the amount of time the user has been idle.
* @param {number=} opt_now The current time can optionally be passed in for the
* computation to avoid an extra Date allocation.
* @return {number} The amount of time in ms that the user has been idle.
*/
goog.ui.ActivityMonitor.prototype.getIdleTime = function(opt_now) {
var now = opt_now || goog.now();
return now - this.lastEventTime_;
};
/**
* Returns the type of the last user event.
* @return {string} event type.
*/
goog.ui.ActivityMonitor.prototype.getLastEventType = function() {
return this.lastEventType_;
};
/**
* Returns the time of the last event
* @return {number} last event time.
*/
goog.ui.ActivityMonitor.prototype.getLastEventTime = function() {
return this.lastEventTime_;
};

View File

@@ -0,0 +1,366 @@
// Copyright 2007 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 Advanced tooltip widget implementation.
*
* @author eae@google.com (Emil A Eklund)
* @see ../demos/advancedtooltip.html
*/
goog.provide('goog.ui.AdvancedTooltip');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.math.Box');
goog.require('goog.math.Coordinate');
goog.require('goog.style');
goog.require('goog.ui.Tooltip');
goog.require('goog.userAgent');
/**
* Advanced tooltip widget with cursor tracking abilities. Works like a regular
* tooltip but can track the cursor position and direction to determine if the
* tooltip should be dismissed or remain open.
*
* @param {Element|string=} opt_el Element to display tooltip for, either
* element reference or string id.
* @param {?string=} opt_str Text message to display in tooltip.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @constructor
* @extends {goog.ui.Tooltip}
*/
goog.ui.AdvancedTooltip = function(opt_el, opt_str, opt_domHelper) {
goog.ui.Tooltip.call(this, opt_el, opt_str, opt_domHelper);
};
goog.inherits(goog.ui.AdvancedTooltip, goog.ui.Tooltip);
/**
* Whether to track the cursor and thereby close the tooltip if it moves away
* from the tooltip and keep it open if it moves towards it.
*
* @type {boolean}
* @private
*/
goog.ui.AdvancedTooltip.prototype.cursorTracking_ = false;
/**
* Delay in milliseconds before tooltips are hidden if cursor tracking is
* enabled and the cursor is moving away from the tooltip.
*
* @type {number}
* @private
*/
goog.ui.AdvancedTooltip.prototype.cursorTrackingHideDelayMs_ = 100;
/**
* Box object representing a margin around the tooltip where the cursor is
* allowed without dismissing the tooltip.
*
* @type {goog.math.Box}
* @private
*/
goog.ui.AdvancedTooltip.prototype.hotSpotPadding_;
/**
* Bounding box.
*
* @type {goog.math.Box}
* @private
*/
goog.ui.AdvancedTooltip.prototype.boundingBox_;
/**
* Anchor bounding box.
*
* @type {goog.math.Box}
* @private
*/
goog.ui.AdvancedTooltip.prototype.anchorBox_;
/**
* Whether the cursor tracking is active.
*
* @type {boolean}
* @private
*/
goog.ui.AdvancedTooltip.prototype.tracking_ = false;
/**
* Sets margin around the tooltip where the cursor is allowed without dismissing
* the tooltip.
*
* @param {goog.math.Box=} opt_box The margin around the tooltip.
*/
goog.ui.AdvancedTooltip.prototype.setHotSpotPadding = function(opt_box) {
this.hotSpotPadding_ = opt_box || null;
};
/**
* @return {goog.math.Box} box The margin around the tooltip where the cursor is
* allowed without dismissing the tooltip.
*/
goog.ui.AdvancedTooltip.prototype.getHotSpotPadding = function() {
return this.hotSpotPadding_;
};
/**
* Sets whether to track the cursor and thereby close the tooltip if it moves
* away from the tooltip and keep it open if it moves towards it.
*
* @param {boolean} b Whether to track the cursor.
*/
goog.ui.AdvancedTooltip.prototype.setCursorTracking = function(b) {
this.cursorTracking_ = b;
};
/**
* @return {boolean} Whether to track the cursor and thereby close the tooltip
* if it moves away from the tooltip and keep it open if it moves towards
* it.
*/
goog.ui.AdvancedTooltip.prototype.getCursorTracking = function() {
return this.cursorTracking_;
};
/**
* Sets delay in milliseconds before tooltips are hidden if cursor tracking is
* enabled and the cursor is moving away from the tooltip.
*
* @param {number} delay The delay in milliseconds.
*/
goog.ui.AdvancedTooltip.prototype.setCursorTrackingHideDelayMs =
function(delay) {
this.cursorTrackingHideDelayMs_ = delay;
};
/**
* @return {number} The delay in milliseconds before tooltips are hidden if
* cursor tracking is enabled and the cursor is moving away from the
* tooltip.
*/
goog.ui.AdvancedTooltip.prototype.getCursorTrackingHideDelayMs = function() {
return this.cursorTrackingHideDelayMs_;
};
/**
* Called after the popup is shown.
* @protected
* @suppress {underscore}
* @override
*/
goog.ui.AdvancedTooltip.prototype.onShow_ = function() {
goog.ui.AdvancedTooltip.superClass_.onShow_.call(this);
this.boundingBox_ = goog.style.getBounds(this.getElement()).toBox();
if (this.anchor) {
this.anchorBox_ = goog.style.getBounds(this.anchor).toBox();
}
this.tracking_ = this.cursorTracking_;
goog.events.listen(this.getDomHelper().getDocument(),
goog.events.EventType.MOUSEMOVE,
this.handleMouseMove, false, this);
};
/**
* Called after the popup is hidden.
* @protected
* @suppress {underscore}
* @override
*/
goog.ui.AdvancedTooltip.prototype.onHide_ = function() {
goog.events.unlisten(this.getDomHelper().getDocument(),
goog.events.EventType.MOUSEMOVE,
this.handleMouseMove, false, this);
this.boundingBox_ = null;
this.anchorBox_ = null;
this.tracking_ = false;
goog.ui.AdvancedTooltip.superClass_.onHide_.call(this);
};
/**
* Returns true if the mouse is in the tooltip.
* @return {boolean} True if the mouse is in the tooltip.
*/
goog.ui.AdvancedTooltip.prototype.isMouseInTooltip = function() {
return this.isCoordinateInTooltip(this.cursorPosition);
};
/**
* Checks whether the supplied coordinate is inside the tooltip, including
* padding if any.
* @param {goog.math.Coordinate} coord Coordinate being tested.
* @return {boolean} Whether the coord is in the tooltip.
* @override
*/
goog.ui.AdvancedTooltip.prototype.isCoordinateInTooltip = function(coord) {
// Check if coord is inside the bounding box of the tooltip
if (this.hotSpotPadding_) {
var offset = goog.style.getPageOffset(this.getElement());
var size = goog.style.getSize(this.getElement());
return offset.x - this.hotSpotPadding_.left <= coord.x &&
coord.x <= offset.x + size.width + this.hotSpotPadding_.right &&
offset.y - this.hotSpotPadding_.top <= coord.y &&
coord.y <= offset.y + size.height + this.hotSpotPadding_.bottom;
}
return goog.ui.AdvancedTooltip.superClass_.isCoordinateInTooltip.call(this,
coord);
};
/**
* Checks if supplied coordinate is in the tooltip, its triggering anchor, or
* a tooltip that has been triggered by a child of this tooltip.
* Called from handleMouseMove to determine if hide timer should be started,
* and from maybeHide to determine if tooltip should be hidden.
* @param {goog.math.Coordinate} coord Coordinate being tested.
* @return {boolean} Whether coordinate is in the anchor, the tooltip, or any
* tooltip whose anchor is a child of this tooltip.
* @private
*/
goog.ui.AdvancedTooltip.prototype.isCoordinateActive_ = function(coord) {
if ((this.anchorBox_ && this.anchorBox_.contains(coord)) ||
this.isCoordinateInTooltip(coord)) {
return true;
}
// Check if mouse might be in active child element.
var childTooltip = this.getChildTooltip();
return !!childTooltip && childTooltip.isCoordinateInTooltip(coord);
};
/**
* Called by timer from mouse out handler. Hides tooltip if cursor is still
* outside element and tooltip.
* @param {Element} el Anchor when hide timer was started.
* @override
*/
goog.ui.AdvancedTooltip.prototype.maybeHide = function(el) {
this.hideTimer = undefined;
if (el == this.anchor) {
// Check if cursor is inside the bounding box of the tooltip or the element
// that triggered it, or if tooltip is active (possibly due to receiving
// the focus), or if there is a nested tooltip being shown.
if (!this.isCoordinateActive_(this.cursorPosition) &&
!this.getActiveElement() &&
!this.hasActiveChild()) {
// Under certain circumstances gecko fires ghost mouse events with the
// coordinates 0, 0 regardless of the cursors position.
if (goog.userAgent.GECKO && this.cursorPosition.x == 0 &&
this.cursorPosition.y == 0) {
return;
}
this.setVisible(false);
}
}
};
/**
* Handler for mouse move events.
*
* @param {goog.events.BrowserEvent} event Event object.
* @protected
* @override
*/
goog.ui.AdvancedTooltip.prototype.handleMouseMove = function(event) {
var startTimer = this.isVisible();
if (this.boundingBox_) {
var scroll = this.getDomHelper().getDocumentScroll();
var c = new goog.math.Coordinate(event.clientX + scroll.x,
event.clientY + scroll.y);
if (this.isCoordinateActive_(c)) {
startTimer = false;
} else if (this.tracking_) {
var prevDist = goog.math.Box.distance(this.boundingBox_,
this.cursorPosition);
var currDist = goog.math.Box.distance(this.boundingBox_, c);
startTimer = currDist >= prevDist;
}
}
if (startTimer) {
this.startHideTimer();
// Even though the mouse coordinate is not on the tooltip (or nested child),
// they may have an active element because of a focus event. Don't let
// that prevent us from taking down the tooltip(s) on this mouse move.
this.setActiveElement(null);
var childTooltip = this.getChildTooltip();
if (childTooltip) {
childTooltip.setActiveElement(null);
}
} else if (this.getState() == goog.ui.Tooltip.State.WAITING_TO_HIDE) {
this.clearHideTimer();
}
goog.ui.AdvancedTooltip.superClass_.handleMouseMove.call(this, event);
};
/**
* Handler for mouse over events for the tooltip element.
*
* @param {goog.events.BrowserEvent} event Event object.
* @protected
* @override
*/
goog.ui.AdvancedTooltip.prototype.handleTooltipMouseOver = function(event) {
if (this.getActiveElement() != this.getElement()) {
this.tracking_ = false;
this.setActiveElement(this.getElement());
}
};
/**
* Override hide delay with cursor tracking hide delay while tracking.
* @return {number} Hide delay to use.
* @override
*/
goog.ui.AdvancedTooltip.prototype.getHideDelayMs = function() {
return this.tracking_ ? this.cursorTrackingHideDelayMs_ :
goog.base(this, 'getHideDelayMs');
};
/**
* Forces the recalculation of the hotspot on the next mouse over event.
* @deprecated Not ever necessary to call this function. Hot spot is calculated
* as neccessary.
*/
goog.ui.AdvancedTooltip.prototype.resetHotSpot = goog.nullFunction;

View File

@@ -0,0 +1,199 @@
// Copyright 2006 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 Animated zippy widget implementation.
*
* @author eae@google.com (Emil A Eklund)
* @see ../demos/zippy.html
*/
goog.provide('goog.ui.AnimatedZippy');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.fx.Animation');
goog.require('goog.fx.Transition');
goog.require('goog.fx.easing');
goog.require('goog.ui.Zippy');
goog.require('goog.ui.ZippyEvent');
/**
* Zippy widget. Expandable/collapsible container, clicking the header toggles
* the visibility of the content.
*
* @param {Element|string|null} header Header element, either element
* reference, string id or null if no header exists.
* @param {Element|string} content Content element, either element reference or
* string id.
* @param {boolean=} opt_expanded Initial expanded/visibility state. Defaults to
* false.
* @param {goog.dom.DomHelper=} opt_domHelper An optional DOM helper.
* @constructor
* @extends {goog.ui.Zippy}
*/
goog.ui.AnimatedZippy = function(header, content, opt_expanded, opt_domHelper) {
var domHelper = opt_domHelper || goog.dom.getDomHelper();
// Create wrapper element and move content into it.
var elWrapper = domHelper.createDom('div', {'style': 'overflow:hidden'});
var elContent = domHelper.getElement(content);
elContent.parentNode.replaceChild(elWrapper, elContent);
elWrapper.appendChild(elContent);
/**
* Content wrapper, used for animation.
* @type {Element}
* @private
*/
this.elWrapper_ = elWrapper;
/**
* Reference to animation or null if animation is not active.
* @type {goog.fx.Animation}
* @private
*/
this.anim_ = null;
// Call constructor of super class.
goog.ui.Zippy.call(this, header, elContent, opt_expanded,
undefined, domHelper);
// Set initial state.
// NOTE: Set the class names as well otherwise animated zippys
// start with empty class names.
var expanded = this.isExpanded();
this.elWrapper_.style.display = expanded ? '' : 'none';
this.updateHeaderClassName(expanded);
};
goog.inherits(goog.ui.AnimatedZippy, goog.ui.Zippy);
/**
* Duration of expand/collapse animation, in milliseconds.
* @type {number}
*/
goog.ui.AnimatedZippy.prototype.animationDuration = 500;
/**
* Acceleration function for expand/collapse animation.
* @type {!Function}
*/
goog.ui.AnimatedZippy.prototype.animationAcceleration = goog.fx.easing.easeOut;
/**
* @return {boolean} Whether the zippy is in the process of being expanded or
* collapsed.
*/
goog.ui.AnimatedZippy.prototype.isBusy = function() {
return this.anim_ != null;
};
/**
* Sets expanded state.
*
* @param {boolean} expanded Expanded/visibility state.
* @override
*/
goog.ui.AnimatedZippy.prototype.setExpanded = function(expanded) {
if (this.isExpanded() == expanded && !this.anim_) {
return;
}
// Reset display property of wrapper to allow content element to be
// measured.
if (this.elWrapper_.style.display == 'none') {
this.elWrapper_.style.display = '';
}
// Measure content element.
var h = this.getContentElement().offsetHeight;
// Stop active animation (if any) and determine starting height.
var startH = 0;
if (this.anim_) {
expanded = this.isExpanded();
goog.events.removeAll(this.anim_);
this.anim_.stop(false);
var marginTop = parseInt(this.getContentElement().style.marginTop, 10);
startH = h - Math.abs(marginTop);
} else {
startH = expanded ? 0 : h;
}
// Updates header class name after the animation has been stopped.
this.updateHeaderClassName(expanded);
// Set up expand/collapse animation.
this.anim_ = new goog.fx.Animation([0, startH],
[0, expanded ? h : 0],
this.animationDuration,
this.animationAcceleration);
var events = [goog.fx.Transition.EventType.BEGIN,
goog.fx.Animation.EventType.ANIMATE,
goog.fx.Transition.EventType.END];
goog.events.listen(this.anim_, events, this.onAnimate_, false, this);
goog.events.listen(this.anim_,
goog.fx.Transition.EventType.END,
goog.bind(this.onAnimationCompleted_, this, expanded));
// Start animation.
this.anim_.play(false);
};
/**
* Called during animation
*
* @param {goog.events.Event} e The event.
* @private
*/
goog.ui.AnimatedZippy.prototype.onAnimate_ = function(e) {
var contentElement = this.getContentElement();
var h = contentElement.offsetHeight;
contentElement.style.marginTop = (e.y - h) + 'px';
};
/**
* Called once the expand/collapse animation has completed.
*
* @param {boolean} expanded Expanded/visibility state.
* @private
*/
goog.ui.AnimatedZippy.prototype.onAnimationCompleted_ = function(expanded) {
// Fix wrong end position if the content has changed during the animation.
if (expanded) {
this.getContentElement().style.marginTop = '0';
}
goog.events.removeAll(this.anim_);
this.setExpandedInternal(expanded);
this.anim_ = null;
if (!expanded) {
this.elWrapper_.style.display = 'none';
}
// Fire toggle event.
this.dispatchEvent(new goog.ui.ZippyEvent(goog.ui.Zippy.Events.TOGGLE,
this, expanded));
};

View File

@@ -0,0 +1,473 @@
// Copyright 2006 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 Definition of the AttachableMenu class.
*
*/
goog.provide('goog.ui.AttachableMenu');
goog.require('goog.a11y.aria');
goog.require('goog.a11y.aria.State');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.classes');
goog.require('goog.events.Event');
goog.require('goog.events.KeyCodes');
goog.require('goog.string');
goog.require('goog.style');
goog.require('goog.ui.ItemEvent');
goog.require('goog.ui.MenuBase');
goog.require('goog.ui.PopupBase');
goog.require('goog.userAgent');
/**
* An implementation of a menu that can attach itself to DOM element that
* are annotated appropriately.
*
* The following attributes are used by the AttachableMenu
*
* menu-item - Should be set on DOM elements that function as items in the
* menu that can be selected.
* classNameSelected - A class that will be added to the element's class names
* when the item is selected via keyboard or mouse.
*
* @param {Element=} opt_element A DOM element for the popup.
* @constructor
* @extends {goog.ui.MenuBase}
* @deprecated Use goog.ui.PopupMenu.
*/
goog.ui.AttachableMenu = function(opt_element) {
goog.ui.MenuBase.call(this, opt_element);
};
goog.inherits(goog.ui.AttachableMenu, goog.ui.MenuBase);
/**
* The currently selected element (mouse was moved over it or keyboard arrows)
* @type {Element}
* @private
*/
goog.ui.AttachableMenu.prototype.selectedElement_ = null;
/**
* Class name to append to a menu item's class when it's selected
* @type {string}
* @private
*/
goog.ui.AttachableMenu.prototype.itemClassName_ = 'menu-item';
/**
* Class name to append to a menu item's class when it's selected
* @type {string}
* @private
*/
goog.ui.AttachableMenu.prototype.selectedItemClassName_ = 'menu-item-selected';
/**
* Keep track of when the last key was pressed so that a keydown-scroll doesn't
* trigger a mouseover event
* @type {number}
* @private
*/
goog.ui.AttachableMenu.prototype.lastKeyDown_ = goog.now();
/** @override */
goog.ui.AttachableMenu.prototype.disposeInternal = function() {
goog.ui.AttachableMenu.superClass_.disposeInternal.call(this);
this.selectedElement_ = null;
};
/**
* Sets the class name to use for menu items
*
* @return {string} The class name to use for items.
*/
goog.ui.AttachableMenu.prototype.getItemClassName = function() {
return this.itemClassName_;
};
/**
* Sets the class name to use for menu items
*
* @param {string} name The class name to use for items.
*/
goog.ui.AttachableMenu.prototype.setItemClassName = function(name) {
this.itemClassName_ = name;
};
/**
* Sets the class name to use for selected menu items
* todo(user) - reevaluate if we can simulate pseudo classes in IE
*
* @return {string} The class name to use for selected items.
*/
goog.ui.AttachableMenu.prototype.getSelectedItemClassName = function() {
return this.selectedItemClassName_;
};
/**
* Sets the class name to use for selected menu items
* todo(user) - reevaluate if we can simulate pseudo classes in IE
*
* @param {string} name The class name to use for selected items.
*/
goog.ui.AttachableMenu.prototype.setSelectedItemClassName = function(name) {
this.selectedItemClassName_ = name;
};
/**
* Returns the selected item
*
* @return {Element} The item selected or null if no item is selected.
* @override
*/
goog.ui.AttachableMenu.prototype.getSelectedItem = function() {
return this.selectedElement_;
};
/** @override */
goog.ui.AttachableMenu.prototype.setSelectedItem = function(obj) {
var elt = /** @type {Element} */ (obj);
if (this.selectedElement_) {
goog.dom.classes.remove(this.selectedElement_, this.selectedItemClassName_);
}
this.selectedElement_ = elt;
var el = this.getElement();
goog.asserts.assert(el, 'The attachable menu DOM element cannot be null.');
if (this.selectedElement_) {
goog.dom.classes.add(this.selectedElement_, this.selectedItemClassName_);
if (elt.id) {
// Update activedescendant to reflect the new selection. ARIA roles for
// menu and menuitem can be set statically (thru Soy templates, for
// example) whereas this needs to be updated as the selection changes.
goog.a11y.aria.setState(el, goog.a11y.aria.State.ACTIVEDESCENDANT,
elt.id);
}
var top = this.selectedElement_.offsetTop;
var height = this.selectedElement_.offsetHeight;
var scrollTop = el.scrollTop;
var scrollHeight = el.offsetHeight;
// If the menu is scrollable this scrolls the selected item into view
// (this has no effect when the menu doesn't scroll)
if (top < scrollTop) {
el.scrollTop = top;
} else if (top + height > scrollTop + scrollHeight) {
el.scrollTop = top + height - scrollHeight;
}
} else {
// Clear off activedescendant to reflect no selection.
goog.a11y.aria.setState(el, goog.a11y.aria.State.ACTIVEDESCENDANT, '');
}
};
/** @override */
goog.ui.AttachableMenu.prototype.showPopupElement = function() {
// The scroll position cannot be set for hidden (display: none) elements in
// gecko browsers.
var el = /** @type {Element} */ (this.getElement());
goog.style.setElementShown(el, true);
el.scrollTop = 0;
el.style.visibility = 'visible';
};
/**
* Called after the menu is shown.
* @protected
* @suppress {underscore}
* @override
*/
goog.ui.AttachableMenu.prototype.onShow_ = function() {
goog.ui.AttachableMenu.superClass_.onShow_.call(this);
// In IE, focusing the menu causes weird scrolling to happen. Focusing the
// first child makes the scroll behavior better, and the key handling still
// works. In FF, focusing the first child causes us to lose key events, so we
// still focus the menu.
var el = this.getElement();
goog.userAgent.IE ? el.firstChild.focus() :
el.focus();
};
/**
* Returns the next or previous item. Used for up/down arrows.
*
* @param {boolean} prev True to go to the previous element instead of next.
* @return {Element} The next or previous element.
* @protected
*/
goog.ui.AttachableMenu.prototype.getNextPrevItem = function(prev) {
// first find the index of the next element
var elements = this.getElement().getElementsByTagName('*');
var elementCount = elements.length;
var index;
// if there is a selected element, find its index and then inc/dec by one
if (this.selectedElement_) {
for (var i = 0; i < elementCount; i++) {
if (elements[i] == this.selectedElement_) {
index = prev ? i - 1 : i + 1;
break;
}
}
}
// if no selected element, start from beginning or end
if (!goog.isDef(index)) {
index = prev ? elementCount - 1 : 0;
}
// iterate forward or backwards through the elements finding the next
// menu item
for (var i = 0; i < elementCount; i++) {
var multiplier = prev ? -1 : 1;
var nextIndex = index + (multiplier * i) % elementCount;
// if overflowed/underflowed, wrap around
if (nextIndex < 0) {
nextIndex += elementCount;
} else if (nextIndex >= elementCount) {
nextIndex -= elementCount;
}
if (this.isMenuItem_(elements[nextIndex])) {
return elements[nextIndex];
}
}
return null;
};
/**
* Mouse over handler for the menu.
* @param {goog.events.Event} e The event object.
* @protected
* @override
*/
goog.ui.AttachableMenu.prototype.onMouseOver = function(e) {
var eltItem = this.getAncestorMenuItem_(/** @type {Element} */ (e.target));
if (eltItem == null) {
return;
}
// Stop the keydown triggering a mouseover in FF.
if (goog.now() - this.lastKeyDown_ > goog.ui.PopupBase.DEBOUNCE_DELAY_MS) {
this.setSelectedItem(eltItem);
}
};
/**
* Mouse out handler for the menu.
* @param {goog.events.Event} e The event object.
* @protected
* @override
*/
goog.ui.AttachableMenu.prototype.onMouseOut = function(e) {
var eltItem = this.getAncestorMenuItem_(/** @type {Element} */ (e.target));
if (eltItem == null) {
return;
}
// Stop the keydown triggering a mouseout in FF.
if (goog.now() - this.lastKeyDown_ > goog.ui.PopupBase.DEBOUNCE_DELAY_MS) {
this.setSelectedItem(null);
}
};
/**
* Mouse down handler for the menu. Prevents default to avoid text selection.
* @param {!goog.events.Event} e The event object.
* @protected
* @override
*/
goog.ui.AttachableMenu.prototype.onMouseDown = goog.events.Event.preventDefault;
/**
* Mouse up handler for the menu.
* @param {goog.events.Event} e The event object.
* @protected
* @override
*/
goog.ui.AttachableMenu.prototype.onMouseUp = function(e) {
var eltItem = this.getAncestorMenuItem_(/** @type {Element} */ (e.target));
if (eltItem == null) {
return;
}
this.setVisible(false);
this.onItemSelected_(eltItem);
};
/**
* Key down handler for the menu.
* @param {goog.events.KeyEvent} e The event object.
* @protected
* @override
*/
goog.ui.AttachableMenu.prototype.onKeyDown = function(e) {
switch (e.keyCode) {
case goog.events.KeyCodes.DOWN:
this.setSelectedItem(this.getNextPrevItem(false));
this.lastKeyDown_ = goog.now();
break;
case goog.events.KeyCodes.UP:
this.setSelectedItem(this.getNextPrevItem(true));
this.lastKeyDown_ = goog.now();
break;
case goog.events.KeyCodes.ENTER:
if (this.selectedElement_) {
this.onItemSelected_();
this.setVisible(false);
}
break;
case goog.events.KeyCodes.ESC:
this.setVisible(false);
break;
default:
if (e.charCode) {
var charStr = String.fromCharCode(e.charCode);
this.selectByName_(charStr, 1, true);
}
break;
}
// Prevent the browser's default keydown behaviour when the menu is open,
// e.g. keyboard scrolling.
e.preventDefault();
// Stop propagation to prevent application level keyboard shortcuts from
// firing.
e.stopPropagation();
this.dispatchEvent(e);
};
/**
* Find an item that has the given prefix and select it.
*
* @param {string} prefix The entered prefix, so far.
* @param {number=} opt_direction 1 to search forward from the selection
* (default), -1 to search backward (e.g. to go to the previous match).
* @param {boolean=} opt_skip True if should skip the current selection,
* unless no other item has the given prefix.
* @private
*/
goog.ui.AttachableMenu.prototype.selectByName_ =
function(prefix, opt_direction, opt_skip) {
var elements = this.getElement().getElementsByTagName('*');
var elementCount = elements.length;
var index;
if (elementCount == 0) {
return;
}
if (!this.selectedElement_ ||
(index = goog.array.indexOf(elements, this.selectedElement_)) == -1) {
// no selection or selection isn't known => start at the beginning
index = 0;
}
var start = index;
var re = new RegExp('^' + goog.string.regExpEscape(prefix), 'i');
var skip = opt_skip && this.selectedElement_;
var dir = opt_direction || 1;
do {
if (elements[index] != skip && this.isMenuItem_(elements[index])) {
var name = goog.dom.getTextContent(elements[index]);
if (name.match(re)) {
break;
}
}
index += dir;
if (index == elementCount) {
index = 0;
} else if (index < 0) {
index = elementCount - 1;
}
} while (index != start);
if (this.selectedElement_ != elements[index]) {
this.setSelectedItem(elements[index]);
}
};
/**
* Dispatch an ITEM_ACTION event when an item is selected
* @param {Object=} opt_item Item selected.
* @private
*/
goog.ui.AttachableMenu.prototype.onItemSelected_ = function(opt_item) {
this.dispatchEvent(new goog.ui.ItemEvent(goog.ui.MenuBase.Events.ITEM_ACTION,
this, opt_item || this.selectedElement_));
};
/**
* Returns whether the specified element is a menu item.
* @param {Element|undefined} elt The element to find a menu item ancestor of.
* @return {boolean} Whether the specified element is a menu item.
* @private
*/
goog.ui.AttachableMenu.prototype.isMenuItem_ = function(elt) {
return !!elt && goog.dom.classes.has(elt, this.itemClassName_);
};
/**
* Returns the menu-item scoping the specified element, or null if there is
* none.
* @param {Element|undefined} elt The element to find a menu item ancestor of.
* @return {Element} The menu-item scoping the specified element, or null if
* there is none.
* @private
*/
goog.ui.AttachableMenu.prototype.getAncestorMenuItem_ = function(elt) {
if (elt) {
var ownerDocumentBody = goog.dom.getOwnerDocument(elt).body;
while (elt != null && elt != ownerDocumentBody) {
if (this.isMenuItem_(elt)) {
return elt;
}
elt = /** @type {Element} */ (elt.parentNode);
}
}
return null;
};

View File

@@ -0,0 +1,178 @@
// Copyright 2007 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 Component for an input field with bidi direction automatic
* detection. The input element directionality is automatically set according
* to the contents (value) of the element.
*
* @see ../demos/bidiinput.html
*/
goog.provide('goog.ui.BidiInput');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.InputHandler');
goog.require('goog.i18n.bidi');
goog.require('goog.ui.Component');
/**
* Default implementation of BidiInput.
*
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @constructor
* @extends {goog.ui.Component}
*/
goog.ui.BidiInput = function(opt_domHelper) {
goog.ui.Component.call(this, opt_domHelper);
};
goog.inherits(goog.ui.BidiInput, goog.ui.Component);
/**
* The input handler that provides the input event.
* @type {goog.events.InputHandler?}
* @private
*/
goog.ui.BidiInput.prototype.inputHandler_ = null;
/**
* Decorates the given HTML element as a BidiInput. The HTML element can be an
* input element with type='text', a textarea element, or any contenteditable.
* Overrides {@link goog.ui.Component#decorateInternal}. Considered protected.
* @param {Element} element Element to decorate.
* @protected
* @override
*/
goog.ui.BidiInput.prototype.decorateInternal = function(element) {
goog.ui.BidiInput.superClass_.decorateInternal.call(this, element);
this.init_();
};
/**
* Creates the element for the text input.
* @protected
* @override
*/
goog.ui.BidiInput.prototype.createDom = function() {
this.setElementInternal(
this.getDomHelper().createDom('input', {'type': 'text'}));
this.init_();
};
/**
* Initializes the events and initial text direction.
* Called from either decorate or createDom, after the input field has
* been created.
* @private
*/
goog.ui.BidiInput.prototype.init_ = function() {
// Set initial direction by current text
this.setDirection_();
// Listen to value change events
this.inputHandler_ = new goog.events.InputHandler(this.getElement());
goog.events.listen(this.inputHandler_,
goog.events.InputHandler.EventType.INPUT,
this.setDirection_, false, this);
};
/**
* Set the direction of the input element based on the current value. If the
* value does not have any strongly directional characters, remove the dir
* attribute so that the direction is inherited instead.
* This method is called when the user changes the input element value, or
* when a program changes the value using
* {@link goog.ui.BidiInput#setValue}
* @private
*/
goog.ui.BidiInput.prototype.setDirection_ = function() {
var element = this.getElement();
var text = this.getValue();
switch (goog.i18n.bidi.estimateDirection(text)) {
case (goog.i18n.bidi.Dir.LTR):
element.dir = 'ltr';
break;
case (goog.i18n.bidi.Dir.RTL):
element.dir = 'rtl';
break;
default:
// Default for no direction, inherit from document.
element.removeAttribute('dir');
}
};
/**
* Returns the direction of the input element.
* @return {?string} Return 'rtl' for right-to-left text,
* 'ltr' for left-to-right text, or null if the value itself is not
* enough to determine directionality (e.g. an empty value), and the
* direction is inherited from a parent element (typically the body
* element).
*/
goog.ui.BidiInput.prototype.getDirection = function() {
var dir = this.getElement().dir;
if (dir == '') {
dir = null;
}
return dir;
};
/**
* Sets the value of the underlying input field, and sets the direction
* according to the given value.
* @param {string} value The Value to set in the underlying input field.
*/
goog.ui.BidiInput.prototype.setValue = function(value) {
var element = this.getElement();
if (goog.isDefAndNotNull(element.value)) {
element.value = value;
} else {
goog.dom.setTextContent(element, value);
}
this.setDirection_();
};
/**
* Returns the value of the underlying input field.
* @return {string} Value of the underlying input field.
*/
goog.ui.BidiInput.prototype.getValue = function() {
var element = this.getElement();
return goog.isDefAndNotNull(element.value) ? element.value :
goog.dom.getRawTextContent(element);
};
/** @override */
goog.ui.BidiInput.prototype.disposeInternal = function() {
if (this.inputHandler_) {
goog.events.removeAll(this.inputHandler_);
this.inputHandler_.dispose();
this.inputHandler_ = null;
goog.ui.BidiInput.superClass_.disposeInternal.call(this);
}
};

View File

@@ -0,0 +1,472 @@
// Copyright 2007 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 Definition of the Bubble class.
*
*
* @see ../demos/bubble.html
*
* TODO: support decoration and addChild
*/
goog.provide('goog.ui.Bubble');
goog.require('goog.Timer');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.math.Box');
goog.require('goog.positioning');
goog.require('goog.positioning.AbsolutePosition');
goog.require('goog.positioning.AnchoredPosition');
goog.require('goog.positioning.Corner');
goog.require('goog.positioning.CornerBit');
goog.require('goog.style');
goog.require('goog.ui.Component');
goog.require('goog.ui.Popup');
/**
* The Bubble provides a general purpose bubble implementation that can be
* anchored to a particular element and displayed for a period of time.
*
* @param {string|Element} message HTML string or an element to display inside
* the bubble.
* @param {Object=} opt_config The configuration
* for the bubble. If not specified, the default configuration will be
* used. {@see goog.ui.Bubble.defaultConfig}.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @constructor
* @extends {goog.ui.Component}
*/
goog.ui.Bubble = function(message, opt_config, opt_domHelper) {
goog.ui.Component.call(this, opt_domHelper);
/**
* The HTML string or element to display inside the bubble.
*
* @type {string|Element}
* @private
*/
this.message_ = message;
/**
* The Popup element used to position and display the bubble.
*
* @type {goog.ui.Popup}
* @private
*/
this.popup_ = new goog.ui.Popup();
/**
* Configuration map that contains bubble's UI elements.
*
* @type {Object}
* @private
*/
this.config_ = opt_config || goog.ui.Bubble.defaultConfig;
/**
* Id of the close button for this bubble.
*
* @type {string}
* @private
*/
this.closeButtonId_ = this.makeId('cb');
/**
* Id of the div for the embedded element.
*
* @type {string}
* @private
*/
this.messageId_ = this.makeId('mi');
};
goog.inherits(goog.ui.Bubble, goog.ui.Component);
/**
* In milliseconds, timeout after which the button auto-hides. Null means
* infinite.
* @type {?number}
* @private
*/
goog.ui.Bubble.prototype.timeout_ = null;
/**
* Key returned by the bubble timer.
* @type {number}
* @private
*/
goog.ui.Bubble.prototype.timerId_ = 0;
/**
* Key returned by the listen function for the close button.
* @type {goog.events.Key}
* @private
*/
goog.ui.Bubble.prototype.listener_ = null;
/**
* Key returned by the listen function for the close button.
* @type {Element}
* @private
*/
goog.ui.Bubble.prototype.anchor_ = null;
/** @override */
goog.ui.Bubble.prototype.createDom = function() {
goog.ui.Bubble.superClass_.createDom.call(this);
var element = this.getElement();
element.style.position = 'absolute';
element.style.visibility = 'hidden';
this.popup_.setElement(element);
};
/**
* Attaches the bubble to an anchor element. Computes the positioning and
* orientation of the bubble.
*
* @param {Element} anchorElement The element to which we are attaching.
*/
goog.ui.Bubble.prototype.attach = function(anchorElement) {
this.setAnchoredPosition_(
anchorElement, this.computePinnedCorner_(anchorElement));
};
/**
* Sets the corner of the bubble to used in the positioning algorithm.
*
* @param {goog.positioning.Corner} corner The bubble corner used for
* positioning constants.
*/
goog.ui.Bubble.prototype.setPinnedCorner = function(corner) {
this.popup_.setPinnedCorner(corner);
};
/**
* Sets the position of the bubble. Pass null for corner in AnchoredPosition
* for corner to be computed automatically.
*
* @param {goog.positioning.AbstractPosition} position The position of the
* bubble.
*/
goog.ui.Bubble.prototype.setPosition = function(position) {
if (position instanceof goog.positioning.AbsolutePosition) {
this.popup_.setPosition(position);
} else if (position instanceof goog.positioning.AnchoredPosition) {
this.setAnchoredPosition_(position.element, position.corner);
} else {
throw Error('Bubble only supports absolute and anchored positions!');
}
};
/**
* Sets the timeout after which bubble hides itself.
*
* @param {number} timeout Timeout of the bubble.
*/
goog.ui.Bubble.prototype.setTimeout = function(timeout) {
this.timeout_ = timeout;
};
/**
* Sets whether the bubble should be automatically hidden whenever user clicks
* outside the bubble element.
*
* @param {boolean} autoHide Whether to hide if user clicks outside the bubble.
*/
goog.ui.Bubble.prototype.setAutoHide = function(autoHide) {
this.popup_.setAutoHide(autoHide);
};
/**
* Sets whether the bubble should be visible.
*
* @param {boolean} visible Desired visibility state.
*/
goog.ui.Bubble.prototype.setVisible = function(visible) {
if (visible && !this.popup_.isVisible()) {
this.configureElement_();
}
this.popup_.setVisible(visible);
if (!this.popup_.isVisible()) {
this.unconfigureElement_();
}
};
/**
* @return {boolean} Whether the bubble is visible.
*/
goog.ui.Bubble.prototype.isVisible = function() {
return this.popup_.isVisible();
};
/** @override */
goog.ui.Bubble.prototype.disposeInternal = function() {
this.unconfigureElement_();
this.popup_.dispose();
this.popup_ = null;
goog.ui.Bubble.superClass_.disposeInternal.call(this);
};
/**
* Creates element's contents and configures all timers. This is called on
* setVisible(true).
* @private
*/
goog.ui.Bubble.prototype.configureElement_ = function() {
if (!this.isInDocument()) {
throw Error('You must render the bubble before showing it!');
}
var element = this.getElement();
var corner = this.popup_.getPinnedCorner();
element.innerHTML = this.computeHtmlForCorner_(corner);
if (typeof this.message_ == 'object') {
var messageDiv = this.getDomHelper().getElement(this.messageId_);
this.getDomHelper().appendChild(messageDiv, this.message_);
}
var closeButton = this.getDomHelper().getElement(this.closeButtonId_);
this.listener_ = goog.events.listen(closeButton,
goog.events.EventType.CLICK, this.hideBubble_, false, this);
if (this.timeout_) {
this.timerId_ = goog.Timer.callOnce(this.hideBubble_, this.timeout_, this);
}
};
/**
* Gets rid of the element's contents and all assoicated timers and listeners.
* This is called on dispose as well as on setVisible(false).
* @private
*/
goog.ui.Bubble.prototype.unconfigureElement_ = function() {
if (this.listener_) {
goog.events.unlistenByKey(this.listener_);
this.listener_ = null;
}
if (this.timerId_) {
goog.Timer.clear(this.timerId_);
this.timerId = null;
}
var element = this.getElement();
if (element) {
this.getDomHelper().removeChildren(element);
element.innerHTML = '';
}
};
/**
* Computes bubble position based on anchored element.
*
* @param {Element} anchorElement The element to which we are attaching.
* @param {goog.positioning.Corner} corner The bubble corner used for
* positioning.
* @private
*/
goog.ui.Bubble.prototype.setAnchoredPosition_ = function(anchorElement,
corner) {
this.popup_.setPinnedCorner(corner);
var margin = this.createMarginForCorner_(corner);
this.popup_.setMargin(margin);
var anchorCorner = goog.positioning.flipCorner(corner);
this.popup_.setPosition(new goog.positioning.AnchoredPosition(
anchorElement, anchorCorner));
};
/**
* Hides the bubble. This is called asynchronously by timer of event processor
* for the mouse click on the close button.
* @private
*/
goog.ui.Bubble.prototype.hideBubble_ = function() {
this.setVisible(false);
};
/**
* Returns an AnchoredPosition that will position the bubble optimally
* given the position of the anchor element and the size of the viewport.
*
* @param {Element} anchorElement The element to which the bubble is attached.
* @return {goog.ui.Popup.AnchoredPosition} The AnchoredPosition to give to
* {@link #setPosition}.
*/
goog.ui.Bubble.prototype.getComputedAnchoredPosition = function(anchorElement) {
return new goog.ui.Popup.AnchoredPosition(
anchorElement, this.computePinnedCorner_(anchorElement));
};
/**
* Computes the pinned corner for the bubble.
*
* @param {Element} anchorElement The element to which the button is attached.
* @return {goog.positioning.Corner} The pinned corner.
* @private
*/
goog.ui.Bubble.prototype.computePinnedCorner_ = function(anchorElement) {
var doc = this.getDomHelper().getOwnerDocument(anchorElement);
var viewportElement = goog.style.getClientViewportElement(doc);
var viewportWidth = viewportElement.offsetWidth;
var viewportHeight = viewportElement.offsetHeight;
var anchorElementOffset = goog.style.getPageOffset(anchorElement);
var anchorElementSize = goog.style.getSize(anchorElement);
var anchorType = 0;
// right margin or left?
if (viewportWidth - anchorElementOffset.x - anchorElementSize.width >
anchorElementOffset.x) {
anchorType += 1;
}
// attaches to the top or to the bottom?
if (viewportHeight - anchorElementOffset.y - anchorElementSize.height >
anchorElementOffset.y) {
anchorType += 2;
}
return goog.ui.Bubble.corners_[anchorType];
};
/**
* Computes the right offset for a given bubble corner
* and creates a margin element for it. This is done to have the
* button anchor element on its frame rather than on the corner.
*
* @param {goog.positioning.Corner} corner The corner.
* @return {goog.math.Box} the computed margin. Only left or right fields are
* non-zero, but they may be negative.
* @private
*/
goog.ui.Bubble.prototype.createMarginForCorner_ = function(corner) {
var margin = new goog.math.Box(0, 0, 0, 0);
if (corner & goog.positioning.CornerBit.RIGHT) {
margin.right -= this.config_.marginShift;
} else {
margin.left -= this.config_.marginShift;
}
return margin;
};
/**
* Computes the HTML string for a given bubble orientation.
*
* @param {goog.positioning.Corner} corner The corner.
* @return {string} The HTML string to place inside the bubble's popup.
* @private
*/
goog.ui.Bubble.prototype.computeHtmlForCorner_ = function(corner) {
var bubbleTopClass;
var bubbleBottomClass;
switch (corner) {
case goog.positioning.Corner.TOP_LEFT:
bubbleTopClass = this.config_.cssBubbleTopLeftAnchor;
bubbleBottomClass = this.config_.cssBubbleBottomNoAnchor;
break;
case goog.positioning.Corner.TOP_RIGHT:
bubbleTopClass = this.config_.cssBubbleTopRightAnchor;
bubbleBottomClass = this.config_.cssBubbleBottomNoAnchor;
break;
case goog.positioning.Corner.BOTTOM_LEFT:
bubbleTopClass = this.config_.cssBubbleTopNoAnchor;
bubbleBottomClass = this.config_.cssBubbleBottomLeftAnchor;
break;
case goog.positioning.Corner.BOTTOM_RIGHT:
bubbleTopClass = this.config_.cssBubbleTopNoAnchor;
bubbleBottomClass = this.config_.cssBubbleBottomRightAnchor;
break;
default:
throw Error('This corner type is not supported by bubble!');
}
var message = null;
if (typeof this.message_ == 'object') {
message = '<div id="' + this.messageId_ + '">';
} else {
message = this.message_;
}
var html =
'<table border=0 cellspacing=0 cellpadding=0 style="z-index:1"' +
' width=' + this.config_.bubbleWidth + '>' +
'<tr><td colspan=4 class="' + bubbleTopClass + '">' +
'<tr>' +
'<td class="' + this.config_.cssBubbleLeft + '">' +
'<td class="' + this.config_.cssBubbleFont + '"' +
' style="padding:0 4px;background:white">' + message +
'<td id="' + this.closeButtonId_ + '"' +
' class="' + this.config_.cssCloseButton + '"/>' +
'<td class="' + this.config_.cssBubbleRight + '">' +
'<tr>' +
'<td colspan=4 class="' + bubbleBottomClass + '">' +
'</table>';
return html;
};
/**
* A default configuration for the bubble.
*
* @type {Object}
*/
goog.ui.Bubble.defaultConfig = {
bubbleWidth: 147,
marginShift: 60,
cssBubbleFont: goog.getCssName('goog-bubble-font'),
cssCloseButton: goog.getCssName('goog-bubble-close-button'),
cssBubbleTopRightAnchor: goog.getCssName('goog-bubble-top-right-anchor'),
cssBubbleTopLeftAnchor: goog.getCssName('goog-bubble-top-left-anchor'),
cssBubbleTopNoAnchor: goog.getCssName('goog-bubble-top-no-anchor'),
cssBubbleBottomRightAnchor:
goog.getCssName('goog-bubble-bottom-right-anchor'),
cssBubbleBottomLeftAnchor: goog.getCssName('goog-bubble-bottom-left-anchor'),
cssBubbleBottomNoAnchor: goog.getCssName('goog-bubble-bottom-no-anchor'),
cssBubbleLeft: goog.getCssName('goog-bubble-left'),
cssBubbleRight: goog.getCssName('goog-bubble-right')
};
/**
* An auxiliary array optimizing the corner computation.
*
* @type {Array.<goog.positioning.Corner>}
* @private
*/
goog.ui.Bubble.corners_ = [
goog.positioning.Corner.BOTTOM_RIGHT,
goog.positioning.Corner.BOTTOM_LEFT,
goog.positioning.Corner.TOP_RIGHT,
goog.positioning.Corner.TOP_LEFT
];

View File

@@ -0,0 +1,213 @@
// Copyright 2007 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 button control. This implementation extends {@link
* goog.ui.Control}.
*
* @author attila@google.com (Attila Bodis)
* @see ../demos/button.html
*/
goog.provide('goog.ui.Button');
goog.provide('goog.ui.Button.Side');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyCodes');
goog.require('goog.events.KeyHandler');
goog.require('goog.ui.ButtonRenderer');
goog.require('goog.ui.ButtonSide');
goog.require('goog.ui.Component');
goog.require('goog.ui.Control');
goog.require('goog.ui.NativeButtonRenderer');
goog.require('goog.ui.registry');
/**
* A button control, rendered as a native browser button by default.
*
* @param {goog.ui.ControlContent} content Text caption or existing DOM
* structure to display as the button's caption.
* @param {goog.ui.ButtonRenderer=} opt_renderer Renderer used to render or
* decorate the button; defaults to {@link goog.ui.NativeButtonRenderer}.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM hepler, used for
* document interaction.
* @constructor
* @extends {goog.ui.Control}
*/
goog.ui.Button = function(content, opt_renderer, opt_domHelper) {
goog.ui.Control.call(this, content, opt_renderer ||
goog.ui.NativeButtonRenderer.getInstance(), opt_domHelper);
};
goog.inherits(goog.ui.Button, goog.ui.Control);
/**
* Constants for button sides, see {@link goog.ui.Button.prototype.setCollapsed}
* for details. Aliased from goog.ui.ButtonSide to support legacy users without
* creating a circular dependency in {@link goog.ui.ButtonRenderer}.
* @enum {number}
* @deprecated use {@link goog.ui.ButtonSide} instead.
*/
goog.ui.Button.Side = goog.ui.ButtonSide;
/**
* Value associated with the button.
* @type {*}
* @private
*/
goog.ui.Button.prototype.value_;
/**
* Tooltip text for the button, displayed on hover.
* @type {string|undefined}
* @private
*/
goog.ui.Button.prototype.tooltip_;
// goog.ui.Button API implementation.
/**
* Returns the value associated with the button.
* @return {*} Button value (undefined if none).
*/
goog.ui.Button.prototype.getValue = function() {
return this.value_;
};
/**
* Sets the value associated with the button, and updates its DOM.
* @param {*} value New button value.
*/
goog.ui.Button.prototype.setValue = function(value) {
this.value_ = value;
var renderer = /** @type {!goog.ui.ButtonRenderer} */ (this.getRenderer());
renderer.setValue(this.getElement(), /** @type {string} */ (value));
};
/**
* Sets the value associated with the button. Unlike {@link #setValue},
* doesn't update the button's DOM. Considered protected; to be called only
* by renderer code during element decoration.
* @param {*} value New button value.
* @protected
*/
goog.ui.Button.prototype.setValueInternal = function(value) {
this.value_ = value;
};
/**
* Returns the tooltip for the button.
* @return {string|undefined} Tooltip text (undefined if none).
*/
goog.ui.Button.prototype.getTooltip = function() {
return this.tooltip_;
};
/**
* Sets the tooltip for the button, and updates its DOM.
* @param {string} tooltip New tooltip text.
*/
goog.ui.Button.prototype.setTooltip = function(tooltip) {
this.tooltip_ = tooltip;
this.getRenderer().setTooltip(this.getElement(), tooltip);
};
/**
* Sets the tooltip for the button. Unlike {@link #setTooltip}, doesn't update
* the button's DOM. Considered protected; to be called only by renderer code
* during element decoration.
* @param {string} tooltip New tooltip text.
* @protected
*/
goog.ui.Button.prototype.setTooltipInternal = function(tooltip) {
this.tooltip_ = tooltip;
};
/**
* Collapses the border on one or both sides of the button, allowing it to be
* combined with the adjancent button(s), forming a single UI componenet with
* multiple targets.
* @param {number} sides Bitmap of one or more {@link goog.ui.ButtonSide}s for
* which borders should be collapsed.
*/
goog.ui.Button.prototype.setCollapsed = function(sides) {
this.getRenderer().setCollapsed(this, sides);
};
// goog.ui.Control & goog.ui.Component API implementation.
/** @override */
goog.ui.Button.prototype.disposeInternal = function() {
goog.ui.Button.superClass_.disposeInternal.call(this);
delete this.value_;
delete this.tooltip_;
};
/** @override */
goog.ui.Button.prototype.enterDocument = function() {
goog.ui.Button.superClass_.enterDocument.call(this);
if (this.isSupportedState(goog.ui.Component.State.FOCUSED)) {
var keyTarget = this.getKeyEventTarget();
if (keyTarget) {
this.getHandler().listen(keyTarget, goog.events.EventType.KEYUP,
this.handleKeyEventInternal);
}
}
};
/**
* Attempts to handle a keyboard event; returns true if the event was handled,
* false otherwise. If the button is enabled and the Enter/Space key was
* pressed, handles the event by dispatching an {@code ACTION} event,
* and returns true. Overrides {@link goog.ui.Control#handleKeyEventInternal}.
* @param {goog.events.KeyEvent} e Key event to handle.
* @return {boolean} Whether the key event was handled.
* @protected
* @override
*/
goog.ui.Button.prototype.handleKeyEventInternal = function(e) {
if (e.keyCode == goog.events.KeyCodes.ENTER &&
e.type == goog.events.KeyHandler.EventType.KEY ||
e.keyCode == goog.events.KeyCodes.SPACE &&
e.type == goog.events.EventType.KEYUP) {
return this.performActionInternal(e);
}
// Return true for space keypress (even though the event is handled on keyup)
// as preventDefault needs to be called up keypress to take effect in IE and
// WebKit.
return e.keyCode == goog.events.KeyCodes.SPACE;
};
// Register a decorator factory function for goog.ui.Buttons.
goog.ui.registry.setDecoratorByClassName(goog.ui.ButtonRenderer.CSS_CLASS,
function() {
return new goog.ui.Button(null);
});

View File

@@ -0,0 +1,214 @@
// 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 Default renderer for {@link goog.ui.Button}s.
*
* @author attila@google.com (Attila Bodis)
*/
goog.provide('goog.ui.ButtonRenderer');
goog.require('goog.a11y.aria');
goog.require('goog.a11y.aria.Role');
goog.require('goog.a11y.aria.State');
goog.require('goog.asserts');
goog.require('goog.ui.ButtonSide');
goog.require('goog.ui.Component');
goog.require('goog.ui.ControlRenderer');
/**
* Default renderer for {@link goog.ui.Button}s. Extends the superclass with
* the following button-specific API methods:
* <ul>
* <li>{@code getValue} - returns the button element's value
* <li>{@code setValue} - updates the button element to reflect its new value
* <li>{@code getTooltip} - returns the button element's tooltip text
* <li>{@code setTooltip} - updates the button element's tooltip text
* <li>{@code setCollapsed} - removes one or both of the button element's
* borders
* </ul>
* For alternate renderers, see {@link goog.ui.NativeButtonRenderer},
* {@link goog.ui.CustomButtonRenderer}, and {@link goog.ui.FlatButtonRenderer}.
* @constructor
* @extends {goog.ui.ControlRenderer}
*/
goog.ui.ButtonRenderer = function() {
goog.ui.ControlRenderer.call(this);
};
goog.inherits(goog.ui.ButtonRenderer, goog.ui.ControlRenderer);
goog.addSingletonGetter(goog.ui.ButtonRenderer);
/**
* Default CSS class to be applied to the root element of components rendered
* by this renderer.
* @type {string}
*/
goog.ui.ButtonRenderer.CSS_CLASS = goog.getCssName('goog-button');
/**
* Returns the ARIA role to be applied to buttons.
* @return {goog.a11y.aria.Role|undefined} ARIA role.
* @override
*/
goog.ui.ButtonRenderer.prototype.getAriaRole = function() {
return goog.a11y.aria.Role.BUTTON;
};
/**
* Updates the button's ARIA (accessibility) state if the button is being
* treated as a checkbox. Also makes sure that attributes which aren't
* supported by buttons aren't being added.
* @param {Element} element Element whose ARIA state is to be updated.
* @param {goog.ui.Component.State} state Component state being enabled or
* disabled.
* @param {boolean} enable Whether the state is being enabled or disabled.
* @protected
* @override
*/
goog.ui.ButtonRenderer.prototype.updateAriaState = function(element, state,
enable) {
switch (state) {
// If button has CHECKED or SELECTED state, assign aria-pressed
case goog.ui.Component.State.SELECTED:
case goog.ui.Component.State.CHECKED:
goog.asserts.assert(element,
'The button DOM element cannot be null.');
goog.a11y.aria.setState(element, goog.a11y.aria.State.PRESSED, enable);
break;
default:
case goog.ui.Component.State.OPENED:
case goog.ui.Component.State.DISABLED:
goog.base(this, 'updateAriaState', element, state, enable);
break;
}
};
/** @override */
goog.ui.ButtonRenderer.prototype.createDom = function(button) {
var element = goog.base(this, 'createDom', button);
this.setTooltip(element, button.getTooltip());
var value = button.getValue();
if (value) {
this.setValue(element, value);
}
// If this is a toggle button, set ARIA state
if (button.isSupportedState(goog.ui.Component.State.CHECKED)) {
this.updateAriaState(element, goog.ui.Component.State.CHECKED,
button.isChecked());
}
return element;
};
/** @override */
goog.ui.ButtonRenderer.prototype.decorate = function(button, element) {
// The superclass implementation takes care of common attributes; we only
// need to set the value and the tooltip.
element = goog.ui.ButtonRenderer.superClass_.decorate.call(this, button,
element);
button.setValueInternal(this.getValue(element));
button.setTooltipInternal(this.getTooltip(element));
// If this is a toggle button, set ARIA state
if (button.isSupportedState(goog.ui.Component.State.CHECKED)) {
this.updateAriaState(element, goog.ui.Component.State.CHECKED,
button.isChecked());
}
return element;
};
/**
* Takes a button's root element, and returns the value associated with it.
* No-op in the base class.
* @param {Element} element The button's root element.
* @return {string|undefined} The button's value (undefined if none).
*/
goog.ui.ButtonRenderer.prototype.getValue = goog.nullFunction;
/**
* Takes a button's root element and a value, and updates the element to reflect
* the new value. No-op in the base class.
* @param {Element} element The button's root element.
* @param {string} value New value.
*/
goog.ui.ButtonRenderer.prototype.setValue = goog.nullFunction;
/**
* Takes a button's root element, and returns its tooltip text.
* @param {Element} element The button's root element.
* @return {string|undefined} The tooltip text.
*/
goog.ui.ButtonRenderer.prototype.getTooltip = function(element) {
return element.title;
};
/**
* Takes a button's root element and a tooltip string, and updates the element
* with the new tooltip.
* @param {Element} element The button's root element.
* @param {string} tooltip New tooltip text.
* @protected
*/
goog.ui.ButtonRenderer.prototype.setTooltip = function(element, tooltip) {
// Don't set a title attribute if there isn't a tooltip. Blank title
// attributes can be interpreted incorrectly by screen readers.
if (element && tooltip) {
element.title = tooltip;
}
};
/**
* Collapses the border on one or both sides of the button, allowing it to be
* combined with the adjacent button(s), forming a single UI componenet with
* multiple targets.
* @param {goog.ui.Button} button Button to update.
* @param {number} sides Bitmap of one or more {@link goog.ui.ButtonSide}s for
* which borders should be collapsed.
* @protected
*/
goog.ui.ButtonRenderer.prototype.setCollapsed = function(button, sides) {
var isRtl = button.isRightToLeft();
var collapseLeftClassName =
goog.getCssName(this.getStructuralCssClass(), 'collapse-left');
var collapseRightClassName =
goog.getCssName(this.getStructuralCssClass(), 'collapse-right');
button.enableClassName(isRtl ? collapseRightClassName : collapseLeftClassName,
!!(sides & goog.ui.ButtonSide.START));
button.enableClassName(isRtl ? collapseLeftClassName : collapseRightClassName,
!!(sides & goog.ui.ButtonSide.END));
};
/** @override */
goog.ui.ButtonRenderer.prototype.getCssClass = function() {
return goog.ui.ButtonRenderer.CSS_CLASS;
};

View File

@@ -0,0 +1,41 @@
// 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 Enum for button side constants. In its own file so as to not
* cause a circular dependency with {@link goog.ui.ButtonRenderer}.
*
* @author doughtie@google.com (Gavin Doughtie)
*/
goog.provide('goog.ui.ButtonSide');
/**
* Constants for button sides, see {@link goog.ui.Button.prototype.setCollapsed}
* for details.
* @enum {number}
*/
goog.ui.ButtonSide = {
/** Neither side. */
NONE: 0,
/** Left for LTR, right for RTL. */
START: 1,
/** Right for LTR, left for RTL. */
END: 2,
/** Both sides. */
BOTH: 3
};

View File

@@ -0,0 +1,199 @@
// Copyright 2006 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 Character counter widget implementation.
*
* @author eae@google.com (Emil A Eklund)
* @see ../demos/charcounter.html
*/
goog.provide('goog.ui.CharCounter');
goog.provide('goog.ui.CharCounter.Display');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.events.InputHandler');
/**
* CharCounter widget. Counts the number of characters in a input field or a
* text box and displays the number of additional characters that may be
* entered before the maximum length is reached.
*
* @extends {goog.events.EventTarget}
* @param {HTMLInputElement|HTMLTextAreaElement} elInput Input or text area
* element to count the number of characters in. You can pass in null
* for this if you don't want to expose the number of chars remaining.
* @param {Element} elCount HTML element to display the remaining number of
* characters in.
* @param {number} maxLength The maximum length.
* @param {goog.ui.CharCounter.Display=} opt_displayMode Display mode for this
* char counter. Defaults to {@link goog.ui.CharCounter.Display.REMAINING}.
* @constructor
*/
goog.ui.CharCounter = function(elInput, elCount, maxLength, opt_displayMode) {
goog.events.EventTarget.call(this);
/**
* Input or text area element to count the number of characters in.
* @type {HTMLInputElement|HTMLTextAreaElement}
* @private
*/
this.elInput_ = elInput;
/**
* HTML element to display the remaining number of characters in.
* @type {Element}
* @private
*/
this.elCount_ = elCount;
/**
* The maximum length.
* @type {number}
* @private
*/
this.maxLength_ = maxLength;
/**
* The display mode for this char counter.
* @type {!goog.ui.CharCounter.Display}
* @private
*/
this.display_ = opt_displayMode || goog.ui.CharCounter.Display.REMAINING;
elInput.maxLength = maxLength;
/**
* The input handler that provides the input event.
* @type {goog.events.InputHandler}
* @private
*/
this.inputHandler_ = new goog.events.InputHandler(elInput);
goog.events.listen(this.inputHandler_,
goog.events.InputHandler.EventType.INPUT, this.onChange_, false, this);
this.checkLength();
};
goog.inherits(goog.ui.CharCounter, goog.events.EventTarget);
/**
* Display mode for the char counter.
* @enum {number}
*/
goog.ui.CharCounter.Display = {
/** Widget displays the number of characters remaining (the default). */
REMAINING: 0,
/** Widget displays the number of characters entered. */
INCREMENTAL: 1
};
/**
* Sets the maximum length.
*
* @param {number} maxLength The maximum length.
*/
goog.ui.CharCounter.prototype.setMaxLength = function(maxLength) {
this.maxLength_ = maxLength;
this.elInput_.maxLength = maxLength;
this.checkLength();
};
/**
* Returns the maximum length.
*
* @return {number} The maximum length.
*/
goog.ui.CharCounter.prototype.getMaxLength = function() {
return this.maxLength_;
};
/**
* Sets the display mode.
*
* @param {!goog.ui.CharCounter.Display} displayMode The display mode.
*/
goog.ui.CharCounter.prototype.setDisplayMode = function(displayMode) {
this.display_ = displayMode;
this.checkLength();
};
/**
* Returns the display mode.
*
* @return {!goog.ui.CharCounter.Display} The display mode.
*/
goog.ui.CharCounter.prototype.getDisplayMode = function() {
return this.display_;
};
/**
* Change event handler for input field.
*
* @param {goog.events.BrowserEvent} event Change event.
* @private
*/
goog.ui.CharCounter.prototype.onChange_ = function(event) {
this.checkLength();
};
/**
* Checks length of text in input field and updates the counter. Truncates text
* if the maximum lengths is exceeded.
*/
goog.ui.CharCounter.prototype.checkLength = function() {
var count = this.elInput_.value.length;
// There's no maxlength property for textareas so instead we truncate the
// text if it gets too long. It's also used to truncate the text in a input
// field if the maximum length is changed.
if (count > this.maxLength_) {
var scrollTop = this.elInput_.scrollTop;
var scrollLeft = this.elInput_.scrollLeft;
this.elInput_.value = this.elInput_.value.substring(0, this.maxLength_);
count = this.maxLength_;
this.elInput_.scrollTop = scrollTop;
this.elInput_.scrollLeft = scrollLeft;
}
if (this.elCount_) {
var incremental = this.display_ == goog.ui.CharCounter.Display.INCREMENTAL;
goog.dom.setTextContent(
this.elCount_,
String(incremental ? count : this.maxLength_ - count));
}
};
/** @override */
goog.ui.CharCounter.prototype.disposeInternal = function() {
goog.ui.CharCounter.superClass_.disposeInternal.call(this);
delete this.elInput_;
this.inputHandler_.dispose();
this.inputHandler_ = null;
};

View File

@@ -0,0 +1,880 @@
// Copyright 2009 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 Character Picker widget for picking any Unicode character.
*
* @see ../demos/charpicker.html
*/
goog.provide('goog.ui.CharPicker');
goog.require('goog.a11y.aria');
goog.require('goog.a11y.aria.State');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom.classes');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventType');
goog.require('goog.events.InputHandler');
goog.require('goog.events.KeyCodes');
goog.require('goog.events.KeyHandler');
goog.require('goog.i18n.CharListDecompressor');
goog.require('goog.i18n.uChar');
goog.require('goog.structs.Set');
goog.require('goog.style');
goog.require('goog.ui.Button');
goog.require('goog.ui.Component');
goog.require('goog.ui.ContainerScroller');
goog.require('goog.ui.FlatButtonRenderer');
goog.require('goog.ui.HoverCard');
goog.require('goog.ui.LabelInput');
goog.require('goog.ui.Menu');
goog.require('goog.ui.MenuButton');
goog.require('goog.ui.MenuItem');
goog.require('goog.ui.Tooltip');
/**
* Character Picker Class. This widget can be used to pick any Unicode
* character by traversing a category-subcategory structure or by inputing its
* hex value.
*
* See charpicker.html demo for example usage.
* @param {goog.i18n.CharPickerData} charPickerData Category names and charlist.
* @param {!goog.i18n.uChar.NameFetcher} charNameFetcher Object which fetches
* the names of the characters that are shown in the widget. These names
* may be stored locally or come from an external source.
* @param {Array.<string>=} opt_recents List of characters to be displayed in
* resently selected characters area.
* @param {number=} opt_initCategory Sequence number of initial category.
* @param {number=} opt_initSubcategory Sequence number of initial subcategory.
* @param {number=} opt_rowCount Number of rows in the grid.
* @param {number=} opt_columnCount Number of columns in the grid.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @constructor
* @extends {goog.ui.Component}
*/
goog.ui.CharPicker = function(charPickerData, charNameFetcher, opt_recents,
opt_initCategory, opt_initSubcategory,
opt_rowCount, opt_columnCount, opt_domHelper) {
goog.ui.Component.call(this, opt_domHelper);
/**
* Object used to retrieve character names.
* @type {!goog.i18n.uChar.NameFetcher}
* @private
*/
this.charNameFetcher_ = charNameFetcher;
/**
* Object containing character lists and category names.
* @type {goog.i18n.CharPickerData}
* @private
*/
this.data_ = charPickerData;
/**
* The category number to be used on widget init.
* @type {number}
* @private
*/
this.initCategory_ = opt_initCategory || 0;
/**
* The subcategory number to be used on widget init.
* @type {number}
* @private
*/
this.initSubcategory_ = opt_initSubcategory || 0;
/**
* Number of columns in the grid.
* @type {number}
* @private
*/
this.columnCount_ = opt_columnCount || 10;
/**
* Number of entries to be added to the grid.
* @type {number}
* @private
*/
this.gridsize_ = (opt_rowCount || 10) * this.columnCount_;
/**
* Number of the recently selected characters displayed.
* @type {number}
* @private
*/
this.recentwidth_ = this.columnCount_ + 1;
/**
* List of recently used characters.
* @type {Array.<string>}
* @private
*/
this.recents_ = opt_recents || [];
/**
* Handler for events.
* @type {goog.events.EventHandler}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
/**
* Decompressor used to get the list of characters from a base88 encoded
* character list.
* @type {Object}
* @private
*/
this.decompressor_ = new goog.i18n.CharListDecompressor();
};
goog.inherits(goog.ui.CharPicker, goog.ui.Component);
/**
* The last selected character.
* @type {?string}
* @private
*/
goog.ui.CharPicker.prototype.selectedChar_ = null;
/**
* Set of formatting characters whose display need to be swapped with nbsp
* to prevent layout issues.
* @type {goog.structs.Set}
* @private
*/
goog.ui.CharPicker.prototype.layoutAlteringChars_ = null;
/**
* The top category menu.
* @type {goog.ui.Menu}
* @private
*/
goog.ui.CharPicker.prototype.menu_ = null;
/**
* The top category menu button.
* @type {goog.ui.MenuButton}
* @private
*/
goog.ui.CharPicker.prototype.menubutton_ = null;
/**
* The subcategory menu.
* @type {goog.ui.Menu}
* @private
*/
goog.ui.CharPicker.prototype.submenu_ = null;
/**
* The subcategory menu button.
* @type {goog.ui.MenuButton}
* @private
*/
goog.ui.CharPicker.prototype.submenubutton_ = null;
/**
* The element representing the number of rows visible in the grid.
* This along with goog.ui.CharPicker.stick_ would help to create a scrollbar
* of right size.
* @type {Element}
* @private
*/
goog.ui.CharPicker.prototype.stickwrap_ = null;
/**
* The component containing all the buttons for each character in display.
* @type {goog.ui.Component}
* @private
*/
goog.ui.CharPicker.prototype.grid_ = null;
/**
* The component used for extra information about the character set displayed.
* @type {goog.ui.Component}
* @private
*/
goog.ui.CharPicker.prototype.notice_ = null;
/**
* Grid displaying recently selected characters.
* @type {goog.ui.Component}
* @private
*/
goog.ui.CharPicker.prototype.recentgrid_ = null;
/**
* Input field for entering the hex value of the character.
* @type {goog.ui.Component}
* @private
*/
goog.ui.CharPicker.prototype.input_ = null;
/**
* OK button for entering hex value of the character.
* @type {goog.ui.Component}
* @private
*/
goog.ui.CharPicker.prototype.okbutton_ = null;
/**
* Element displaying character name in preview.
* @type {Element}
* @private
*/
goog.ui.CharPicker.prototype.charNameEl_ = null;
/**
* Element displaying character in preview.
* @type {Element}
* @private
*/
goog.ui.CharPicker.prototype.zoomEl_ = null;
/**
* Element displaying character number (codepoint) in preview.
* @type {Element}
* @private
*/
goog.ui.CharPicker.prototype.unicodeEl_ = null;
/**
* Hover card for displaying the preview of a character.
* Preview would contain character in large size and its U+ notation. It would
* also display the name, if available.
* @type {goog.ui.HoverCard}
* @private
*/
goog.ui.CharPicker.prototype.hc_ = null;
/**
* Gets the last selected character.
* @return {?string} The last selected character.
*/
goog.ui.CharPicker.prototype.getSelectedChar = function() {
return this.selectedChar_;
};
/**
* Gets the list of characters user selected recently.
* @return {Array.<string>} The recent character list.
*/
goog.ui.CharPicker.prototype.getRecentChars = function() {
return this.recents_;
};
/** @override */
goog.ui.CharPicker.prototype.createDom = function() {
goog.ui.CharPicker.superClass_.createDom.call(this);
this.decorateInternal(this.getDomHelper().createElement('div'));
};
/** @override */
goog.ui.CharPicker.prototype.disposeInternal = function() {
goog.dispose(this.hc_);
this.hc_ = null;
goog.dispose(this.eventHandler_);
this.eventHandler_ = null;
goog.ui.CharPicker.superClass_.disposeInternal.call(this);
};
/** @override */
goog.ui.CharPicker.prototype.decorateInternal = function(element) {
goog.ui.CharPicker.superClass_.decorateInternal.call(this, element);
// The chars below cause layout disruption or too narrow to hover:
// \u0020, \u00AD, \u2000 - \u200f, \u2028 - \u202f, \u3000, \ufeff
var chrs = this.decompressor_.toCharList(':2%C^O80V1H2s2G40Q%s0');
this.layoutAlteringChars_ = new goog.structs.Set(chrs);
this.menu_ = new goog.ui.Menu(this.getDomHelper());
var categories = this.data_.categories;
for (var i = 0; i < this.data_.categories.length; i++) {
this.menu_.addChild(this.createMenuItem_(i, categories[i]), true);
}
this.menubutton_ = new goog.ui.MenuButton('Category Menu', this.menu_,
/* opt_renderer */ undefined, this.getDomHelper());
this.addChild(this.menubutton_, true);
this.submenu_ = new goog.ui.Menu(this.getDomHelper());
this.submenubutton_ = new goog.ui.MenuButton('Subcategory Menu',
this.submenu_, /* opt_renderer */ undefined, this.getDomHelper());
this.addChild(this.submenubutton_, true);
// The containing component for grid component and the scroller.
var gridcontainer = new goog.ui.Component(this.getDomHelper());
this.addChild(gridcontainer, true);
var stickwrap = new goog.ui.Component(this.getDomHelper());
gridcontainer.addChild(stickwrap, true);
this.stickwrap_ = stickwrap.getElement();
var stick = new goog.ui.Component(this.getDomHelper());
stickwrap.addChild(stick, true);
this.stick_ = stick.getElement();
this.grid_ = new goog.ui.Component(this.getDomHelper());
gridcontainer.addChild(this.grid_, true);
this.notice_ = new goog.ui.Component(this.getDomHelper());
this.notice_.setElementInternal(this.getDomHelper().createDom('div'));
this.addChild(this.notice_, true);
// The component used for displaying 'Recent Selections' label.
/**
* @desc The text label above the list of recently selected characters.
*/
var MSG_CHAR_PICKER_RECENT_SELECTIONS = goog.getMsg('Recent Selections:');
var recenttext = new goog.ui.Component(this.getDomHelper());
recenttext.setElementInternal(this.getDomHelper().createDom('span', null,
MSG_CHAR_PICKER_RECENT_SELECTIONS));
this.addChild(recenttext, true);
this.recentgrid_ = new goog.ui.Component(this.getDomHelper());
this.addChild(this.recentgrid_, true);
// The component used for displaying 'U+'.
var uplus = new goog.ui.Component(this.getDomHelper());
uplus.setElementInternal(this.getDomHelper().createDom('span', null, 'U+'));
this.addChild(uplus, true);
/**
* @desc The text inside the input box to specify the hex code of a character.
*/
var MSG_CHAR_PICKER_HEX_INPUT = goog.getMsg('Hex Input');
this.input_ = new goog.ui.LabelInput(
MSG_CHAR_PICKER_HEX_INPUT, this.getDomHelper());
this.addChild(this.input_, true);
this.okbutton_ = new goog.ui.Button(
'OK', /* opt_renderer */ undefined, this.getDomHelper());
this.addChild(this.okbutton_, true);
this.okbutton_.setEnabled(false);
this.zoomEl_ = this.getDomHelper().createDom('div',
{id: 'zoom', className: goog.getCssName('goog-char-picker-char-zoom')});
this.charNameEl_ = this.getDomHelper().createDom('div',
{id: 'charName', className: goog.getCssName('goog-char-picker-name')});
this.unicodeEl_ = this.getDomHelper().createDom('div',
{id: 'unicode', className: goog.getCssName('goog-char-picker-unicode')});
var card = this.getDomHelper().createDom('div',
{'id': 'preview'},
this.zoomEl_, this.charNameEl_, this.unicodeEl_);
goog.style.setElementShown(card, false);
this.hc_ = new goog.ui.HoverCard({'DIV': 'char'},
/* opt_checkDescendants */ undefined, this.getDomHelper());
this.hc_.setElement(card);
var self = this;
/**
* Function called by hover card just before it is visible to collect data.
*/
function onBeforeShow() {
var trigger = self.hc_.getAnchorElement();
var ch = self.getChar_(trigger);
if (ch) {
self.zoomEl_.innerHTML = self.displayChar_(ch);
self.unicodeEl_.innerHTML = goog.i18n.uChar.toHexString(ch);
// Clear the character name since we don't want to show old data because
// it is retrieved asynchronously and the DOM object is re-used
self.charNameEl_.innerHTML = '';
self.charNameFetcher_.getName(ch, function(charName) {
if (charName) {
self.charNameEl_.innerHTML = charName;
}
});
}
}
goog.events.listen(this.hc_, goog.ui.HoverCard.EventType.BEFORE_SHOW,
onBeforeShow);
goog.dom.classes.add(element, goog.getCssName('goog-char-picker'));
goog.dom.classes.add(this.stick_, goog.getCssName('goog-stick'));
goog.dom.classes.add(this.stickwrap_, goog.getCssName('goog-stickwrap'));
goog.dom.classes.add(gridcontainer.getElement(),
goog.getCssName('goog-char-picker-grid-container'));
goog.dom.classes.add(this.grid_.getElement(),
goog.getCssName('goog-char-picker-grid'));
goog.dom.classes.add(this.recentgrid_.getElement(),
goog.getCssName('goog-char-picker-grid'));
goog.dom.classes.add(this.recentgrid_.getElement(),
goog.getCssName('goog-char-picker-recents'));
goog.dom.classes.add(this.notice_.getElement(),
goog.getCssName('goog-char-picker-notice'));
goog.dom.classes.add(uplus.getElement(),
goog.getCssName('goog-char-picker-uplus'));
goog.dom.classes.add(this.input_.getElement(),
goog.getCssName('goog-char-picker-input-box'));
goog.dom.classes.add(this.okbutton_.getElement(),
goog.getCssName('goog-char-picker-okbutton'));
goog.dom.classes.add(card, goog.getCssName('goog-char-picker-hovercard'));
this.hc_.className = goog.getCssName('goog-char-picker-hovercard');
this.grid_.buttoncount = this.gridsize_;
this.recentgrid_.buttoncount = this.recentwidth_;
this.populateGridWithButtons_(this.grid_);
this.populateGridWithButtons_(this.recentgrid_);
this.updateGrid_(this.recentgrid_, this.recents_);
this.setSelectedCategory_(this.initCategory_, this.initSubcategory_);
new goog.ui.ContainerScroller(this.menu_);
new goog.ui.ContainerScroller(this.submenu_);
goog.dom.classes.add(this.menu_.getElement(),
goog.getCssName('goog-char-picker-menu'));
goog.dom.classes.add(this.submenu_.getElement(),
goog.getCssName('goog-char-picker-menu'));
};
/** @override */
goog.ui.CharPicker.prototype.enterDocument = function() {
goog.ui.CharPicker.superClass_.enterDocument.call(this);
var inputkh = new goog.events.InputHandler(this.input_.getElement());
this.keyHandler_ = new goog.events.KeyHandler(this.input_.getElement());
// Stop the propagation of ACTION events at menu and submenu buttons.
// If stopped at capture phase, the button will not be set to normal state.
// If not stopped, the user widget will receive the event, which is
// undesired. User widget should receive an event only on the character
// click.
this.eventHandler_.
listen(
this.menubutton_,
goog.ui.Component.EventType.ACTION,
goog.events.Event.stopPropagation).
listen(
this.submenubutton_,
goog.ui.Component.EventType.ACTION,
goog.events.Event.stopPropagation).
listen(
this,
goog.ui.Component.EventType.ACTION,
this.handleSelectedItem_,
true).
listen(
inputkh,
goog.events.InputHandler.EventType.INPUT,
this.handleInput_).
listen(
this.keyHandler_,
goog.events.KeyHandler.EventType.KEY,
this.handleEnter_).
listen(
this.recentgrid_,
goog.ui.Component.EventType.FOCUS,
this.handleFocus_).
listen(
this.grid_,
goog.ui.Component.EventType.FOCUS,
this.handleFocus_);
goog.events.listen(this.okbutton_.getElement(),
goog.events.EventType.MOUSEDOWN, this.handleOkClick_, true, this);
goog.events.listen(this.stickwrap_, goog.events.EventType.SCROLL,
this.handleScroll_, true, this);
};
/**
* Handles the button focus by updating the aria label with the character name
* so it becomes possible to get spoken feedback while tabbing through the
* visible symbols.
* @param {goog.events.Event} e The focus event.
* @private
*/
goog.ui.CharPicker.prototype.handleFocus_ = function(e) {
var button = e.target;
var element = button.getElement();
var ch = this.getChar_(element);
// Clear the aria label to avoid speaking the old value in case the button
// element has no char attribute or the character name cannot be retrieved.
goog.a11y.aria.setState(element, goog.a11y.aria.State.LABEL, '');
if (ch) {
// This is working with screen readers because the call to getName is
// synchronous once the values have been prefetched by the RemoteNameFetcher
// and because it is always synchronous when using the LocalNameFetcher.
// Also, the special character itself is not used as the label because some
// screen readers, notably ChromeVox, are not able to speak them.
// TODO(user): Consider changing the NameFetcher API to provide a
// method that lets the caller retrieve multiple character names at once
// so that this asynchronous gymnastic can be avoided.
this.charNameFetcher_.getName(ch, function(charName) {
if (charName) {
goog.a11y.aria.setState(
element, goog.a11y.aria.State.LABEL, charName);
}
});
}
};
/**
* On scroll, updates the grid with characters correct to the scroll position.
* @param {goog.events.Event} e Scroll event to handle.
* @private
*/
goog.ui.CharPicker.prototype.handleScroll_ = function(e) {
var height = e.target.scrollHeight;
var top = e.target.scrollTop;
var itempos = Math.ceil(top * this.items.length / (this.columnCount_ *
height)) * this.columnCount_;
if (this.itempos != itempos) {
this.itempos = itempos;
this.modifyGridWithItems_(this.grid_, this.items, itempos);
}
e.stopPropagation();
};
/**
* On a menu click, sets correct character set in the grid; on a grid click
* accept the character as the selected one and adds to recent selection, if not
* already present.
* @param {goog.events.Event} e Event for the click on menus or grid.
* @private
*/
goog.ui.CharPicker.prototype.handleSelectedItem_ = function(e) {
if (e.target.getParent() == this.menu_) {
this.menu_.setVisible(false);
this.setSelectedCategory_(e.target.getValue());
} else if (e.target.getParent() == this.submenu_) {
this.submenu_.setVisible(false);
this.setSelectedSubcategory_(e.target.getValue());
} else if (e.target.getParent() == this.grid_) {
var button = e.target.getElement();
this.selectedChar_ = this.getChar_(button);
this.updateRecents_(this.selectedChar_);
} else if (e.target.getParent() == this.recentgrid_) {
this.selectedChar_ = this.getChar_(e.target.getElement());
}
};
/**
* When user types the characters displays the preview. Enables the OK button,
* if the character is valid.
* @param {goog.events.Event} e Event for typing in input field.
* @private
*/
goog.ui.CharPicker.prototype.handleInput_ = function(e) {
var ch = this.getInputChar();
if (ch) {
var unicode = goog.i18n.uChar.toHexString(ch);
this.zoomEl_.innerHTML = ch;
this.unicodeEl_.innerHTML = unicode;
this.charNameEl_.innerHTML = '';
var coord =
new goog.ui.Tooltip.ElementTooltipPosition(this.input_.getElement());
this.hc_.setPosition(coord);
this.hc_.triggerForElement(this.input_.getElement());
this.okbutton_.setEnabled(true);
} else {
this.hc_.cancelTrigger();
this.hc_.setVisible(false);
this.okbutton_.setEnabled(false);
}
};
/**
* On OK click accepts the character and updates the recent char list.
* @param {goog.events.Event=} opt_event Event for click on OK button.
* @return {boolean} Indicates whether to propagate event.
* @private
*/
goog.ui.CharPicker.prototype.handleOkClick_ = function(opt_event) {
var ch = this.getInputChar();
if (ch && ch.charCodeAt(0)) {
this.selectedChar_ = ch;
this.updateRecents_(ch);
return true;
}
return false;
};
/**
* Behaves exactly like the OK button on Enter key.
* @param {goog.events.KeyEvent} e Event for enter on the input field.
* @return {boolean} Indicates whether to propagate event.
* @private
*/
goog.ui.CharPicker.prototype.handleEnter_ = function(e) {
if (e.keyCode == goog.events.KeyCodes.ENTER) {
return this.handleOkClick_() ?
this.dispatchEvent(goog.ui.Component.EventType.ACTION) : false;
}
return false;
};
/**
* Gets the character from the event target.
* @param {Element} e Event target containing the 'char' attribute.
* @return {string} The character specified in the event.
* @private
*/
goog.ui.CharPicker.prototype.getChar_ = function(e) {
return e.getAttribute('char');
};
/**
* Creates a menu entry for either the category listing or subcategory listing.
* @param {number} id Id to be used for the entry.
* @param {string} caption Text displayed for the menu item.
* @return {goog.ui.MenuItem} Menu item to be added to the menu listing.
* @private
*/
goog.ui.CharPicker.prototype.createMenuItem_ = function(id, caption) {
var item = new goog.ui.MenuItem(caption, /* model */ id, this.getDomHelper());
item.setVisible(true);
return item;
};
/**
* Sets the category and updates the submenu items and grid accordingly.
* @param {number} category Category index used to index the data tables.
* @param {number=} opt_subcategory Subcategory index used with category index.
* @private
*/
goog.ui.CharPicker.prototype.setSelectedCategory_ = function(category,
opt_subcategory) {
this.category = category;
this.menubutton_.setCaption(this.data_.categories[category]);
while (this.submenu_.hasChildren()) {
this.submenu_.removeChildAt(0, true).dispose();
}
var subcategories = this.data_.subcategories[category];
var charList = this.data_.charList[category];
for (var i = 0; i < subcategories.length; i++) {
var subtitle = charList[i].length == 0;
var item = this.createMenuItem_(i, subcategories[i]);
this.submenu_.addChild(item, true);
}
this.setSelectedSubcategory_(opt_subcategory || 0);
};
/**
* Sets the subcategory and updates the grid accordingly.
* @param {number} subcategory Sub-category index used to index the data tables.
* @private
*/
goog.ui.CharPicker.prototype.setSelectedSubcategory_ = function(subcategory) {
var subcategories = this.data_.subcategories;
var name = subcategories[this.category][subcategory];
this.submenubutton_.setCaption(name);
this.setSelectedGrid_(this.category, subcategory);
};
/**
* Updates the grid according to a given category and subcategory.
* @param {number} category Index to the category table.
* @param {number} subcategory Index to the subcategory table.
* @private
*/
goog.ui.CharPicker.prototype.setSelectedGrid_ = function(category,
subcategory) {
var charLists = this.data_.charList;
var charListStr = charLists[category][subcategory];
var content = this.decompressor_.toCharList(charListStr);
this.charNameFetcher_.prefetch(charListStr);
this.updateGrid_(this.grid_, content);
};
/**
* Updates the grid with new character list.
* @param {goog.ui.Component} grid The grid which is updated with a new set of
* characters.
* @param {Array.<string>} items Characters to be added to the grid.
* @private
*/
goog.ui.CharPicker.prototype.updateGrid_ = function(grid, items) {
if (grid == this.grid_) {
/**
* @desc The message used when there are invisible characters like space
* or format control characters.
*/
var MSG_PLEASE_HOVER =
goog.getMsg('Please hover over each cell for the character name.');
this.notice_.getElement().innerHTML =
this.charNameFetcher_.isNameAvailable(items[0]) ? MSG_PLEASE_HOVER : '';
this.items = items;
if (this.stickwrap_.offsetHeight > 0) {
this.stick_.style.height =
this.stickwrap_.offsetHeight * items.length / this.gridsize_ + 'px';
} else {
// This is the last ditch effort if height is not avaialble.
// Maximum of 3em is assumed to the the cell height. Extra space after
// last character in the grid is OK.
this.stick_.style.height = 3 * this.columnCount_ * items.length /
this.gridsize_ + 'em';
}
this.stickwrap_.scrollTop = 0;
}
this.modifyGridWithItems_(grid, items, 0);
};
/**
* Updates the grid with new character list for a given starting point.
* @param {goog.ui.Component} grid The grid which is updated with a new set of
* characters.
* @param {Array.<string>} items Characters to be added to the grid.
* @param {number} start The index from which the characters should be
* displayed.
* @private
*/
goog.ui.CharPicker.prototype.modifyGridWithItems_ = function(grid, items,
start) {
for (var buttonpos = 0, itempos = start;
buttonpos < grid.buttoncount && itempos < items.length;
buttonpos++, itempos++) {
this.modifyCharNode_(grid.getChildAt(buttonpos), items[itempos]);
}
for (; buttonpos < grid.buttoncount; buttonpos++) {
grid.getChildAt(buttonpos).setVisible(false);
}
};
/**
* Creates the grid for characters to displayed for selection.
* @param {goog.ui.Component} grid The grid which is updated with a new set of
* characters.
* @private
*/
goog.ui.CharPicker.prototype.populateGridWithButtons_ = function(grid) {
for (var i = 0; i < grid.buttoncount; i++) {
var button = new goog.ui.Button(' ',
goog.ui.FlatButtonRenderer.getInstance(),
this.getDomHelper());
// Dispatch the focus event so we can update the aria description while
// the user tabs through the cells.
button.setDispatchTransitionEvents(goog.ui.Component.State.FOCUSED, true);
grid.addChild(button, true);
button.setVisible(false);
var buttonEl = button.getElement();
goog.asserts.assert(buttonEl, 'The button DOM element cannot be null.');
// Override the button role so the user doesn't hear "button" each time he
// tabs through the cells.
goog.a11y.aria.removeRole(buttonEl);
}
};
/**
* Updates the grid cell with new character.
* @param {goog.ui.Component} button This button is proped up for new character.
* @param {string} ch Character to be displayed by the button.
* @private
*/
goog.ui.CharPicker.prototype.modifyCharNode_ = function(button, ch) {
var text = this.displayChar_(ch);
var buttonEl = button.getElement();
buttonEl.innerHTML = text;
buttonEl.setAttribute('char', ch);
button.setVisible(true);
};
/**
* Adds a given character to the recent character list.
* @param {string} character Character to be added to the recent list.
* @private
*/
goog.ui.CharPicker.prototype.updateRecents_ = function(character) {
if (character && character.charCodeAt(0) &&
!goog.array.contains(this.recents_, character)) {
this.recents_.unshift(character);
if (this.recents_.length > this.recentwidth_) {
this.recents_.pop();
}
this.updateGrid_(this.recentgrid_, this.recents_);
}
};
/**
* Gets the user inputed unicode character.
* @return {string} Unicode character inputed by user.
*/
goog.ui.CharPicker.prototype.getInputChar = function() {
var text = this.input_.getValue();
var code = parseInt(text, 16);
return /** @type {string} */ (goog.i18n.uChar.fromCharCode(code));
};
/**
* Gets the display character for the given character.
* @param {string} ch Character whose display is fetched.
* @return {string} The display of the given character.
* @private
*/
goog.ui.CharPicker.prototype.displayChar_ = function(ch) {
return this.layoutAlteringChars_.contains(ch) ? '\u00A0' : ch;
};

View File

@@ -0,0 +1,269 @@
// Copyright 2009 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 Tristate checkbox widget.
*
* @see ../demos/checkbox.html
*/
goog.provide('goog.ui.Checkbox');
goog.provide('goog.ui.Checkbox.State');
goog.require('goog.a11y.aria');
goog.require('goog.a11y.aria.State');
goog.require('goog.asserts');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyCodes');
goog.require('goog.ui.CheckboxRenderer');
goog.require('goog.ui.Component.EventType');
goog.require('goog.ui.Component.State');
goog.require('goog.ui.Control');
goog.require('goog.ui.registry');
/**
* 3-state checkbox widget. Fires CHECK or UNCHECK events before toggled and
* CHANGE event after toggled by user.
* The checkbox can also be enabled/disabled and get focused and highlighted.
*
* @param {goog.ui.Checkbox.State=} opt_checked Checked state to set.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for
* document interaction.
* @param {goog.ui.CheckboxRenderer=} opt_renderer Renderer used to render or
* decorate the checkbox; defaults to {@link goog.ui.CheckboxRenderer}.
* @constructor
* @extends {goog.ui.Control}
*/
goog.ui.Checkbox = function(opt_checked, opt_domHelper, opt_renderer) {
var renderer = opt_renderer || goog.ui.CheckboxRenderer.getInstance();
goog.ui.Control.call(this, null, renderer, opt_domHelper);
// The checkbox maintains its own tri-state CHECKED state.
// The control class maintains DISABLED, ACTIVE, and FOCUSED (which enable tab
// navigation, and keyHandling with SPACE).
/**
* Checked state of the checkbox.
* @type {goog.ui.Checkbox.State}
* @private
*/
this.checked_ = goog.isDef(opt_checked) ?
opt_checked : goog.ui.Checkbox.State.UNCHECKED;
};
goog.inherits(goog.ui.Checkbox, goog.ui.Control);
/**
* Possible checkbox states.
* @enum {?boolean}
*/
goog.ui.Checkbox.State = {
CHECKED: true,
UNCHECKED: false,
UNDETERMINED: null
};
/**
* Label element bound to the checkbox.
* @type {Element}
* @private
*/
goog.ui.Checkbox.prototype.label_ = null;
/**
* @return {goog.ui.Checkbox.State} Checked state of the checkbox.
*/
goog.ui.Checkbox.prototype.getChecked = function() {
return this.checked_;
};
/**
* @return {boolean} Whether the checkbox is checked.
* @override
*/
goog.ui.Checkbox.prototype.isChecked = function() {
return this.checked_ == goog.ui.Checkbox.State.CHECKED;
};
/**
* @return {boolean} Whether the checkbox is not checked.
*/
goog.ui.Checkbox.prototype.isUnchecked = function() {
return this.checked_ == goog.ui.Checkbox.State.UNCHECKED;
};
/**
* @return {boolean} Whether the checkbox is in partially checked state.
*/
goog.ui.Checkbox.prototype.isUndetermined = function() {
return this.checked_ == goog.ui.Checkbox.State.UNDETERMINED;
};
/**
* Sets the checked state of the checkbox.
* @param {?boolean} checked The checked state to set.
* @override
*/
goog.ui.Checkbox.prototype.setChecked = function(checked) {
if (checked != this.checked_) {
this.checked_ = /** @type {goog.ui.Checkbox.State} */ (checked);
this.getRenderer().setCheckboxState(this.getElement(), this.checked_);
}
};
/**
* Sets the checked state for the checkbox. Unlike {@link #setChecked},
* doesn't update the checkbox's DOM. Considered protected; to be called
* only by renderer code during element decoration.
* @param {goog.ui.Checkbox.State} checked New checkbox state.
*/
goog.ui.Checkbox.prototype.setCheckedInternal = function(checked) {
this.checked_ = checked;
};
/**
* Binds an HTML element to the checkbox which if clicked toggles the checkbox.
* Behaves the same way as the 'label' HTML tag. The label element has to be the
* direct or non-direct ancestor of the checkbox element because it will get the
* focus when keyboard support is implemented.
*
* @param {Element} label The label control to set. If null, only the checkbox
* reacts to clicks.
*/
goog.ui.Checkbox.prototype.setLabel = function(label) {
if (this.isInDocument()) {
this.exitDocument();
this.label_ = label;
this.enterDocument();
} else {
this.label_ = label;
}
};
/**
* Toggles the checkbox. State transitions:
* <ul>
* <li>unchecked -> checked
* <li>undetermined -> checked
* <li>checked -> unchecked
* </ul>
*/
goog.ui.Checkbox.prototype.toggle = function() {
this.setChecked(this.checked_ ? goog.ui.Checkbox.State.UNCHECKED :
goog.ui.Checkbox.State.CHECKED);
};
/** @override */
goog.ui.Checkbox.prototype.enterDocument = function() {
goog.base(this, 'enterDocument');
if (this.isHandleMouseEvents()) {
var handler = this.getHandler();
// Listen to the label, if it was set.
if (this.label_) {
// Any mouse events that happen to the associated label should have the
// same effect on the checkbox as if they were happening to the checkbox
// itself.
handler.
listen(this.label_, goog.events.EventType.CLICK,
this.handleClickOrSpace_).
listen(this.label_, goog.events.EventType.MOUSEOVER,
this.handleMouseOver).
listen(this.label_, goog.events.EventType.MOUSEOUT,
this.handleMouseOut).
listen(this.label_, goog.events.EventType.MOUSEDOWN,
this.handleMouseDown).
listen(this.label_, goog.events.EventType.MOUSEUP,
this.handleMouseUp);
}
// Checkbox needs to explicitly listen for click event.
handler.listen(this.getElement(),
goog.events.EventType.CLICK, this.handleClickOrSpace_);
}
// Set aria label.
if (this.label_) {
if (!this.label_.id) {
this.label_.id = this.makeId('lbl');
}
var checkboxElement = this.getElement();
goog.asserts.assert(checkboxElement,
'The checkbox DOM element cannot be null.');
goog.a11y.aria.setState(checkboxElement,
goog.a11y.aria.State.LABELLEDBY,
this.label_.id);
}
};
/**
* Fix for tabindex not being updated so that disabled checkbox is not
* focusable. In particular this fails in Chrome.
* Note: in general tabIndex=-1 will prevent from keyboard focus but enables
* mouse focus, however in this case the control class prevents mouse focus.
* @override
*/
goog.ui.Checkbox.prototype.setEnabled = function(enabled) {
goog.base(this, 'setEnabled', enabled);
var el = this.getElement();
if (el) {
el.tabIndex = this.isEnabled() ? 0 : -1;
}
};
/**
* Handles the click event.
* @param {!goog.events.BrowserEvent} e The event.
* @private
*/
goog.ui.Checkbox.prototype.handleClickOrSpace_ = function(e) {
e.stopPropagation();
var eventType = this.checked_ ? goog.ui.Component.EventType.UNCHECK :
goog.ui.Component.EventType.CHECK;
if (this.isEnabled() && this.dispatchEvent(eventType)) {
e.preventDefault(); // Prevent scrolling in Chrome if SPACE is pressed.
this.toggle();
this.dispatchEvent(goog.ui.Component.EventType.CHANGE);
}
};
/** @override */
goog.ui.Checkbox.prototype.handleKeyEventInternal = function(e) {
if (e.keyCode == goog.events.KeyCodes.SPACE) {
this.handleClickOrSpace_(e);
}
return false;
};
/**
* Register this control so it can be created from markup.
*/
goog.ui.registry.setDecoratorByClassName(
goog.ui.CheckboxRenderer.CSS_CLASS,
function() {
return new goog.ui.Checkbox();
});

View File

@@ -0,0 +1,53 @@
// Copyright 2007 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 menu item class that supports checkbox semantics.
*
* @author attila@google.com (Attila Bodis)
*/
goog.provide('goog.ui.CheckBoxMenuItem');
goog.require('goog.ui.MenuItem');
goog.require('goog.ui.registry');
/**
* Class representing a checkbox menu item. This is just a convenience class
* that extends {@link goog.ui.MenuItem} by making it checkable.
*
* @param {goog.ui.ControlContent} content Text caption or DOM structure to
* display as the content of the item (use to add icons or styling to
* menus).
* @param {*=} opt_model Data/model associated with the menu item.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper used for
* document interactions.
* @constructor
* @extends {goog.ui.MenuItem}
*/
goog.ui.CheckBoxMenuItem = function(content, opt_model, opt_domHelper) {
goog.ui.MenuItem.call(this, content, opt_model, opt_domHelper);
this.setCheckable(true);
};
goog.inherits(goog.ui.CheckBoxMenuItem, goog.ui.MenuItem);
// Register a decorator factory function for goog.ui.CheckBoxMenuItems.
goog.ui.registry.setDecoratorByClassName(
goog.getCssName('goog-checkbox-menuitem'), function() {
// CheckBoxMenuItem defaults to using MenuItemRenderer.
return new goog.ui.CheckBoxMenuItem(null);
});

View File

@@ -0,0 +1,171 @@
// Copyright 2011 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 Default renderer for {@link goog.ui.Checkbox}s.
*
*/
goog.provide('goog.ui.CheckboxRenderer');
goog.require('goog.a11y.aria');
goog.require('goog.a11y.aria.Role');
goog.require('goog.a11y.aria.State');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom.classes');
goog.require('goog.object');
goog.require('goog.ui.ControlRenderer');
/**
* Default renderer for {@link goog.ui.Checkbox}s. Extends the superclass
* to support checkbox states:
* @constructor
* @extends {goog.ui.ControlRenderer}
*/
goog.ui.CheckboxRenderer = function() {
goog.base(this);
};
goog.inherits(goog.ui.CheckboxRenderer, goog.ui.ControlRenderer);
goog.addSingletonGetter(goog.ui.CheckboxRenderer);
/**
* Default CSS class to be applied to the root element of components rendered
* by this renderer.
* @type {string}
*/
goog.ui.CheckboxRenderer.CSS_CLASS = goog.getCssName('goog-checkbox');
/** @override */
goog.ui.CheckboxRenderer.prototype.createDom = function(checkbox) {
var element = checkbox.getDomHelper().createDom(
'span', this.getClassNames(checkbox).join(' '));
var state = checkbox.getChecked();
this.setCheckboxState(element, state);
return element;
};
/** @override */
goog.ui.CheckboxRenderer.prototype.decorate = function(checkbox, element) {
// The superclass implementation takes care of common attributes; we only
// need to set the checkbox state.
element = goog.base(this, 'decorate', checkbox, element);
var classes = goog.dom.classes.get(element);
// Update the checked state of the element based on its css classNames
// with the following order: undetermined -> checked -> unchecked.
var checked = goog.ui.Checkbox.State.UNCHECKED;
if (goog.array.contains(classes,
this.getClassForCheckboxState(goog.ui.Checkbox.State.UNDETERMINED))) {
checked = goog.ui.Checkbox.State.UNDETERMINED;
} else if (goog.array.contains(classes,
this.getClassForCheckboxState(goog.ui.Checkbox.State.CHECKED))) {
checked = goog.ui.Checkbox.State.CHECKED;
} else if (goog.array.contains(classes,
this.getClassForCheckboxState(goog.ui.Checkbox.State.UNCHECKED))) {
checked = goog.ui.Checkbox.State.UNCHECKED;
}
checkbox.setCheckedInternal(checked);
goog.asserts.assert(element, 'The element cannot be null.');
goog.a11y.aria.setState(element, goog.a11y.aria.State.CHECKED,
this.ariaStateFromCheckState_(checked));
return element;
};
/**
* Returns the ARIA role to be applied to checkboxes.
* @return {goog.a11y.aria.Role} ARIA role.
* @override
*/
goog.ui.CheckboxRenderer.prototype.getAriaRole = function() {
return goog.a11y.aria.Role.CHECKBOX;
};
/**
* Updates the appearance of the control in response to a checkbox state
* change.
* @param {Element} element Checkbox element.
* @param {goog.ui.Checkbox.State} state Updated checkbox state.
*/
goog.ui.CheckboxRenderer.prototype.setCheckboxState = function(
element, state) {
if (element) {
var classToAdd = this.getClassForCheckboxState(state);
goog.asserts.assert(classToAdd);
if (goog.dom.classes.has(element, classToAdd)) {
return;
}
goog.object.forEach(goog.ui.Checkbox.State, function(state) {
var className = this.getClassForCheckboxState(state);
goog.dom.classes.enable(element, className,
className == classToAdd);
}, this);
goog.a11y.aria.setState(element, goog.a11y.aria.State.CHECKED,
this.ariaStateFromCheckState_(state));
}
};
/**
* Gets the checkbox's ARIA (accessibility) state from its checked state.
* @param {goog.ui.Checkbox.State} state Checkbox state.
* @return {string} The value of goog.a11y.aria.state.CHECKED. Either 'true',
* 'false', or 'mixed'.
* @private
*/
goog.ui.CheckboxRenderer.prototype.ariaStateFromCheckState_ = function(state) {
if (state == goog.ui.Checkbox.State.UNDETERMINED) {
return 'mixed';
} else if (state == goog.ui.Checkbox.State.CHECKED) {
return 'true';
} else {
return 'false';
}
};
/** @override */
goog.ui.CheckboxRenderer.prototype.getCssClass = function() {
return goog.ui.CheckboxRenderer.CSS_CLASS;
};
/**
* Takes a single {@link goog.ui.Checkbox.State}, and returns the
* corresponding CSS class name.
* @param {goog.ui.Checkbox.State} state Checkbox state.
* @return {string} CSS class representing the given state.
* @protected
*/
goog.ui.CheckboxRenderer.prototype.getClassForCheckboxState = function(state) {
var baseClass = this.getStructuralCssClass();
if (state == goog.ui.Checkbox.State.CHECKED) {
return goog.getCssName(baseClass, 'checked');
} else if (state == goog.ui.Checkbox.State.UNCHECKED) {
return goog.getCssName(baseClass, 'unchecked');
} else if (state == goog.ui.Checkbox.State.UNDETERMINED) {
return goog.getCssName(baseClass, 'undetermined');
}
throw Error('Invalid checkbox state: ' + state);
};

View File

@@ -0,0 +1,58 @@
// 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 A color button rendered via
* {@link goog.ui.ColorButtonRenderer}.
*
*/
goog.provide('goog.ui.ColorButton');
goog.require('goog.ui.Button');
goog.require('goog.ui.ColorButtonRenderer');
goog.require('goog.ui.registry');
/**
* A color button control. Identical to {@link goog.ui.Button}, except it
* defaults its renderer to {@link goog.ui.ColorButtonRenderer}.
* This button displays a particular color and is clickable.
* It is primarily useful with {@link goog.ui.ColorSplitBehavior} to cache the
* last selected color.
*
* @param {goog.ui.ControlContent} content Text caption or existing DOM
* structure to display as the button's caption.
* @param {goog.ui.ButtonRenderer=} opt_renderer Optional renderer used to
* render or decorate the button; defaults to
* {@link goog.ui.ColorButtonRenderer}.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for
* document interaction.
* @constructor
* @extends {goog.ui.Button}
*/
goog.ui.ColorButton = function(content, opt_renderer, opt_domHelper) {
goog.ui.Button.call(this, content, opt_renderer ||
goog.ui.ColorButtonRenderer.getInstance(), opt_domHelper);
};
goog.inherits(goog.ui.ColorButton, goog.ui.Button);
// Register a decorator factory function for goog.ui.ColorButtons.
goog.ui.registry.setDecoratorByClassName(goog.ui.ColorButtonRenderer.CSS_CLASS,
function() {
// ColorButton defaults to using ColorButtonRenderer.
return new goog.ui.ColorButton(null);
});

View File

@@ -0,0 +1,72 @@
// 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 Renderer for {@link goog.ui.ColorButton}s.
*
*/
goog.provide('goog.ui.ColorButtonRenderer');
goog.require('goog.dom.classes');
goog.require('goog.functions');
goog.require('goog.ui.ColorMenuButtonRenderer');
/**
* Renderer for {@link goog.ui.ColorButton}s.
* Uses {@link goog.ui.ColorMenuButton}s but disables the dropdown.
*
* @constructor
* @extends {goog.ui.ColorMenuButtonRenderer}
*/
goog.ui.ColorButtonRenderer = function() {
goog.base(this);
/**
* @override
*/
// TODO(user): enable disabling the dropdown in goog.ui.ColorMenuButton
this.createDropdown = goog.functions.NULL;
};
goog.inherits(goog.ui.ColorButtonRenderer, goog.ui.ColorMenuButtonRenderer);
goog.addSingletonGetter(goog.ui.ColorButtonRenderer);
/**
* Default CSS class to be applied to the root element of components rendered
* by this renderer. Additionally, applies class to the button's caption.
* @type {string}
*/
goog.ui.ColorButtonRenderer.CSS_CLASS = goog.getCssName('goog-color-button');
/** @override */
goog.ui.ColorButtonRenderer.prototype.createCaption = function(content, dom) {
var caption = goog.base(this, 'createCaption', content, dom);
goog.dom.classes.add(caption, goog.ui.ColorButtonRenderer.CSS_CLASS);
return caption;
};
/** @override */
goog.ui.ColorButtonRenderer.prototype.initializeDom = function(button) {
goog.base(this, 'initializeDom', button);
goog.dom.classes.add(button.getElement(),
goog.ui.ColorButtonRenderer.CSS_CLASS);
};

View File

@@ -0,0 +1,215 @@
// 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 color menu button. Extends {@link goog.ui.MenuButton} by
* showing the currently selected color in the button caption.
*
* @author robbyw@google.com (Robby Walker)
* @author attila@google.com (Attila Bodis)
*/
goog.provide('goog.ui.ColorMenuButton');
goog.require('goog.array');
goog.require('goog.object');
goog.require('goog.ui.ColorMenuButtonRenderer');
goog.require('goog.ui.ColorPalette');
goog.require('goog.ui.Component');
goog.require('goog.ui.Menu');
goog.require('goog.ui.MenuButton');
goog.require('goog.ui.registry');
/**
* A color menu button control. Extends {@link goog.ui.MenuButton} by adding
* an API for getting and setting the currently selected color from a menu of
* color palettes.
*
* @param {goog.ui.ControlContent} content Text caption or existing DOM
* structure to display as the button's caption.
* @param {goog.ui.Menu=} opt_menu Menu to render under the button when clicked;
* should contain at least one {@link goog.ui.ColorPalette} if present.
* @param {goog.ui.MenuButtonRenderer=} opt_renderer Button renderer;
* defaults to {@link goog.ui.ColorMenuButtonRenderer}.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM hepler, used for
* document interaction.
* @constructor
* @extends {goog.ui.MenuButton}
*/
goog.ui.ColorMenuButton = function(content, opt_menu, opt_renderer,
opt_domHelper) {
goog.ui.MenuButton.call(this, content, opt_menu, opt_renderer ||
goog.ui.ColorMenuButtonRenderer.getInstance(), opt_domHelper);
};
goog.inherits(goog.ui.ColorMenuButton, goog.ui.MenuButton);
/**
* Default color palettes.
* @type {!Object}
*/
goog.ui.ColorMenuButton.PALETTES = {
/** Default grayscale colors. */
GRAYSCALE: [
'#000', '#444', '#666', '#999', '#ccc', '#eee', '#f3f3f3', '#fff'
],
/** Default solid colors. */
SOLID: [
'#f00', '#f90', '#ff0', '#0f0', '#0ff', '#00f', '#90f', '#f0f'
],
/** Default pastel colors. */
PASTEL: [
'#f4cccc', '#fce5cd', '#fff2cc', '#d9ead3', '#d0e0e3', '#cfe2f3', '#d9d2e9',
'#ead1dc',
'#ea9999', '#f9cb9c', '#ffe599', '#b6d7a8', '#a2c4c9', '#9fc5e8', '#b4a7d6',
'#d5a6bd',
'#e06666', '#f6b26b', '#ffd966', '#93c47d', '#76a5af', '#6fa8dc', '#8e7cc3',
'#c27ba0',
'#cc0000', '#e69138', '#f1c232', '#6aa84f', '#45818e', '#3d85c6', '#674ea7',
'#a64d79',
'#990000', '#b45f06', '#bf9000', '#38761d', '#134f5c', '#0b5394', '#351c75',
'#741b47',
'#660000', '#783f04', '#7f6000', '#274e13', '#0c343d', '#073763', '#20124d',
'#4c1130'
]
};
/**
* Value for the "no color" menu item object in the color menu (if present).
* The {@link goog.ui.ColorMenuButton#handleMenuAction} method interprets
* ACTION events dispatched by an item with this value as meaning "clear the
* selected color."
* @type {string}
*/
goog.ui.ColorMenuButton.NO_COLOR = 'none';
/**
* Factory method that creates and returns a new {@link goog.ui.Menu} instance
* containing default color palettes.
* @param {Array.<goog.ui.Control>=} opt_extraItems Optional extra menu items to
* add before the color palettes.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM hepler, used for
* document interaction.
* @return {goog.ui.Menu} Color menu.
*/
goog.ui.ColorMenuButton.newColorMenu = function(opt_extraItems, opt_domHelper) {
var menu = new goog.ui.Menu(opt_domHelper);
if (opt_extraItems) {
goog.array.forEach(opt_extraItems, function(item) {
menu.addChild(item, true);
});
}
goog.object.forEach(goog.ui.ColorMenuButton.PALETTES, function(colors) {
var palette = new goog.ui.ColorPalette(colors, null, opt_domHelper);
palette.setSize(8);
menu.addChild(palette, true);
});
return menu;
};
/**
* Returns the currently selected color (null if none).
* @return {?string} The selected color.
*/
goog.ui.ColorMenuButton.prototype.getSelectedColor = function() {
return /** @type {string} */ (this.getValue());
};
/**
* Sets the selected color, or clears the selected color if the argument is
* null or not any of the available color choices.
* @param {?string} color New color.
*/
goog.ui.ColorMenuButton.prototype.setSelectedColor = function(color) {
this.setValue(color);
};
/**
* Sets the value associated with the color menu button. Overrides
* {@link goog.ui.Button#setValue} by interpreting the value as a color
* spec string.
* @param {*} value New button value; should be a color spec string.
* @override
*/
goog.ui.ColorMenuButton.prototype.setValue = function(value) {
var color = /** @type {?string} */ (value);
for (var i = 0, item; item = this.getItemAt(i); i++) {
if (typeof item.setSelectedColor == 'function') {
// This menu item looks like a color palette.
item.setSelectedColor(color);
}
}
goog.ui.ColorMenuButton.superClass_.setValue.call(this, color);
};
/**
* Handles {@link goog.ui.Component.EventType.ACTION} events dispatched by
* the menu item clicked by the user. Updates the button, calls the superclass
* implementation to hide the menu, stops the propagation of the event, and
* dispatches an ACTION event on behalf of the button itself. Overrides
* {@link goog.ui.MenuButton#handleMenuAction}.
* @param {goog.events.Event} e Action event to handle.
* @override
*/
goog.ui.ColorMenuButton.prototype.handleMenuAction = function(e) {
if (typeof e.target.getSelectedColor == 'function') {
// User clicked something that looks like a color palette.
this.setValue(e.target.getSelectedColor());
} else if (e.target.getValue() == goog.ui.ColorMenuButton.NO_COLOR) {
// User clicked the special "no color" menu item.
this.setValue(null);
}
goog.ui.ColorMenuButton.superClass_.handleMenuAction.call(this, e);
e.stopPropagation();
this.dispatchEvent(goog.ui.Component.EventType.ACTION);
};
/**
* Opens or closes the menu. Overrides {@link goog.ui.MenuButton#setOpen} by
* generating a default color menu on the fly if needed.
* @param {boolean} open Whether to open or close the menu.
* @param {goog.events.Event=} opt_e Mousedown event that caused the menu to
* be opened.
* @override
*/
goog.ui.ColorMenuButton.prototype.setOpen = function(open, opt_e) {
if (open && this.getItemCount() == 0) {
this.setMenu(
goog.ui.ColorMenuButton.newColorMenu(null, this.getDomHelper()));
this.setValue(/** @type {?string} */ (this.getValue()));
}
goog.ui.ColorMenuButton.superClass_.setOpen.call(this, open, opt_e);
};
// Register a decorator factory function for goog.ui.ColorMenuButtons.
goog.ui.registry.setDecoratorByClassName(
goog.ui.ColorMenuButtonRenderer.CSS_CLASS,
function() {
return new goog.ui.ColorMenuButton(null);
});

View File

@@ -0,0 +1,144 @@
// 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 Renderer for {@link goog.ui.ColorMenuButton}s.
*
* @author robbyw@google.com (Robby Walker)
* @author attila@google.com (Attila Bodis)
*/
goog.provide('goog.ui.ColorMenuButtonRenderer');
goog.require('goog.color');
goog.require('goog.dom.classes');
goog.require('goog.ui.MenuButtonRenderer');
goog.require('goog.userAgent');
/**
* Renderer for {@link goog.ui.ColorMenuButton}s.
* @constructor
* @extends {goog.ui.MenuButtonRenderer}
*/
goog.ui.ColorMenuButtonRenderer = function() {
goog.ui.MenuButtonRenderer.call(this);
};
goog.inherits(goog.ui.ColorMenuButtonRenderer, goog.ui.MenuButtonRenderer);
goog.addSingletonGetter(goog.ui.ColorMenuButtonRenderer);
/**
* Default CSS class to be applied to the root element of components rendered
* by this renderer.
* @type {string}
*/
goog.ui.ColorMenuButtonRenderer.CSS_CLASS =
goog.getCssName('goog-color-menu-button');
/**
* Overrides the superclass implementation by wrapping the caption text or DOM
* structure in a color indicator element. Creates the following DOM structure:
* <div class="goog-inline-block goog-menu-button-caption">
* <div class="goog-color-menu-button-indicator">
* Contents...
* </div>
* </div>
* The 'goog-color-menu-button-indicator' style should be defined to have a
* bottom border of nonzero width and a default color that blends into its
* background.
* @param {goog.ui.ControlContent} content Text caption or DOM structure.
* @param {goog.dom.DomHelper} dom DOM helper, used for document interaction.
* @return {Element} Caption element.
* @override
*/
goog.ui.ColorMenuButtonRenderer.prototype.createCaption = function(content,
dom) {
return goog.ui.ColorMenuButtonRenderer.superClass_.createCaption.call(this,
goog.ui.ColorMenuButtonRenderer.wrapCaption(content, dom), dom);
};
/**
* Wrap a caption in a div with the color-menu-button-indicator CSS class.
* @param {goog.ui.ControlContent} content Text caption or DOM structure.
* @param {goog.dom.DomHelper} dom DOM helper, used for document interaction.
* @return {Element} Caption element.
*/
goog.ui.ColorMenuButtonRenderer.wrapCaption = function(content, dom) {
return dom.createDom('div',
goog.getCssName(goog.ui.ColorMenuButtonRenderer.CSS_CLASS, 'indicator'),
content);
};
/**
* Takes a color menu button control's root element and a value object
* (which is assumed to be a color), and updates the button's DOM to reflect
* the new color. Overrides {@link goog.ui.ButtonRenderer#setValue}.
* @param {Element} element The button control's root element (if rendered).
* @param {*} value New value; assumed to be a color spec string.
* @override
*/
goog.ui.ColorMenuButtonRenderer.prototype.setValue = function(element, value) {
if (element) {
goog.ui.ColorMenuButtonRenderer.setCaptionValue(
this.getContentElement(element), value);
}
};
/**
* Takes a control's content element and a value object (which is assumed
* to be a color), and updates its DOM to reflect the new color.
* @param {Element} caption A content element of a control.
* @param {*} value New value; assumed to be a color spec string.
*/
goog.ui.ColorMenuButtonRenderer.setCaptionValue = function(caption, value) {
// Assume that the caption's first child is the indicator.
if (caption && caption.firstChild) {
// Normalize the value to a hex color spec or null (otherwise setting
// borderBottomColor will cause a JS error on IE).
var hexColor;
var strValue = /** @type {string} */ (value);
hexColor = strValue && goog.color.isValidColor(strValue) ?
goog.color.parse(strValue).hex :
null;
// Stupid IE6/7 doesn't do transparent borders.
// TODO(attila): Add user-agent version check when IE8 comes out...
caption.firstChild.style.borderBottomColor = hexColor ||
(goog.userAgent.IE ? '' : 'transparent');
}
};
/**
* Initializes the button's DOM when it enters the document. Overrides the
* superclass implementation by making sure the button's color indicator is
* initialized.
* @param {goog.ui.Control} button goog.ui.ColorMenuButton whose DOM is to be
* initialized as it enters the document.
* @override
*/
goog.ui.ColorMenuButtonRenderer.prototype.initializeDom = function(button) {
this.setValue(button.getElement(), button.getValue());
goog.dom.classes.add(button.getElement(),
goog.ui.ColorMenuButtonRenderer.CSS_CLASS);
goog.ui.ColorMenuButtonRenderer.superClass_.initializeDom.call(this,
button);
};

View File

@@ -0,0 +1,177 @@
// Copyright 2007 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 control for representing a palette of colors, that the user
* can highlight or select via the keyboard or the mouse.
*
*/
goog.provide('goog.ui.ColorPalette');
goog.require('goog.array');
goog.require('goog.color');
goog.require('goog.style');
goog.require('goog.ui.Palette');
goog.require('goog.ui.PaletteRenderer');
/**
* A color palette is a grid of color swatches that the user can highlight or
* select via the keyboard or the mouse. The selection state of the palette is
* controlled by a selection model. When the user makes a selection, the
* component fires an ACTION event. Event listeners may retrieve the selected
* color using the {@link #getSelectedColor} method.
*
* @param {Array.<string>=} opt_colors Array of colors in any valid CSS color
* format.
* @param {goog.ui.PaletteRenderer=} opt_renderer Renderer used to render or
* decorate the palette; defaults to {@link goog.ui.PaletteRenderer}.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for
* document interaction.
* @constructor
* @extends {goog.ui.Palette}
*/
goog.ui.ColorPalette = function(opt_colors, opt_renderer, opt_domHelper) {
/**
* Array of colors to show in the palette.
* @type {Array.<string>}
* @private
*/
this.colors_ = opt_colors || [];
goog.ui.Palette.call(this, null,
opt_renderer || goog.ui.PaletteRenderer.getInstance(), opt_domHelper);
// Set the colors separately from the super call since we need the correct
// DomHelper to be initialized for this class.
this.setColors(this.colors_);
};
goog.inherits(goog.ui.ColorPalette, goog.ui.Palette);
/**
* Array of normalized colors. Initialized lazily as often never needed.
* @type {?Array.<string>}
* @private
*/
goog.ui.ColorPalette.prototype.normalizedColors_ = null;
/**
* Array of labels for the colors. Will be used for the tooltips and
* accessibility.
* @type {?Array.<string>}
* @private
*/
goog.ui.ColorPalette.prototype.labels_ = null;
/**
* Returns the array of colors represented in the color palette.
* @return {Array.<string>} Array of colors.
*/
goog.ui.ColorPalette.prototype.getColors = function() {
return this.colors_;
};
/**
* Sets the colors that are contained in the palette.
* @param {Array.<string>} colors Array of colors in any valid CSS color format.
* @param {Array.<string>=} opt_labels The array of labels to be used as
* tooltips. When not provided, the color value will be used.
*/
goog.ui.ColorPalette.prototype.setColors = function(colors, opt_labels) {
this.colors_ = colors;
this.labels_ = opt_labels || null;
this.normalizedColors_ = null;
this.setContent(this.createColorNodes());
};
/**
* @return {?string} The current selected color in hex, or null.
*/
goog.ui.ColorPalette.prototype.getSelectedColor = function() {
var selectedItem = /** @type {Element} */ (this.getSelectedItem());
if (selectedItem) {
var color = goog.style.getStyle(selectedItem, 'background-color');
return goog.ui.ColorPalette.parseColor_(color);
} else {
return null;
}
};
/**
* Sets the selected color. Clears the selection if the argument is null or
* can't be parsed as a color.
* @param {?string} color The color to set as selected; null clears the
* selection.
*/
goog.ui.ColorPalette.prototype.setSelectedColor = function(color) {
var hexColor = goog.ui.ColorPalette.parseColor_(color);
if (!this.normalizedColors_) {
this.normalizedColors_ = goog.array.map(this.colors_, function(color) {
return goog.ui.ColorPalette.parseColor_(color);
});
}
this.setSelectedIndex(hexColor ?
goog.array.indexOf(this.normalizedColors_, hexColor) : -1);
};
/**
* @return {Array.<Node>} An array of DOM nodes for each color.
* @protected
*/
goog.ui.ColorPalette.prototype.createColorNodes = function() {
return goog.array.map(this.colors_, function(color, index) {
var swatch = this.getDomHelper().createDom('div', {
'class': goog.getCssName(this.getRenderer().getCssClass(),
'colorswatch'),
'style': 'background-color:' + color
});
if (this.labels_ && this.labels_[index]) {
swatch.title = this.labels_[index];
} else {
swatch.title = color.charAt(0) == '#' ?
'RGB (' + goog.color.hexToRgb(color).join(', ') + ')' : color;
}
return swatch;
}, this);
};
/**
* Takes a string, attempts to parse it as a color spec, and returns a
* normalized hex color spec if successful (null otherwise).
* @param {?string} color String possibly containing a color spec; may be null.
* @return {?string} Normalized hex color spec, or null if the argument can't
* be parsed as a color.
* @private
*/
goog.ui.ColorPalette.parseColor_ = function(color) {
if (color) {
/** @preserveTry */
try {
return goog.color.parse(color).hex;
} catch (ex) {
// Fall through.
}
}
return null;
};

View File

@@ -0,0 +1,344 @@
// Copyright 2007 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 color picker component. A color picker can compose several
* instances of goog.ui.ColorPalette.
*
* NOTE: The ColorPicker is in a state of transition towards the common
* component/control/container interface we are developing. If the API changes
* we will do our best to update your code. The end result will be that a
* color picker will compose multiple color palettes. In the simple case this
* will be one grid, but may consistute 3 distinct grids, a custom color picker
* or even a color wheel.
*
*/
goog.provide('goog.ui.ColorPicker');
goog.provide('goog.ui.ColorPicker.EventType');
goog.require('goog.ui.ColorPalette');
goog.require('goog.ui.Component');
/**
* Create a new, empty color picker.
*
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @param {goog.ui.ColorPalette=} opt_colorPalette Optional color palette to
* use for this color picker.
* @extends {goog.ui.Component}
* @constructor
*/
goog.ui.ColorPicker = function(opt_domHelper, opt_colorPalette) {
goog.ui.Component.call(this, opt_domHelper);
/**
* The color palette used inside the color picker.
* @type {goog.ui.ColorPalette?}
* @private
*/
this.colorPalette_ = opt_colorPalette || null;
this.getHandler().listen(
this, goog.ui.Component.EventType.ACTION, this.onColorPaletteAction_);
};
goog.inherits(goog.ui.ColorPicker, goog.ui.Component);
/**
* Default number of columns in the color palette. May be overridden by calling
* setSize.
*
* @type {number}
*/
goog.ui.ColorPicker.DEFAULT_NUM_COLS = 5;
/**
* Constants for event names.
* @enum {string}
*/
goog.ui.ColorPicker.EventType = {
CHANGE: 'change'
};
/**
* Whether the component is focusable.
* @type {boolean}
* @private
*/
goog.ui.ColorPicker.prototype.focusable_ = true;
/**
* Gets the array of colors displayed by the color picker.
* Modifying this array will lead to unexpected behavior.
* @return {Array.<string>?} The colors displayed by this widget.
*/
goog.ui.ColorPicker.prototype.getColors = function() {
return this.colorPalette_ ? this.colorPalette_.getColors() : null;
};
/**
* Sets the array of colors to be displayed by the color picker.
* @param {Array.<string>} colors The array of colors to be added.
*/
goog.ui.ColorPicker.prototype.setColors = function(colors) {
// TODO(user): Don't add colors directly, we should add palettes and the
// picker should support multiple palettes.
if (!this.colorPalette_) {
this.createColorPalette_(colors);
} else {
this.colorPalette_.setColors(colors);
}
};
/**
* Sets the array of colors to be displayed by the color picker.
* @param {Array.<string>} colors The array of colors to be added.
* @deprecated Use setColors.
*/
goog.ui.ColorPicker.prototype.addColors = function(colors) {
this.setColors(colors);
};
/**
* Sets the size of the palette. Will throw an error after the picker has been
* rendered.
* @param {goog.math.Size|number} size The size of the grid.
*/
goog.ui.ColorPicker.prototype.setSize = function(size) {
// TODO(user): The color picker should contain multiple palettes which will
// all be resized at this point.
if (!this.colorPalette_) {
this.createColorPalette_([]);
}
this.colorPalette_.setSize(size);
};
/**
* Gets the number of columns displayed.
* @return {goog.math.Size?} The size of the grid.
*/
goog.ui.ColorPicker.prototype.getSize = function() {
return this.colorPalette_ ? this.colorPalette_.getSize() : null;
};
/**
* Sets the number of columns. Will throw an error after the picker has been
* rendered.
* @param {number} n The number of columns.
* @deprecated Use setSize.
*/
goog.ui.ColorPicker.prototype.setColumnCount = function(n) {
this.setSize(n);
};
/**
* @return {number} The index of the color selected.
*/
goog.ui.ColorPicker.prototype.getSelectedIndex = function() {
return this.colorPalette_ ? this.colorPalette_.getSelectedIndex() : -1;
};
/**
* Sets which color is selected. A value that is out-of-range means that no
* color is selected.
* @param {number} ind The index in this.colors_ of the selected color.
*/
goog.ui.ColorPicker.prototype.setSelectedIndex = function(ind) {
if (this.colorPalette_) {
this.colorPalette_.setSelectedIndex(ind);
}
};
/**
* Gets the color that is currently selected in this color picker.
* @return {?string} The hex string of the color selected, or null if no
* color is selected.
*/
goog.ui.ColorPicker.prototype.getSelectedColor = function() {
return this.colorPalette_ ? this.colorPalette_.getSelectedColor() : null;
};
/**
* Sets which color is selected. Noop if the color palette hasn't been created
* yet.
* @param {string} color The selected color.
*/
goog.ui.ColorPicker.prototype.setSelectedColor = function(color) {
// TODO(user): This will set the color in the first available palette that
// contains it
if (this.colorPalette_) {
this.colorPalette_.setSelectedColor(color);
}
};
/**
* Returns true if the component is focusable, false otherwise. The default
* is true. Focusable components always have a tab index and allocate a key
* handler to handle keyboard events while focused.
* @return {boolean} True iff the component is focusable.
*/
goog.ui.ColorPicker.prototype.isFocusable = function() {
return this.focusable_;
};
/**
* Sets whether the component is focusable. The default is true.
* Focusable components always have a tab index and allocate a key handler to
* handle keyboard events while focused.
* @param {boolean} focusable True iff the component is focusable.
*/
goog.ui.ColorPicker.prototype.setFocusable = function(focusable) {
this.focusable_ = focusable;
if (this.colorPalette_) {
this.colorPalette_.setSupportedState(goog.ui.Component.State.FOCUSED,
focusable);
}
};
/**
* ColorPickers cannot be used to decorate pre-existing html, since the
* structure they build is fairly complicated.
* @param {Element} element Element to decorate.
* @return {boolean} Returns always false.
* @override
*/
goog.ui.ColorPicker.prototype.canDecorate = function(element) {
return false;
};
/**
* Renders the color picker inside the provided element. This will override the
* current content of the element.
* @override
*/
goog.ui.ColorPicker.prototype.enterDocument = function() {
goog.ui.ColorPicker.superClass_.enterDocument.call(this);
if (this.colorPalette_) {
this.colorPalette_.render(this.getElement());
}
this.getElement().unselectable = 'on';
};
/** @override */
goog.ui.ColorPicker.prototype.disposeInternal = function() {
goog.ui.ColorPicker.superClass_.disposeInternal.call(this);
if (this.colorPalette_) {
this.colorPalette_.dispose();
this.colorPalette_ = null;
}
};
/**
* Sets the focus to the color picker's palette.
*/
goog.ui.ColorPicker.prototype.focus = function() {
if (this.colorPalette_) {
this.colorPalette_.getElement().focus();
}
};
/**
* Handles actions from the color palette.
*
* @param {goog.events.Event} e The event.
* @private
*/
goog.ui.ColorPicker.prototype.onColorPaletteAction_ = function(e) {
e.stopPropagation();
this.dispatchEvent(goog.ui.ColorPicker.EventType.CHANGE);
};
/**
* Create a color palette for the color picker.
* @param {Array.<string>} colors Array of colors.
* @private
*/
goog.ui.ColorPicker.prototype.createColorPalette_ = function(colors) {
// TODO(user): The color picker should eventually just contain a number of
// palettes and manage the interactions between them. This will go away then.
var cp = new goog.ui.ColorPalette(colors, null, this.getDomHelper());
cp.setSize(goog.ui.ColorPicker.DEFAULT_NUM_COLS);
cp.setSupportedState(goog.ui.Component.State.FOCUSED, this.focusable_);
// TODO(user): Use addChild(cp, true) and remove calls to render.
this.addChild(cp);
this.colorPalette_ = cp;
if (this.isInDocument()) {
this.colorPalette_.render(this.getElement());
}
};
/**
* Returns an unrendered instance of the color picker. The colors and layout
* are a simple color grid, the same as the old Gmail color picker.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @return {goog.ui.ColorPicker} The unrendered instance.
*/
goog.ui.ColorPicker.createSimpleColorGrid = function(opt_domHelper) {
var cp = new goog.ui.ColorPicker(opt_domHelper);
cp.setSize(7);
cp.setColors(goog.ui.ColorPicker.SIMPLE_GRID_COLORS);
return cp;
};
/**
* Array of colors for a 7-cell wide simple-grid color picker.
* @type {Array.<string>}
*/
goog.ui.ColorPicker.SIMPLE_GRID_COLORS = [
// grays
'#ffffff', '#cccccc', '#c0c0c0', '#999999', '#666666', '#333333', '#000000',
// reds
'#ffcccc', '#ff6666', '#ff0000', '#cc0000', '#990000', '#660000', '#330000',
// oranges
'#ffcc99', '#ff9966', '#ff9900', '#ff6600', '#cc6600', '#993300', '#663300',
// yellows
'#ffff99', '#ffff66', '#ffcc66', '#ffcc33', '#cc9933', '#996633', '#663333',
// olives
'#ffffcc', '#ffff33', '#ffff00', '#ffcc00', '#999900', '#666600', '#333300',
// greens
'#99ff99', '#66ff99', '#33ff33', '#33cc00', '#009900', '#006600', '#003300',
// turquoises
'#99ffff', '#33ffff', '#66cccc', '#00cccc', '#339999', '#336666', '#003333',
// blues
'#ccffff', '#66ffff', '#33ccff', '#3366ff', '#3333ff', '#000099', '#000066',
// purples
'#ccccff', '#9999ff', '#6666cc', '#6633ff', '#6600cc', '#333399', '#330099',
// violets
'#ffccff', '#ff99ff', '#cc66cc', '#cc33cc', '#993399', '#663366', '#330033'
];

View File

@@ -0,0 +1,60 @@
// 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 Behavior for combining a color button and a menu.
*
* @see ../demos/split.html
*/
goog.provide('goog.ui.ColorSplitBehavior');
goog.require('goog.ui.ColorMenuButton');
goog.require('goog.ui.SplitBehavior');
/**
* Constructs a ColorSplitBehavior for combining a color button and a menu.
* To use this, provide a goog.ui.ColorButton which will be attached with
* a goog.ui.ColorMenuButton (with no caption).
* Whenever a color is selected from the ColorMenuButton, it will be placed in
* the ColorButton and the user can apply it over and over (by clicking the
* ColorButton).
* Primary use case - setting the color of text/background in a text editor.
*
* @param {!goog.ui.Button} colorButton A button to interact with a color menu
* button (preferably a goog.ui.ColorButton).
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for
* document interaction.
* @extends {goog.ui.SplitBehavior}
* @constructor
*/
goog.ui.ColorSplitBehavior = function(colorButton, opt_domHelper) {
goog.base(this, colorButton,
new goog.ui.ColorMenuButton(goog.ui.ColorSplitBehavior.ZERO_WIDTH_SPACE_),
goog.ui.SplitBehavior.DefaultHandlers.VALUE,
undefined,
opt_domHelper);
};
goog.inherits(goog.ui.ColorSplitBehavior, goog.ui.SplitBehavior);
/**
* A zero width space character.
* @type {string}
* @private
*/
goog.ui.ColorSplitBehavior.ZERO_WIDTH_SPACE_ = '\uFEFF';

View File

@@ -0,0 +1,961 @@
// Copyright 2007 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 combo box control that allows user input with
* auto-suggestion from a limited set of options.
*
* @see ../demos/combobox.html
*/
goog.provide('goog.ui.ComboBox');
goog.provide('goog.ui.ComboBoxItem');
goog.require('goog.Timer');
goog.require('goog.dom');
goog.require('goog.dom.classlist');
goog.require('goog.events.EventType');
goog.require('goog.events.InputHandler');
goog.require('goog.events.KeyCodes');
goog.require('goog.events.KeyHandler');
goog.require('goog.log');
goog.require('goog.positioning.Corner');
goog.require('goog.positioning.MenuAnchoredPosition');
goog.require('goog.string');
goog.require('goog.style');
goog.require('goog.ui.Component');
goog.require('goog.ui.ItemEvent');
goog.require('goog.ui.LabelInput');
goog.require('goog.ui.Menu');
goog.require('goog.ui.MenuItem');
goog.require('goog.ui.MenuSeparator');
goog.require('goog.ui.registry');
goog.require('goog.userAgent');
/**
* A ComboBox control.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @param {goog.ui.Menu=} opt_menu Optional menu.
* @extends {goog.ui.Component}
* @constructor
*/
goog.ui.ComboBox = function(opt_domHelper, opt_menu) {
goog.ui.Component.call(this, opt_domHelper);
this.labelInput_ = new goog.ui.LabelInput();
this.enabled_ = true;
// TODO(user): Allow lazy creation of menus/menu items
this.menu_ = opt_menu || new goog.ui.Menu(this.getDomHelper());
this.setupMenu_();
};
goog.inherits(goog.ui.ComboBox, goog.ui.Component);
/**
* Number of milliseconds to wait before dismissing combobox after blur.
* @type {number}
*/
goog.ui.ComboBox.BLUR_DISMISS_TIMER_MS = 250;
/**
* A logger to help debugging of combo box behavior.
* @type {goog.log.Logger}
* @private
*/
goog.ui.ComboBox.prototype.logger_ =
goog.log.getLogger('goog.ui.ComboBox');
/**
* Whether the combo box is enabled.
* @type {boolean}
* @private
*/
goog.ui.ComboBox.prototype.enabled_;
/**
* Keyboard event handler to manage key events dispatched by the input element.
* @type {goog.events.KeyHandler}
* @private
*/
goog.ui.ComboBox.prototype.keyHandler_;
/**
* Input handler to take care of firing events when the user inputs text in
* the input.
* @type {goog.events.InputHandler?}
* @private
*/
goog.ui.ComboBox.prototype.inputHandler_ = null;
/**
* The last input token.
* @type {?string}
* @private
*/
goog.ui.ComboBox.prototype.lastToken_ = null;
/**
* A LabelInput control that manages the focus/blur state of the input box.
* @type {goog.ui.LabelInput?}
* @private
*/
goog.ui.ComboBox.prototype.labelInput_ = null;
/**
* Drop down menu for the combo box. Will be created at construction time.
* @type {goog.ui.Menu?}
* @private
*/
goog.ui.ComboBox.prototype.menu_ = null;
/**
* The cached visible count.
* @type {number}
* @private
*/
goog.ui.ComboBox.prototype.visibleCount_ = -1;
/**
* The input element.
* @type {Element}
* @private
*/
goog.ui.ComboBox.prototype.input_ = null;
/**
* The match function. The first argument for the match function will be
* a MenuItem's caption and the second will be the token to evaluate.
* @type {Function}
* @private
*/
goog.ui.ComboBox.prototype.matchFunction_ = goog.string.startsWith;
/**
* Element used as the combo boxes button.
* @type {Element}
* @private
*/
goog.ui.ComboBox.prototype.button_ = null;
/**
* Default text content for the input box when it is unchanged and unfocussed.
* @type {string}
* @private
*/
goog.ui.ComboBox.prototype.defaultText_ = '';
/**
* Name for the input box created
* @type {string}
* @private
*/
goog.ui.ComboBox.prototype.fieldName_ = '';
/**
* Timer identifier for delaying the dismissal of the combo menu.
* @type {?number}
* @private
*/
goog.ui.ComboBox.prototype.dismissTimer_ = null;
/**
* True if the unicode inverted triangle should be displayed in the dropdown
* button. Defaults to false.
* @type {boolean} useDropdownArrow
* @private
*/
goog.ui.ComboBox.prototype.useDropdownArrow_ = false;
/**
* Create the DOM objects needed for the combo box. A span and text input.
* @override
*/
goog.ui.ComboBox.prototype.createDom = function() {
this.input_ = this.getDomHelper().createDom(
'input', {name: this.fieldName_, type: 'text', autocomplete: 'off'});
this.button_ = this.getDomHelper().createDom('span',
goog.getCssName('goog-combobox-button'));
this.setElementInternal(this.getDomHelper().createDom('span',
goog.getCssName('goog-combobox'), this.input_, this.button_));
if (this.useDropdownArrow_) {
this.button_.innerHTML = '&#x25BC;';
goog.style.setUnselectable(this.button_, true /* unselectable */);
}
this.input_.setAttribute('label', this.defaultText_);
this.labelInput_.decorate(this.input_);
this.menu_.setFocusable(false);
if (!this.menu_.isInDocument()) {
this.addChild(this.menu_, true);
}
};
/**
* Enables/Disables the combo box.
* @param {boolean} enabled Whether to enable (true) or disable (false) the
* combo box.
*/
goog.ui.ComboBox.prototype.setEnabled = function(enabled) {
this.enabled_ = enabled;
this.labelInput_.setEnabled(enabled);
goog.dom.classlist.enable(this.getElement(),
goog.getCssName('goog-combobox-disabled'), !enabled);
};
/** @override */
goog.ui.ComboBox.prototype.enterDocument = function() {
goog.ui.ComboBox.superClass_.enterDocument.call(this);
var handler = this.getHandler();
handler.listen(this.getElement(),
goog.events.EventType.MOUSEDOWN, this.onComboMouseDown_);
handler.listen(this.getDomHelper().getDocument(),
goog.events.EventType.MOUSEDOWN, this.onDocClicked_);
handler.listen(this.input_,
goog.events.EventType.BLUR, this.onInputBlur_);
this.keyHandler_ = new goog.events.KeyHandler(this.input_);
handler.listen(this.keyHandler_,
goog.events.KeyHandler.EventType.KEY, this.handleKeyEvent);
this.inputHandler_ = new goog.events.InputHandler(this.input_);
handler.listen(this.inputHandler_,
goog.events.InputHandler.EventType.INPUT, this.onInputEvent_);
handler.listen(this.menu_,
goog.ui.Component.EventType.ACTION, this.onMenuSelected_);
};
/** @override */
goog.ui.ComboBox.prototype.exitDocument = function() {
this.keyHandler_.dispose();
delete this.keyHandler_;
this.inputHandler_.dispose();
this.inputHandler_ = null;
goog.ui.ComboBox.superClass_.exitDocument.call(this);
};
/**
* Combo box currently can't decorate elements.
* @return {boolean} The value false.
* @override
*/
goog.ui.ComboBox.prototype.canDecorate = function() {
return false;
};
/** @override */
goog.ui.ComboBox.prototype.disposeInternal = function() {
goog.ui.ComboBox.superClass_.disposeInternal.call(this);
this.clearDismissTimer_();
this.labelInput_.dispose();
this.menu_.dispose();
this.labelInput_ = null;
this.menu_ = null;
this.input_ = null;
this.button_ = null;
};
/**
* Dismisses the menu and resets the value of the edit field.
*/
goog.ui.ComboBox.prototype.dismiss = function() {
this.clearDismissTimer_();
this.hideMenu_();
this.menu_.setHighlightedIndex(-1);
};
/**
* Adds a new menu item at the end of the menu.
* @param {goog.ui.MenuItem} item Menu item to add to the menu.
*/
goog.ui.ComboBox.prototype.addItem = function(item) {
this.menu_.addChild(item, true);
this.visibleCount_ = -1;
};
/**
* Adds a new menu item at a specific index in the menu.
* @param {goog.ui.MenuItem} item Menu item to add to the menu.
* @param {number} n Index at which to insert the menu item.
*/
goog.ui.ComboBox.prototype.addItemAt = function(item, n) {
this.menu_.addChildAt(item, n, true);
this.visibleCount_ = -1;
};
/**
* Removes an item from the menu and disposes it.
* @param {goog.ui.MenuItem} item The menu item to remove.
*/
goog.ui.ComboBox.prototype.removeItem = function(item) {
var child = this.menu_.removeChild(item, true);
if (child) {
child.dispose();
this.visibleCount_ = -1;
}
};
/**
* Remove all of the items from the ComboBox menu
*/
goog.ui.ComboBox.prototype.removeAllItems = function() {
for (var i = this.getItemCount() - 1; i >= 0; --i) {
this.removeItem(this.getItemAt(i));
}
};
/**
* Removes a menu item at a given index in the menu.
* @param {number} n Index of item.
*/
goog.ui.ComboBox.prototype.removeItemAt = function(n) {
var child = this.menu_.removeChildAt(n, true);
if (child) {
child.dispose();
this.visibleCount_ = -1;
}
};
/**
* Returns a reference to the menu item at a given index.
* @param {number} n Index of menu item.
* @return {goog.ui.MenuItem?} Reference to the menu item.
*/
goog.ui.ComboBox.prototype.getItemAt = function(n) {
return /** @type {goog.ui.MenuItem?} */(this.menu_.getChildAt(n));
};
/**
* Returns the number of items in the list, including non-visible items,
* such as separators.
* @return {number} Number of items in the menu for this combobox.
*/
goog.ui.ComboBox.prototype.getItemCount = function() {
return this.menu_.getChildCount();
};
/**
* @return {goog.ui.Menu} The menu that pops up.
*/
goog.ui.ComboBox.prototype.getMenu = function() {
return this.menu_;
};
/**
* @return {Element} The input element.
*/
goog.ui.ComboBox.prototype.getInputElement = function() {
return this.input_;
};
/**
* @return {number} The number of visible items in the menu.
* @private
*/
goog.ui.ComboBox.prototype.getNumberOfVisibleItems_ = function() {
if (this.visibleCount_ == -1) {
var count = 0;
for (var i = 0, n = this.menu_.getChildCount(); i < n; i++) {
var item = this.menu_.getChildAt(i);
if (!(item instanceof goog.ui.MenuSeparator) && item.isVisible()) {
count++;
}
}
this.visibleCount_ = count;
}
goog.log.info(this.logger_,
'getNumberOfVisibleItems() - ' + this.visibleCount_);
return this.visibleCount_;
};
/**
* Sets the match function to be used when filtering the combo box menu.
* @param {Function} matchFunction The match function to be used when filtering
* the combo box menu.
*/
goog.ui.ComboBox.prototype.setMatchFunction = function(matchFunction) {
this.matchFunction_ = matchFunction;
};
/**
* @return {Function} The match function for the combox box.
*/
goog.ui.ComboBox.prototype.getMatchFunction = function() {
return this.matchFunction_;
};
/**
* Sets the default text for the combo box.
* @param {string} text The default text for the combo box.
*/
goog.ui.ComboBox.prototype.setDefaultText = function(text) {
this.defaultText_ = text;
if (this.labelInput_) {
this.labelInput_.setLabel(this.defaultText_);
}
};
/**
* @return {string} text The default text for the combox box.
*/
goog.ui.ComboBox.prototype.getDefaultText = function() {
return this.defaultText_;
};
/**
* Sets the field name for the combo box.
* @param {string} fieldName The field name for the combo box.
*/
goog.ui.ComboBox.prototype.setFieldName = function(fieldName) {
this.fieldName_ = fieldName;
};
/**
* @return {string} The field name for the combo box.
*/
goog.ui.ComboBox.prototype.getFieldName = function() {
return this.fieldName_;
};
/**
* Set to true if a unicode inverted triangle should be displayed in the
* dropdown button.
* This option defaults to false for backwards compatibility.
* @param {boolean} useDropdownArrow True to use the dropdown arrow.
*/
goog.ui.ComboBox.prototype.setUseDropdownArrow = function(useDropdownArrow) {
this.useDropdownArrow_ = !!useDropdownArrow;
};
/**
* Sets the current value of the combo box.
* @param {string} value The new value.
*/
goog.ui.ComboBox.prototype.setValue = function(value) {
goog.log.info(this.logger_, 'setValue() - ' + value);
if (this.labelInput_.getValue() != value) {
this.labelInput_.setValue(value);
this.handleInputChange_();
}
};
/**
* @return {string} The current value of the combo box.
*/
goog.ui.ComboBox.prototype.getValue = function() {
return this.labelInput_.getValue();
};
/**
* @return {string} HTML escaped token.
*/
goog.ui.ComboBox.prototype.getToken = function() {
// TODO(user): Remove HTML escaping and fix the existing calls.
return goog.string.htmlEscape(this.getTokenText_());
};
/**
* @return {string} The token for the current cursor position in the
* input box, when multi-input is disabled it will be the full input value.
* @private
*/
goog.ui.ComboBox.prototype.getTokenText_ = function() {
// TODO(user): Implement multi-input such that getToken returns a substring
// of the whole input delimited by commas.
return goog.string.trim(this.labelInput_.getValue().toLowerCase());
};
/**
* @private
*/
goog.ui.ComboBox.prototype.setupMenu_ = function() {
var sm = this.menu_;
sm.setVisible(false);
sm.setAllowAutoFocus(false);
sm.setAllowHighlightDisabled(true);
};
/**
* Shows the menu if it isn't already showing. Also positions the menu
* correctly, resets the menu item visibilities and highlights the relevent
* item.
* @param {boolean} showAll Whether to show all items, with the first matching
* item highlighted.
* @private
*/
goog.ui.ComboBox.prototype.maybeShowMenu_ = function(showAll) {
var isVisible = this.menu_.isVisible();
var numVisibleItems = this.getNumberOfVisibleItems_();
if (isVisible && numVisibleItems == 0) {
goog.log.fine(this.logger_, 'no matching items, hiding');
this.hideMenu_();
} else if (!isVisible && numVisibleItems > 0) {
if (showAll) {
goog.log.fine(this.logger_, 'showing menu');
this.setItemVisibilityFromToken_('');
this.setItemHighlightFromToken_(this.getTokenText_());
}
// In Safari 2.0, when clicking on the combox box, the blur event is
// received after the click event that invokes this function. Since we want
// to cancel the dismissal after the blur event is processed, we have to
// wait for all event processing to happen.
goog.Timer.callOnce(this.clearDismissTimer_, 1, this);
this.showMenu_();
}
this.positionMenu();
};
/**
* Positions the menu.
* @protected
*/
goog.ui.ComboBox.prototype.positionMenu = function() {
if (this.menu_ && this.menu_.isVisible()) {
var position = new goog.positioning.MenuAnchoredPosition(this.getElement(),
goog.positioning.Corner.BOTTOM_START, true);
position.reposition(this.menu_.getElement(),
goog.positioning.Corner.TOP_START);
}
};
/**
* Show the menu and add an active class to the combo box's element.
* @private
*/
goog.ui.ComboBox.prototype.showMenu_ = function() {
this.menu_.setVisible(true);
goog.dom.classlist.add(this.getElement(),
goog.getCssName('goog-combobox-active'));
};
/**
* Hide the menu and remove the active class from the combo box's element.
* @private
*/
goog.ui.ComboBox.prototype.hideMenu_ = function() {
this.menu_.setVisible(false);
goog.dom.classlist.remove(this.getElement(),
goog.getCssName('goog-combobox-active'));
};
/**
* Clears the dismiss timer if it's active.
* @private
*/
goog.ui.ComboBox.prototype.clearDismissTimer_ = function() {
if (this.dismissTimer_) {
goog.Timer.clear(this.dismissTimer_);
this.dismissTimer_ = null;
}
};
/**
* Event handler for when the combo box area has been clicked.
* @param {goog.events.BrowserEvent} e The browser event.
* @private
*/
goog.ui.ComboBox.prototype.onComboMouseDown_ = function(e) {
// We only want this event on the element itself or the input or the button.
if (this.enabled_ &&
(e.target == this.getElement() || e.target == this.input_ ||
goog.dom.contains(this.button_, /** @type {Node} */ (e.target)))) {
if (this.menu_.isVisible()) {
goog.log.fine(this.logger_, 'Menu is visible, dismissing');
this.dismiss();
} else {
goog.log.fine(this.logger_, 'Opening dropdown');
this.maybeShowMenu_(true);
if (goog.userAgent.OPERA) {
// select() doesn't focus <input> elements in Opera.
this.input_.focus();
}
this.input_.select();
this.menu_.setMouseButtonPressed(true);
// Stop the click event from stealing focus
e.preventDefault();
}
}
// Stop the event from propagating outside of the combo box
e.stopPropagation();
};
/**
* Event handler for when the document is clicked.
* @param {goog.events.BrowserEvent} e The browser event.
* @private
*/
goog.ui.ComboBox.prototype.onDocClicked_ = function(e) {
if (!goog.dom.contains(
this.menu_.getElement(), /** @type {Node} */ (e.target))) {
goog.log.info(this.logger_, 'onDocClicked_() - dismissing immediately');
this.dismiss();
}
};
/**
* Handle the menu's select event.
* @param {goog.events.Event} e The event.
* @private
*/
goog.ui.ComboBox.prototype.onMenuSelected_ = function(e) {
goog.log.info(this.logger_, 'onMenuSelected_()');
var item = /** @type {!goog.ui.MenuItem} */ (e.target);
// Stop propagation of the original event and redispatch to allow the menu
// select to be cancelled at this level. i.e. if a menu item should cause
// some behavior such as a user prompt instead of assigning the caption as
// the value.
if (this.dispatchEvent(new goog.ui.ItemEvent(
goog.ui.Component.EventType.ACTION, this, item))) {
var caption = item.getCaption();
goog.log.fine(this.logger_,
'Menu selection: ' + caption + '. Dismissing menu');
if (this.labelInput_.getValue() != caption) {
this.labelInput_.setValue(caption);
this.dispatchEvent(goog.ui.Component.EventType.CHANGE);
}
this.dismiss();
}
e.stopPropagation();
};
/**
* Event handler for when the input box looses focus -- hide the menu
* @param {goog.events.BrowserEvent} e The browser event.
* @private
*/
goog.ui.ComboBox.prototype.onInputBlur_ = function(e) {
goog.log.info(this.logger_, 'onInputBlur_() - delayed dismiss');
this.clearDismissTimer_();
this.dismissTimer_ = goog.Timer.callOnce(
this.dismiss, goog.ui.ComboBox.BLUR_DISMISS_TIMER_MS, this);
};
/**
* Handles keyboard events from the input box. Returns true if the combo box
* was able to handle the event, false otherwise.
* @param {goog.events.KeyEvent} e Key event to handle.
* @return {boolean} Whether the event was handled by the combo box.
* @protected
* @suppress {visibility} performActionInternal
*/
goog.ui.ComboBox.prototype.handleKeyEvent = function(e) {
var isMenuVisible = this.menu_.isVisible();
// Give the menu a chance to handle the event.
if (isMenuVisible && this.menu_.handleKeyEvent(e)) {
return true;
}
// The menu is either hidden or didn't handle the event.
var handled = false;
switch (e.keyCode) {
case goog.events.KeyCodes.ESC:
// If the menu is visible and the user hit Esc, dismiss the menu.
if (isMenuVisible) {
goog.log.fine(this.logger_,
'Dismiss on Esc: ' + this.labelInput_.getValue());
this.dismiss();
handled = true;
}
break;
case goog.events.KeyCodes.TAB:
// If the menu is open and an option is highlighted, activate it.
if (isMenuVisible) {
var highlighted = this.menu_.getHighlighted();
if (highlighted) {
goog.log.fine(this.logger_,
'Select on Tab: ' + this.labelInput_.getValue());
highlighted.performActionInternal(e);
handled = true;
}
}
break;
case goog.events.KeyCodes.UP:
case goog.events.KeyCodes.DOWN:
// If the menu is hidden and the user hit the up/down arrow, show it.
if (!isMenuVisible) {
goog.log.fine(this.logger_, 'Up/Down - maybe show menu');
this.maybeShowMenu_(true);
handled = true;
}
break;
}
if (handled) {
e.preventDefault();
}
return handled;
};
/**
* Handles the content of the input box changing.
* @param {goog.events.Event} e The INPUT event to handle.
* @private
*/
goog.ui.ComboBox.prototype.onInputEvent_ = function(e) {
// If the key event is text-modifying, update the menu.
goog.log.fine(this.logger_,
'Key is modifying: ' + this.labelInput_.getValue());
this.handleInputChange_();
};
/**
* Handles the content of the input box changing, either because of user
* interaction or programmatic changes.
* @private
*/
goog.ui.ComboBox.prototype.handleInputChange_ = function() {
var token = this.getTokenText_();
this.setItemVisibilityFromToken_(token);
if (goog.dom.getActiveElement(this.getDomHelper().getDocument()) ==
this.input_) {
// Do not alter menu visibility unless the user focus is currently on the
// combobox (otherwise programmatic changes may cause the menu to become
// visible).
this.maybeShowMenu_(false);
}
var highlighted = this.menu_.getHighlighted();
if (token == '' || !highlighted || !highlighted.isVisible()) {
this.setItemHighlightFromToken_(token);
}
this.lastToken_ = token;
this.dispatchEvent(goog.ui.Component.EventType.CHANGE);
};
/**
* Loops through all menu items setting their visibility according to a token.
* @param {string} token The token.
* @private
*/
goog.ui.ComboBox.prototype.setItemVisibilityFromToken_ = function(token) {
goog.log.info(this.logger_, 'setItemVisibilityFromToken_() - ' + token);
var isVisibleItem = false;
var count = 0;
var recheckHidden = !this.matchFunction_(token, this.lastToken_);
for (var i = 0, n = this.menu_.getChildCount(); i < n; i++) {
var item = this.menu_.getChildAt(i);
if (item instanceof goog.ui.MenuSeparator) {
// Ensure that separators are only shown if there is at least one visible
// item before them.
item.setVisible(isVisibleItem);
isVisibleItem = false;
} else if (item instanceof goog.ui.MenuItem) {
if (!item.isVisible() && !recheckHidden) continue;
var caption = item.getCaption();
var visible = this.isItemSticky_(item) ||
caption && this.matchFunction_(caption.toLowerCase(), token);
if (typeof item.setFormatFromToken == 'function') {
item.setFormatFromToken(token);
}
item.setVisible(!!visible);
isVisibleItem = visible || isVisibleItem;
} else {
// Assume all other items are correctly using their visibility.
isVisibleItem = item.isVisible() || isVisibleItem;
}
if (!(item instanceof goog.ui.MenuSeparator) && item.isVisible()) {
count++;
}
}
this.visibleCount_ = count;
};
/**
* Highlights the first token that matches the given token.
* @param {string} token The token.
* @private
*/
goog.ui.ComboBox.prototype.setItemHighlightFromToken_ = function(token) {
goog.log.info(this.logger_, 'setItemHighlightFromToken_() - ' + token);
if (token == '') {
this.menu_.setHighlightedIndex(-1);
return;
}
for (var i = 0, n = this.menu_.getChildCount(); i < n; i++) {
var item = this.menu_.getChildAt(i);
var caption = item.getCaption();
if (caption && this.matchFunction_(caption.toLowerCase(), token)) {
this.menu_.setHighlightedIndex(i);
if (item.setFormatFromToken) {
item.setFormatFromToken(token);
}
return;
}
}
this.menu_.setHighlightedIndex(-1);
};
/**
* Returns true if the item has an isSticky method and the method returns true.
* @param {goog.ui.MenuItem} item The item.
* @return {boolean} Whether the item has an isSticky method and the method
* returns true.
* @private
*/
goog.ui.ComboBox.prototype.isItemSticky_ = function(item) {
return typeof item.isSticky == 'function' && item.isSticky();
};
/**
* Class for combo box items.
* @param {goog.ui.ControlContent} content Text caption or DOM structure to
* display as the content of the item (use to add icons or styling to
* menus).
* @param {Object=} opt_data Identifying data for the menu item.
* @param {goog.dom.DomHelper=} opt_domHelper Optional dom helper used for dom
* interactions.
* @param {goog.ui.MenuItemRenderer=} opt_renderer Optional renderer.
* @constructor
* @extends {goog.ui.MenuItem}
*/
goog.ui.ComboBoxItem = function(content, opt_data, opt_domHelper,
opt_renderer) {
goog.ui.MenuItem.call(this, content, opt_data, opt_domHelper, opt_renderer);
};
goog.inherits(goog.ui.ComboBoxItem, goog.ui.MenuItem);
// Register a decorator factory function for goog.ui.ComboBoxItems.
goog.ui.registry.setDecoratorByClassName(goog.getCssName('goog-combobox-item'),
function() {
// ComboBoxItem defaults to using MenuItemRenderer.
return new goog.ui.ComboBoxItem(null);
});
/**
* Whether the menu item is sticky, non-sticky items will be hidden as the
* user types.
* @type {boolean}
* @private
*/
goog.ui.ComboBoxItem.prototype.isSticky_ = false;
/**
* Sets the menu item to be sticky or not sticky.
* @param {boolean} sticky Whether the menu item should be sticky.
*/
goog.ui.ComboBoxItem.prototype.setSticky = function(sticky) {
this.isSticky_ = sticky;
};
/**
* @return {boolean} Whether the menu item is sticky.
*/
goog.ui.ComboBoxItem.prototype.isSticky = function() {
return this.isSticky_;
};
/**
* Sets the format for a menu item based on a token, bolding the token.
* @param {string} token The token.
*/
goog.ui.ComboBoxItem.prototype.setFormatFromToken = function(token) {
if (this.isEnabled()) {
var caption = this.getCaption();
var index = caption.toLowerCase().indexOf(token);
if (index >= 0) {
var domHelper = this.getDomHelper();
this.setContent([
domHelper.createTextNode(caption.substr(0, index)),
domHelper.createDom('b', null, caption.substr(index, token.length)),
domHelper.createTextNode(caption.substr(index + token.length))
]);
}
}
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,371 @@
// 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 Base class for container renderers.
*
* @author attila@google.com (Attila Bodis)
*/
goog.provide('goog.ui.ContainerRenderer');
goog.require('goog.a11y.aria');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.classes');
goog.require('goog.string');
goog.require('goog.style');
goog.require('goog.ui.registry');
goog.require('goog.userAgent');
/**
* Default renderer for {@link goog.ui.Container}. Can be used as-is, but
* subclasses of Container will probably want to use renderers specifically
* tailored for them by extending this class.
* @constructor
*/
goog.ui.ContainerRenderer = function() {
};
goog.addSingletonGetter(goog.ui.ContainerRenderer);
/**
* Constructs a new renderer and sets the CSS class that the renderer will use
* as the base CSS class to apply to all elements rendered by that renderer.
* An example to use this function using a menu is:
*
* <pre>
* var myCustomRenderer = goog.ui.ContainerRenderer.getCustomRenderer(
* goog.ui.MenuRenderer, 'my-special-menu');
* var newMenu = new goog.ui.Menu(opt_domHelper, myCustomRenderer);
* </pre>
*
* Your styles for the menu can now be:
* <pre>
* .my-special-menu { }
* </pre>
*
* <em>instead</em> of
* <pre>
* .CSS_MY_SPECIAL_MENU .goog-menu { }
* </pre>
*
* You would want to use this functionality when you want an instance of a
* component to have specific styles different than the other components of the
* same type in your application. This avoids using descendant selectors to
* apply the specific styles to this component.
*
* @param {Function} ctor The constructor of the renderer you want to create.
* @param {string} cssClassName The name of the CSS class for this renderer.
* @return {goog.ui.ContainerRenderer} An instance of the desired renderer with
* its getCssClass() method overridden to return the supplied custom CSS
* class name.
*/
goog.ui.ContainerRenderer.getCustomRenderer = function(ctor, cssClassName) {
var renderer = new ctor();
/**
* Returns the CSS class to be applied to the root element of components
* rendered using this renderer.
* @return {string} Renderer-specific CSS class.
*/
renderer.getCssClass = function() {
return cssClassName;
};
return renderer;
};
/**
* Default CSS class to be applied to the root element of containers rendered
* by this renderer.
* @type {string}
*/
goog.ui.ContainerRenderer.CSS_CLASS = goog.getCssName('goog-container');
/**
* Returns the ARIA role to be applied to the container.
* See http://wiki/Main/ARIA for more info.
* @return {undefined|string} ARIA role.
*/
goog.ui.ContainerRenderer.prototype.getAriaRole = function() {
// By default, the ARIA role is unspecified.
return undefined;
};
/**
* Enables or disables the tab index of the element. Only elements with a
* valid tab index can receive focus.
* @param {Element} element Element whose tab index is to be changed.
* @param {boolean} enable Whether to add or remove the element's tab index.
*/
goog.ui.ContainerRenderer.prototype.enableTabIndex = function(element, enable) {
if (element) {
element.tabIndex = enable ? 0 : -1;
}
};
/**
* Creates and returns the container's root element. The default
* simply creates a DIV and applies the renderer's own CSS class name to it.
* To be overridden in subclasses.
* @param {goog.ui.Container} container Container to render.
* @return {Element} Root element for the container.
*/
goog.ui.ContainerRenderer.prototype.createDom = function(container) {
return container.getDomHelper().createDom('div',
this.getClassNames(container).join(' '));
};
/**
* Returns the DOM element into which child components are to be rendered,
* or null if the container hasn't been rendered yet.
* @param {Element} element Root element of the container whose content element
* is to be returned.
* @return {Element} Element to contain child elements (null if none).
*/
goog.ui.ContainerRenderer.prototype.getContentElement = function(element) {
return element;
};
/**
* Default implementation of {@code canDecorate}; returns true if the element
* is a DIV, false otherwise.
* @param {Element} element Element to decorate.
* @return {boolean} Whether the renderer can decorate the element.
*/
goog.ui.ContainerRenderer.prototype.canDecorate = function(element) {
return element.tagName == 'DIV';
};
/**
* Default implementation of {@code decorate} for {@link goog.ui.Container}s.
* Decorates the element with the container, and attempts to decorate its child
* elements. Returns the decorated element.
* @param {goog.ui.Container} container Container to decorate the element.
* @param {Element} element Element to decorate.
* @return {Element} Decorated element.
*/
goog.ui.ContainerRenderer.prototype.decorate = function(container, element) {
// Set the container's ID to the decorated element's DOM ID, if any.
if (element.id) {
container.setId(element.id);
}
// Configure the container's state based on the CSS class names it has.
var baseClass = this.getCssClass();
var hasBaseClass = false;
var classNames = goog.dom.classes.get(element);
if (classNames) {
goog.array.forEach(classNames, function(className) {
if (className == baseClass) {
hasBaseClass = true;
} else if (className) {
this.setStateFromClassName(container, className, baseClass);
}
}, this);
}
if (!hasBaseClass) {
// Make sure the container's root element has the renderer's own CSS class.
goog.dom.classes.add(element, baseClass);
}
// Decorate the element's children, if applicable. This should happen after
// the container's own state has been initialized, since how children are
// decorated may depend on the state of the container.
this.decorateChildren(container, this.getContentElement(element));
return element;
};
/**
* Sets the container's state based on the given CSS class name, encountered
* during decoration. CSS class names that don't represent container states
* are ignored. Considered protected; subclasses should override this method
* to support more states and CSS class names.
* @param {goog.ui.Container} container Container to update.
* @param {string} className CSS class name.
* @param {string} baseClass Base class name used as the root of state-specific
* class names (typically the renderer's own class name).
* @protected
*/
goog.ui.ContainerRenderer.prototype.setStateFromClassName = function(container,
className, baseClass) {
if (className == goog.getCssName(baseClass, 'disabled')) {
container.setEnabled(false);
} else if (className == goog.getCssName(baseClass, 'horizontal')) {
container.setOrientation(goog.ui.Container.Orientation.HORIZONTAL);
} else if (className == goog.getCssName(baseClass, 'vertical')) {
container.setOrientation(goog.ui.Container.Orientation.VERTICAL);
}
};
/**
* Takes a container and an element that may contain child elements, decorates
* the child elements, and adds the corresponding components to the container
* as child components. Any non-element child nodes (e.g. empty text nodes
* introduced by line breaks in the HTML source) are removed from the element.
* @param {goog.ui.Container} container Container whose children are to be
* discovered.
* @param {Element} element Element whose children are to be decorated.
* @param {Element=} opt_firstChild the first child to be decorated.
*/
goog.ui.ContainerRenderer.prototype.decorateChildren = function(container,
element, opt_firstChild) {
if (element) {
var node = opt_firstChild || element.firstChild, next;
// Tag soup HTML may result in a DOM where siblings have different parents.
while (node && node.parentNode == element) {
// Get the next sibling here, since the node may be replaced or removed.
next = node.nextSibling;
if (node.nodeType == goog.dom.NodeType.ELEMENT) {
// Decorate element node.
var child = this.getDecoratorForChild(/** @type {Element} */(node));
if (child) {
// addChild() may need to look at the element.
child.setElementInternal(/** @type {Element} */(node));
// If the container is disabled, mark the child disabled too. See
// bug 1263729. Note that this must precede the call to addChild().
if (!container.isEnabled()) {
child.setEnabled(false);
}
container.addChild(child);
child.decorate(/** @type {Element} */(node));
}
} else if (!node.nodeValue || goog.string.trim(node.nodeValue) == '') {
// Remove empty text node, otherwise madness ensues (e.g. controls that
// use goog-inline-block will flicker and shift on hover on Gecko).
element.removeChild(node);
}
node = next;
}
}
};
/**
* Inspects the element, and creates an instance of {@link goog.ui.Control} or
* an appropriate subclass best suited to decorate it. Returns the control (or
* null if no suitable class was found). This default implementation uses the
* element's CSS class to find the appropriate control class to instantiate.
* May be overridden in subclasses.
* @param {Element} element Element to decorate.
* @return {goog.ui.Control?} A new control suitable to decorate the element
* (null if none).
*/
goog.ui.ContainerRenderer.prototype.getDecoratorForChild = function(element) {
return /** @type {goog.ui.Control} */ (
goog.ui.registry.getDecorator(element));
};
/**
* Initializes the container's DOM when the container enters the document.
* Called from {@link goog.ui.Container#enterDocument}.
* @param {goog.ui.Container} container Container whose DOM is to be initialized
* as it enters the document.
*/
goog.ui.ContainerRenderer.prototype.initializeDom = function(container) {
var elem = container.getElement();
goog.asserts.assert(elem, 'The container DOM element cannot be null.');
// Make sure the container's element isn't selectable. On Gecko, recursively
// marking each child element unselectable is expensive and unnecessary, so
// only mark the root element unselectable.
goog.style.setUnselectable(elem, true, goog.userAgent.GECKO);
// IE doesn't support outline:none, so we have to use the hideFocus property.
if (goog.userAgent.IE) {
elem.hideFocus = true;
}
// Set the ARIA role.
var ariaRole = this.getAriaRole();
if (ariaRole) {
goog.a11y.aria.setRole(elem, ariaRole);
}
};
/**
* Returns the element within the container's DOM that should receive keyboard
* focus (null if none). The default implementation returns the container's
* root element.
* @param {goog.ui.Container} container Container whose key event target is
* to be returned.
* @return {Element} Key event target (null if none).
*/
goog.ui.ContainerRenderer.prototype.getKeyEventTarget = function(container) {
return container.getElement();
};
/**
* Returns the CSS class to be applied to the root element of containers
* rendered using this renderer.
* @return {string} Renderer-specific CSS class.
*/
goog.ui.ContainerRenderer.prototype.getCssClass = function() {
return goog.ui.ContainerRenderer.CSS_CLASS;
};
/**
* Returns all CSS class names applicable to the given container, based on its
* state. The array of class names returned includes the renderer's own CSS
* class, followed by a CSS class indicating the container's orientation,
* followed by any state-specific CSS classes.
* @param {goog.ui.Container} container Container whose CSS classes are to be
* returned.
* @return {Array.<string>} Array of CSS class names applicable to the
* container.
*/
goog.ui.ContainerRenderer.prototype.getClassNames = function(container) {
var baseClass = this.getCssClass();
var isHorizontal =
container.getOrientation() == goog.ui.Container.Orientation.HORIZONTAL;
var classNames = [
baseClass,
(isHorizontal ?
goog.getCssName(baseClass, 'horizontal') :
goog.getCssName(baseClass, 'vertical'))
];
if (!container.isEnabled()) {
classNames.push(goog.getCssName(baseClass, 'disabled'));
}
return classNames;
};
/**
* Returns the default orientation of containers rendered or decorated by this
* renderer. The base class implementation returns {@code VERTICAL}.
* @return {goog.ui.Container.Orientation} Default orientation for containers
* created or decorated by this renderer.
*/
goog.ui.ContainerRenderer.prototype.getDefaultOrientation = function() {
return goog.ui.Container.Orientation.VERTICAL;
};

View File

@@ -0,0 +1,222 @@
// 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 Scroll behavior that can be added onto a container.
* @author gboyer@google.com (Garry Boyer)
*/
goog.provide('goog.ui.ContainerScroller');
goog.require('goog.Disposable');
goog.require('goog.Timer');
goog.require('goog.events.EventHandler');
goog.require('goog.style');
goog.require('goog.ui.Component');
goog.require('goog.ui.Container');
/**
* Plug-on scrolling behavior for a container.
*
* Use this to style containers, such as pop-up menus, to be scrolling, and
* automatically keep the highlighted element visible.
*
* To use this, first style your container with the desired overflow
* properties and height to achieve vertical scrolling. Also, the scrolling
* div should have no vertical padding, for two reasons: it is difficult to
* compensate for, and is generally not what you want due to the strange way
* CSS handles padding on the scrolling dimension.
*
* The container must already be rendered before this may be constructed.
*
* @param {!goog.ui.Container} container The container to attach behavior to.
* @constructor
* @extends {goog.Disposable}
*/
goog.ui.ContainerScroller = function(container) {
goog.Disposable.call(this);
/**
* The container that we are bestowing scroll behavior on.
* @type {!goog.ui.Container}
* @private
*/
this.container_ = container;
/**
* Event handler for this object.
* @type {!goog.events.EventHandler}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
this.eventHandler_.listen(container, goog.ui.Component.EventType.HIGHLIGHT,
this.onHighlight_);
this.eventHandler_.listen(container, goog.ui.Component.EventType.ENTER,
this.onEnter_);
this.eventHandler_.listen(container, goog.ui.Container.EventType.AFTER_SHOW,
this.onAfterShow_);
this.eventHandler_.listen(container, goog.ui.Component.EventType.HIDE,
this.onHide_);
// TODO(gboyer): Allow a ContainerScroller to be attached with a Container
// before the container is rendered.
this.doScrolling_(true);
};
goog.inherits(goog.ui.ContainerScroller, goog.Disposable);
/**
* The last target the user hovered over.
*
* @see #onEnter_
* @type {goog.ui.Component}
* @private
*/
goog.ui.ContainerScroller.prototype.lastEnterTarget_ = null;
/**
* The scrollTop of the container before it was hidden.
* Used to restore the scroll position when the container is shown again.
* @type {?number}
* @private
*/
goog.ui.ContainerScroller.prototype.scrollTopBeforeHide_ = null;
/**
* Whether we are disabling the default handler for hovering.
*
* @see #onEnter_
* @see #temporarilyDisableHover_
* @type {boolean}
* @private
*/
goog.ui.ContainerScroller.prototype.disableHover_ = false;
/**
* Handles hover events on the container's children.
*
* Helps enforce two constraints: scrolling should not cause mouse highlights,
* and mouse highlights should not cause scrolling.
*
* @param {goog.events.Event} e The container's ENTER event.
* @private
*/
goog.ui.ContainerScroller.prototype.onEnter_ = function(e) {
if (this.disableHover_) {
// The container was scrolled recently. Since the mouse may be over the
// container, stop the default action of the ENTER event from causing
// highlights.
e.preventDefault();
} else {
// The mouse is moving and causing hover events. Stop the resulting
// highlight (if it happens) from causing a scroll.
this.lastEnterTarget_ = /** @type {goog.ui.Component} */ (e.target);
}
};
/**
* Handles highlight events on the container's children.
* @param {goog.events.Event} e The container's highlight event.
* @private
*/
goog.ui.ContainerScroller.prototype.onHighlight_ = function(e) {
this.doScrolling_();
};
/**
* Handles AFTER_SHOW events on the container. Makes the container
* scroll to the previously scrolled position (if there was one),
* then adjust it to make the highlighted element be in view (if there is one).
* If there was no previous scroll position, then center the highlighted
* element (if there is one).
* @param {goog.events.Event} e The container's AFTER_SHOW event.
* @private
*/
goog.ui.ContainerScroller.prototype.onAfterShow_ = function(e) {
if (this.scrollTopBeforeHide_ != null) {
this.container_.getElement().scrollTop = this.scrollTopBeforeHide_;
// Make sure the highlighted item is still visible, in case the list
// or its hilighted item has changed.
this.doScrolling_(false);
} else {
this.doScrolling_(true);
}
};
/**
* Handles hide events on the container. Clears out the last enter target,
* since it is no longer applicable, and remembers the scroll position of
* the menu so that it can be restored when the menu is reopened.
* @param {goog.events.Event} e The container's hide event.
* @private
*/
goog.ui.ContainerScroller.prototype.onHide_ = function(e) {
if (e.target == this.container_) {
this.lastEnterTarget_ = null;
this.scrollTopBeforeHide_ = this.container_.getElement().scrollTop;
}
};
/**
* Centers the currently highlighted item, if this is scrollable.
* @param {boolean=} opt_center Whether to center the highlighted element
* rather than simply ensure it is in view. Useful for the first
* render.
* @private
*/
goog.ui.ContainerScroller.prototype.doScrolling_ = function(opt_center) {
var highlighted = this.container_.getHighlighted();
// Only scroll if we're visible and there is a highlighted item.
if (this.container_.isVisible() && highlighted &&
highlighted != this.lastEnterTarget_) {
var element = this.container_.getElement();
goog.style.scrollIntoContainerView(highlighted.getElement(), element,
opt_center);
this.temporarilyDisableHover_();
this.lastEnterTarget_ = null;
}
};
/**
* Temporarily disables hover events from changing highlight.
* @see #onEnter_
* @private
*/
goog.ui.ContainerScroller.prototype.temporarilyDisableHover_ = function() {
this.disableHover_ = true;
goog.Timer.callOnce(function() {
this.disableHover_ = false;
}, 0, this);
};
/** @override */
goog.ui.ContainerScroller.prototype.disposeInternal = function() {
goog.ui.ContainerScroller.superClass_.disposeInternal.call(this);
this.eventHandler_.dispose();
this.lastEnterTarget_ = null;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
// Copyright 2009 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 Type declaration for control content.
*
* @author nicksantos@google.com (Nick Santos)
*/
goog.provide('goog.ui.ControlContent');
/**
* Type declaration for text caption or DOM structure to be used as the content
* of {@link goog.ui.Control}s.
* @typedef {string|Node|Array.<Node>|NodeList}
*/
goog.ui.ControlContent;

View File

@@ -0,0 +1,855 @@
// 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 Base class for control renderers.
* TODO(attila): If the renderer framework works well, pull it into Component.
*
* @author attila@google.com (Attila Bodis)
*/
goog.provide('goog.ui.ControlRenderer');
goog.require('goog.a11y.aria');
goog.require('goog.a11y.aria.State');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.classes');
goog.require('goog.object');
goog.require('goog.style');
goog.require('goog.ui.Component');
goog.require('goog.userAgent');
/**
* Default renderer for {@link goog.ui.Control}s. Can be used as-is, but
* subclasses of Control will probably want to use renderers specifically
* tailored for them by extending this class. Controls that use renderers
* delegate one or more of the following API methods to the renderer:
* <ul>
* <li>{@code createDom} - renders the DOM for the component
* <li>{@code canDecorate} - determines whether an element can be decorated
* by the component
* <li>{@code decorate} - decorates an existing element with the component
* <li>{@code setState} - updates the appearance of the component based on
* its state
* <li>{@code getContent} - returns the component's content
* <li>{@code setContent} - sets the component's content
* </ul>
* Controls are stateful; renderers, on the other hand, should be stateless and
* reusable.
* @constructor
*/
goog.ui.ControlRenderer = function() {
};
goog.addSingletonGetter(goog.ui.ControlRenderer);
/**
* Constructs a new renderer and sets the CSS class that the renderer will use
* as the base CSS class to apply to all elements rendered by that renderer.
* An example to use this function using a color palette:
*
* <pre>
* var myCustomRenderer = goog.ui.ControlRenderer.getCustomRenderer(
* goog.ui.PaletteRenderer, 'my-special-palette');
* var newColorPalette = new goog.ui.ColorPalette(
* colors, myCustomRenderer, opt_domHelper);
* </pre>
*
* Your CSS can look like this now:
* <pre>
* .my-special-palette { }
* .my-special-palette-table { }
* .my-special-palette-cell { }
* etc.
* </pre>
*
* <em>instead</em> of
* <pre>
* .CSS_MY_SPECIAL_PALETTE .goog-palette { }
* .CSS_MY_SPECIAL_PALETTE .goog-palette-table { }
* .CSS_MY_SPECIAL_PALETTE .goog-palette-cell { }
* etc.
* </pre>
*
* You would want to use this functionality when you want an instance of a
* component to have specific styles different than the other components of the
* same type in your application. This avoids using descendant selectors to
* apply the specific styles to this component.
*
* @param {Function} ctor The constructor of the renderer you are trying to
* create.
* @param {string} cssClassName The name of the CSS class for this renderer.
* @return {goog.ui.ControlRenderer} An instance of the desired renderer with
* its getCssClass() method overridden to return the supplied custom CSS
* class name.
*/
goog.ui.ControlRenderer.getCustomRenderer = function(ctor, cssClassName) {
var renderer = new ctor();
/**
* Returns the CSS class to be applied to the root element of components
* rendered using this renderer.
* @return {string} Renderer-specific CSS class.
*/
renderer.getCssClass = function() {
return cssClassName;
};
return renderer;
};
/**
* Default CSS class to be applied to the root element of components rendered
* by this renderer.
* @type {string}
*/
goog.ui.ControlRenderer.CSS_CLASS = goog.getCssName('goog-control');
/**
* Array of arrays of CSS classes that we want composite classes added and
* removed for in IE6 and lower as a workaround for lack of multi-class CSS
* selector support.
*
* Subclasses that have accompanying CSS requiring this workaround should define
* their own static IE6_CLASS_COMBINATIONS constant and override
* getIe6ClassCombinations to return it.
*
* For example, if your stylesheet uses the selector .button.collapse-left
* (and is compiled to .button_collapse-left for the IE6 version of the
* stylesheet,) you should include ['button', 'collapse-left'] in this array
* and the class button_collapse-left will be applied to the root element
* whenever both button and collapse-left are applied individually.
*
* Members of each class name combination will be joined with underscores in the
* order that they're defined in the array. You should alphabetize them (for
* compatibility with the CSS compiler) unless you are doing something special.
* @type {Array.<Array.<string>>}
*/
goog.ui.ControlRenderer.IE6_CLASS_COMBINATIONS = [];
/**
* Map of component states to corresponding ARIA states. Since the mapping of
* component states to ARIA states is neither component- nor renderer-specific,
* this is a static property of the renderer class, and is initialized on first
* use.
* @type {Object}
* @private
*/
goog.ui.ControlRenderer.ARIA_STATE_MAP_;
/**
* Returns the ARIA role to be applied to the control.
* See http://wiki/Main/ARIA for more info.
* @return {goog.a11y.aria.Role|undefined} ARIA role.
*/
goog.ui.ControlRenderer.prototype.getAriaRole = function() {
// By default, the ARIA role is unspecified.
return undefined;
};
/**
* Returns the control's contents wrapped in a DIV, with the renderer's own
* CSS class and additional state-specific classes applied to it.
* @param {goog.ui.Control} control Control to render.
* @return {Element} Root element for the control.
*/
goog.ui.ControlRenderer.prototype.createDom = function(control) {
// Create and return DIV wrapping contents.
var element = control.getDomHelper().createDom(
'div', this.getClassNames(control).join(' '), control.getContent());
this.setAriaStates(control, element);
return element;
};
/**
* Takes the control's root element and returns the parent element of the
* control's contents. Since by default controls are rendered as a single
* DIV, the default implementation returns the element itself. Subclasses
* with more complex DOM structures must override this method as needed.
* @param {Element} element Root element of the control whose content element
* is to be returned.
* @return {Element} The control's content element.
*/
goog.ui.ControlRenderer.prototype.getContentElement = function(element) {
return element;
};
/**
* Updates the control's DOM by adding or removing the specified class name
* to/from its root element. May add additional combined classes as needed in
* IE6 and lower. Because of this, subclasses should use this method when
* modifying class names on the control's root element.
* @param {goog.ui.Control|Element} control Control instance (or root element)
* to be updated.
* @param {string} className CSS class name to add or remove.
* @param {boolean} enable Whether to add or remove the class name.
*/
goog.ui.ControlRenderer.prototype.enableClassName = function(control,
className, enable) {
var element = /** @type {Element} */ (
control.getElement ? control.getElement() : control);
if (element) {
// For IE6, we need to enable any combined classes involving this class
// as well.
if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('7')) {
var combinedClasses = this.getAppliedCombinedClassNames_(
goog.dom.classes.get(element), className);
combinedClasses.push(className);
var f = enable ? goog.dom.classes.add : goog.dom.classes.remove;
goog.partial(f, element).apply(null, combinedClasses);
} else {
goog.dom.classes.enable(element, className, enable);
}
}
};
/**
* Updates the control's DOM by adding or removing the specified extra class
* name to/from its element.
* @param {goog.ui.Control} control Control to be updated.
* @param {string} className CSS class name to add or remove.
* @param {boolean} enable Whether to add or remove the class name.
*/
goog.ui.ControlRenderer.prototype.enableExtraClassName = function(control,
className, enable) {
// The base class implementation is trivial; subclasses should override as
// needed.
this.enableClassName(control, className, enable);
};
/**
* Returns true if this renderer can decorate the element, false otherwise.
* The default implementation always returns true.
* @param {Element} element Element to decorate.
* @return {boolean} Whether the renderer can decorate the element.
*/
goog.ui.ControlRenderer.prototype.canDecorate = function(element) {
return true;
};
/**
* Default implementation of {@code decorate} for {@link goog.ui.Control}s.
* Initializes the control's ID, content, and state based on the ID of the
* element, its child nodes, and its CSS classes, respectively. Returns the
* element.
* @param {goog.ui.Control} control Control instance to decorate the element.
* @param {Element} element Element to decorate.
* @return {Element} Decorated element.
*/
goog.ui.ControlRenderer.prototype.decorate = function(control, element) {
// Set the control's ID to the decorated element's DOM ID, if any.
if (element.id) {
control.setId(element.id);
}
// Set the control's content to the decorated element's content.
var contentElem = this.getContentElement(element);
if (contentElem && contentElem.firstChild) {
control.setContentInternal(contentElem.firstChild.nextSibling ?
goog.array.clone(contentElem.childNodes) : contentElem.firstChild);
} else {
control.setContentInternal(null);
}
// Initialize the control's state based on the decorated element's CSS class.
// This implementation is optimized to minimize object allocations, string
// comparisons, and DOM access.
var state = 0x00;
var rendererClassName = this.getCssClass();
var structuralClassName = this.getStructuralCssClass();
var hasRendererClassName = false;
var hasStructuralClassName = false;
var hasCombinedClassName = false;
var classNames = goog.dom.classes.get(element);
goog.array.forEach(classNames, function(className) {
if (!hasRendererClassName && className == rendererClassName) {
hasRendererClassName = true;
if (structuralClassName == rendererClassName) {
hasStructuralClassName = true;
}
} else if (!hasStructuralClassName && className == structuralClassName) {
hasStructuralClassName = true;
} else {
state |= this.getStateFromClass(className);
}
}, this);
control.setStateInternal(state);
// Make sure the element has the renderer's CSS classes applied, as well as
// any extra class names set on the control.
if (!hasRendererClassName) {
classNames.push(rendererClassName);
if (structuralClassName == rendererClassName) {
hasStructuralClassName = true;
}
}
if (!hasStructuralClassName) {
classNames.push(structuralClassName);
}
var extraClassNames = control.getExtraClassNames();
if (extraClassNames) {
classNames.push.apply(classNames, extraClassNames);
}
// For IE6, rewrite all classes on the decorated element if any combined
// classes apply.
if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('7')) {
var combinedClasses = this.getAppliedCombinedClassNames_(
classNames);
if (combinedClasses.length > 0) {
classNames.push.apply(classNames, combinedClasses);
hasCombinedClassName = true;
}
}
// Only write to the DOM if new class names had to be added to the element.
if (!hasRendererClassName || !hasStructuralClassName ||
extraClassNames || hasCombinedClassName) {
goog.dom.classes.set(element, classNames.join(' '));
}
this.setAriaStates(control, element);
return element;
};
/**
* Initializes the control's DOM by configuring properties that can only be set
* after the DOM has entered the document. This implementation sets up BiDi
* and keyboard focus. Called from {@link goog.ui.Control#enterDocument}.
* @param {goog.ui.Control} control Control whose DOM is to be initialized
* as it enters the document.
*/
goog.ui.ControlRenderer.prototype.initializeDom = function(control) {
// Initialize render direction (BiDi). We optimize the left-to-right render
// direction by assuming that elements are left-to-right by default, and only
// updating their styling if they are explicitly set to right-to-left.
if (control.isRightToLeft()) {
this.setRightToLeft(control.getElement(), true);
}
// Initialize keyboard focusability (tab index). We assume that components
// aren't focusable by default (i.e have no tab index), and only touch the
// DOM if the component is focusable, enabled, and visible, and therefore
// needs a tab index.
if (control.isEnabled()) {
this.setFocusable(control, control.isVisible());
}
};
/**
* Sets the element's ARIA role.
* @param {Element} element Element to update.
* @param {?goog.a11y.aria.Role=} opt_preferredRole The preferred ARIA role.
*/
goog.ui.ControlRenderer.prototype.setAriaRole = function(element,
opt_preferredRole) {
var ariaRole = opt_preferredRole || this.getAriaRole();
if (ariaRole) {
goog.asserts.assert(element,
'The element passed as a first parameter cannot be null.');
goog.a11y.aria.setRole(element, ariaRole);
}
};
/**
* Sets the element's ARIA states. An element does not need an ARIA role in
* order to have an ARIA state. Only states which are initialized to be true
* will be set.
* @param {!goog.ui.Control} control Control whose ARIA state will be updated.
* @param {!Element} element Element whose ARIA state is to be updated.
*/
goog.ui.ControlRenderer.prototype.setAriaStates = function(control, element) {
goog.asserts.assert(control);
goog.asserts.assert(element);
if (!control.isVisible()) {
goog.a11y.aria.setState(
element, goog.a11y.aria.State.HIDDEN, !control.isVisible());
}
if (!control.isEnabled()) {
this.updateAriaState(
element, goog.ui.Component.State.DISABLED, !control.isEnabled());
}
if (control.isSupportedState(goog.ui.Component.State.SELECTED)) {
this.updateAriaState(
element, goog.ui.Component.State.SELECTED, control.isSelected());
}
if (control.isSupportedState(goog.ui.Component.State.CHECKED)) {
this.updateAriaState(
element, goog.ui.Component.State.CHECKED, control.isChecked());
}
if (control.isSupportedState(goog.ui.Component.State.OPENED)) {
this.updateAriaState(
element, goog.ui.Component.State.OPENED, control.isOpen());
}
};
/**
* Allows or disallows text selection within the control's DOM.
* @param {Element} element The control's root element.
* @param {boolean} allow Whether the element should allow text selection.
*/
goog.ui.ControlRenderer.prototype.setAllowTextSelection = function(element,
allow) {
// On all browsers other than IE and Opera, it isn't necessary to recursively
// apply unselectable styling to the element's children.
goog.style.setUnselectable(element, !allow,
!goog.userAgent.IE && !goog.userAgent.OPERA);
};
/**
* Applies special styling to/from the control's element if it is rendered
* right-to-left, and removes it if it is rendered left-to-right.
* @param {Element} element The control's root element.
* @param {boolean} rightToLeft Whether the component is rendered
* right-to-left.
*/
goog.ui.ControlRenderer.prototype.setRightToLeft = function(element,
rightToLeft) {
this.enableClassName(element,
goog.getCssName(this.getStructuralCssClass(), 'rtl'), rightToLeft);
};
/**
* Returns true if the control's key event target supports keyboard focus
* (based on its {@code tabIndex} attribute), false otherwise.
* @param {goog.ui.Control} control Control whose key event target is to be
* checked.
* @return {boolean} Whether the control's key event target is focusable.
*/
goog.ui.ControlRenderer.prototype.isFocusable = function(control) {
var keyTarget;
if (control.isSupportedState(goog.ui.Component.State.FOCUSED) &&
(keyTarget = control.getKeyEventTarget())) {
return goog.dom.isFocusableTabIndex(keyTarget);
}
return false;
};
/**
* Updates the control's key event target to make it focusable or non-focusable
* via its {@code tabIndex} attribute. Does nothing if the control doesn't
* support the {@code FOCUSED} state, or if it has no key event target.
* @param {goog.ui.Control} control Control whose key event target is to be
* updated.
* @param {boolean} focusable Whether to enable keyboard focus support on the
* control's key event target.
*/
goog.ui.ControlRenderer.prototype.setFocusable = function(control, focusable) {
var keyTarget;
if (control.isSupportedState(goog.ui.Component.State.FOCUSED) &&
(keyTarget = control.getKeyEventTarget())) {
if (!focusable && control.isFocused()) {
// Blur before hiding. Note that IE calls onblur handlers asynchronously.
try {
keyTarget.blur();
} catch (e) {
// TODO(user|user): Find out why this fails on IE.
}
// The blur event dispatched by the key event target element when blur()
// was called on it should have been handled by the control's handleBlur()
// method, so at this point the control should no longer be focused.
// However, blur events are unreliable on IE and FF3, so if at this point
// the control is still focused, we trigger its handleBlur() method
// programmatically.
if (control.isFocused()) {
control.handleBlur(null);
}
}
// Don't overwrite existing tab index values unless needed.
if (goog.dom.isFocusableTabIndex(keyTarget) != focusable) {
goog.dom.setFocusableTabIndex(keyTarget, focusable);
}
}
};
/**
* Shows or hides the element.
* @param {Element} element Element to update.
* @param {boolean} visible Whether to show the element.
*/
goog.ui.ControlRenderer.prototype.setVisible = function(element, visible) {
// The base class implementation is trivial; subclasses should override as
// needed. It should be possible to do animated reveals, for example.
goog.style.setElementShown(element, visible);
if (element) {
goog.a11y.aria.setState(element, goog.a11y.aria.State.HIDDEN, !visible);
}
};
/**
* Updates the appearance of the control in response to a state change.
* @param {goog.ui.Control} control Control instance to update.
* @param {goog.ui.Component.State} state State to enable or disable.
* @param {boolean} enable Whether the control is entering or exiting the state.
*/
goog.ui.ControlRenderer.prototype.setState = function(control, state, enable) {
var element = control.getElement();
if (element) {
var className = this.getClassForState(state);
if (className) {
this.enableClassName(control, className, enable);
}
this.updateAriaState(element, state, enable);
}
};
/**
* Updates the element's ARIA (accessibility) state.
* @param {Element} element Element whose ARIA state is to be updated.
* @param {goog.ui.Component.State} state Component state being enabled or
* disabled.
* @param {boolean} enable Whether the state is being enabled or disabled.
* @protected
*/
goog.ui.ControlRenderer.prototype.updateAriaState = function(element, state,
enable) {
// Ensure the ARIA state map exists.
if (!goog.ui.ControlRenderer.ARIA_STATE_MAP_) {
goog.ui.ControlRenderer.ARIA_STATE_MAP_ = goog.object.create(
goog.ui.Component.State.DISABLED, goog.a11y.aria.State.DISABLED,
goog.ui.Component.State.SELECTED, goog.a11y.aria.State.SELECTED,
goog.ui.Component.State.CHECKED, goog.a11y.aria.State.CHECKED,
goog.ui.Component.State.OPENED, goog.a11y.aria.State.EXPANDED);
}
var ariaState = goog.ui.ControlRenderer.ARIA_STATE_MAP_[state];
if (ariaState) {
goog.asserts.assert(element,
'The element passed as a first parameter cannot be null.');
goog.a11y.aria.setState(element, ariaState, enable);
}
};
/**
* Takes a control's root element, and sets its content to the given text
* caption or DOM structure. The default implementation replaces the children
* of the given element. Renderers that create more complex DOM structures
* must override this method accordingly.
* @param {Element} element The control's root element.
* @param {goog.ui.ControlContent} content Text caption or DOM structure to be
* set as the control's content. The DOM nodes will not be cloned, they
* will only moved under the content element of the control.
*/
goog.ui.ControlRenderer.prototype.setContent = function(element, content) {
var contentElem = this.getContentElement(element);
if (contentElem) {
goog.dom.removeChildren(contentElem);
if (content) {
if (goog.isString(content)) {
goog.dom.setTextContent(contentElem, content);
} else {
var childHandler = function(child) {
if (child) {
var doc = goog.dom.getOwnerDocument(contentElem);
contentElem.appendChild(goog.isString(child) ?
doc.createTextNode(child) : child);
}
};
if (goog.isArray(content)) {
// Array of nodes.
goog.array.forEach(content, childHandler);
} else if (goog.isArrayLike(content) && !('nodeType' in content)) {
// NodeList. The second condition filters out TextNode which also has
// length attribute but is not array like. The nodes have to be cloned
// because childHandler removes them from the list during iteration.
goog.array.forEach(goog.array.clone(/** @type {NodeList} */(content)),
childHandler);
} else {
// Node or string.
childHandler(content);
}
}
}
}
};
/**
* Returns the element within the component's DOM that should receive keyboard
* focus (null if none). The default implementation returns the control's root
* element.
* @param {goog.ui.Control} control Control whose key event target is to be
* returned.
* @return {Element} The key event target.
*/
goog.ui.ControlRenderer.prototype.getKeyEventTarget = function(control) {
return control.getElement();
};
// CSS class name management.
/**
* Returns the CSS class name to be applied to the root element of all
* components rendered or decorated using this renderer. The class name
* is expected to uniquely identify the renderer class, i.e. no two
* renderer classes are expected to share the same CSS class name.
* @return {string} Renderer-specific CSS class name.
*/
goog.ui.ControlRenderer.prototype.getCssClass = function() {
return goog.ui.ControlRenderer.CSS_CLASS;
};
/**
* Returns an array of combinations of classes to apply combined class names for
* in IE6 and below. See {@link IE6_CLASS_COMBINATIONS} for more detail. This
* method doesn't reference {@link IE6_CLASS_COMBINATIONS} so that it can be
* compiled out, but subclasses should return their IE6_CLASS_COMBINATIONS
* static constant instead.
* @return {Array.<Array.<string>>} Array of class name combinations.
*/
goog.ui.ControlRenderer.prototype.getIe6ClassCombinations = function() {
return [];
};
/**
* Returns the name of a DOM structure-specific CSS class to be applied to the
* root element of all components rendered or decorated using this renderer.
* Unlike the class name returned by {@link #getCssClass}, the structural class
* name may be shared among different renderers that generate similar DOM
* structures. The structural class name also serves as the basis of derived
* class names used to identify and style structural elements of the control's
* DOM, as well as the basis for state-specific class names. The default
* implementation returns the same class name as {@link #getCssClass}, but
* subclasses are expected to override this method as needed.
* @return {string} DOM structure-specific CSS class name (same as the renderer-
* specific CSS class name by default).
*/
goog.ui.ControlRenderer.prototype.getStructuralCssClass = function() {
return this.getCssClass();
};
/**
* Returns all CSS class names applicable to the given control, based on its
* state. The return value is an array of strings containing
* <ol>
* <li>the renderer-specific CSS class returned by {@link #getCssClass},
* followed by
* <li>the structural CSS class returned by {@link getStructuralCssClass} (if
* different from the renderer-specific CSS class), followed by
* <li>any state-specific classes returned by {@link #getClassNamesForState},
* followed by
* <li>any extra classes returned by the control's {@code getExtraClassNames}
* method and
* <li>for IE6 and lower, additional combined classes from
* {@link getAppliedCombinedClassNames_}.
* </ol>
* Since all controls have at least one renderer-specific CSS class name, this
* method is guaranteed to return an array of at least one element.
* @param {goog.ui.Control} control Control whose CSS classes are to be
* returned.
* @return {Array.<string>} Array of CSS class names applicable to the control.
* @protected
*/
goog.ui.ControlRenderer.prototype.getClassNames = function(control) {
var cssClass = this.getCssClass();
// Start with the renderer-specific class name.
var classNames = [cssClass];
// Add structural class name, if different.
var structuralCssClass = this.getStructuralCssClass();
if (structuralCssClass != cssClass) {
classNames.push(structuralCssClass);
}
// Add state-specific class names, if any.
var classNamesForState = this.getClassNamesForState(control.getState());
classNames.push.apply(classNames, classNamesForState);
// Add extra class names, if any.
var extraClassNames = control.getExtraClassNames();
if (extraClassNames) {
classNames.push.apply(classNames, extraClassNames);
}
// Add composite classes for IE6 support
if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('7')) {
classNames.push.apply(classNames,
this.getAppliedCombinedClassNames_(classNames));
}
return classNames;
};
/**
* Returns an array of all the combined class names that should be applied based
* on the given list of classes. Checks the result of
* {@link getIe6ClassCombinations} for any combinations that have all
* members contained in classes. If a combination matches, the members are
* joined with an underscore (in order), and added to the return array.
*
* If opt_includedClass is provided, return only the combined classes that have
* all members contained in classes AND include opt_includedClass as well.
* opt_includedClass is added to classes as well.
* @param {Array.<string>} classes Array of classes to return matching combined
* classes for.
* @param {?string=} opt_includedClass If provided, get only the combined
* classes that include this one.
* @return {Array.<string>} Array of combined class names that should be
* applied.
* @private
*/
goog.ui.ControlRenderer.prototype.getAppliedCombinedClassNames_ = function(
classes, opt_includedClass) {
var toAdd = [];
if (opt_includedClass) {
classes = classes.concat([opt_includedClass]);
}
goog.array.forEach(this.getIe6ClassCombinations(), function(combo) {
if (goog.array.every(combo, goog.partial(goog.array.contains, classes)) &&
(!opt_includedClass || goog.array.contains(combo, opt_includedClass))) {
toAdd.push(combo.join('_'));
}
});
return toAdd;
};
/**
* Takes a bit mask of {@link goog.ui.Component.State}s, and returns an array
* of the appropriate class names representing the given state, suitable to be
* applied to the root element of a component rendered using this renderer, or
* null if no state-specific classes need to be applied. This default
* implementation uses the renderer's {@link getClassForState} method to
* generate each state-specific class.
* @param {number} state Bit mask of component states.
* @return {!Array.<string>} Array of CSS class names representing the given
* state.
* @protected
*/
goog.ui.ControlRenderer.prototype.getClassNamesForState = function(state) {
var classNames = [];
while (state) {
// For each enabled state, push the corresponding CSS class name onto
// the classNames array.
var mask = state & -state; // Least significant bit
classNames.push(this.getClassForState(
/** @type {goog.ui.Component.State} */ (mask)));
state &= ~mask;
}
return classNames;
};
/**
* Takes a single {@link goog.ui.Component.State}, and returns the
* corresponding CSS class name (null if none).
* @param {goog.ui.Component.State} state Component state.
* @return {string|undefined} CSS class representing the given state (undefined
* if none).
* @protected
*/
goog.ui.ControlRenderer.prototype.getClassForState = function(state) {
if (!this.classByState_) {
this.createClassByStateMap_();
}
return this.classByState_[state];
};
/**
* Takes a single CSS class name which may represent a component state, and
* returns the corresponding component state (0x00 if none).
* @param {string} className CSS class name, possibly representing a component
* state.
* @return {goog.ui.Component.State} state Component state corresponding
* to the given CSS class (0x00 if none).
* @protected
*/
goog.ui.ControlRenderer.prototype.getStateFromClass = function(className) {
if (!this.stateByClass_) {
this.createStateByClassMap_();
}
var state = parseInt(this.stateByClass_[className], 10);
return /** @type {goog.ui.Component.State} */ (isNaN(state) ? 0x00 : state);
};
/**
* Creates the lookup table of states to classes, used during state changes.
* @private
*/
goog.ui.ControlRenderer.prototype.createClassByStateMap_ = function() {
var baseClass = this.getStructuralCssClass();
/**
* Map of component states to state-specific structural class names,
* used when changing the DOM in response to a state change. Precomputed
* and cached on first use to minimize object allocations and string
* concatenation.
* @type {Object}
* @private
*/
this.classByState_ = goog.object.create(
goog.ui.Component.State.DISABLED, goog.getCssName(baseClass, 'disabled'),
goog.ui.Component.State.HOVER, goog.getCssName(baseClass, 'hover'),
goog.ui.Component.State.ACTIVE, goog.getCssName(baseClass, 'active'),
goog.ui.Component.State.SELECTED, goog.getCssName(baseClass, 'selected'),
goog.ui.Component.State.CHECKED, goog.getCssName(baseClass, 'checked'),
goog.ui.Component.State.FOCUSED, goog.getCssName(baseClass, 'focused'),
goog.ui.Component.State.OPENED, goog.getCssName(baseClass, 'open'));
};
/**
* Creates the lookup table of classes to states, used during decoration.
* @private
*/
goog.ui.ControlRenderer.prototype.createStateByClassMap_ = function() {
// We need the classByState_ map so we can transpose it.
if (!this.classByState_) {
this.createClassByStateMap_();
}
/**
* Map of state-specific structural class names to component states,
* used during element decoration. Precomputed and cached on first use
* to minimize object allocations and string concatenation.
* @type {Object}
* @private
*/
this.stateByClass_ = goog.object.transpose(this.classByState_);
};

View File

@@ -0,0 +1,183 @@
// 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 Displays and edits the value of a cookie.
* Intended only for debugging.
*/
goog.provide('goog.ui.CookieEditor');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.events.EventType');
goog.require('goog.net.cookies');
goog.require('goog.string');
goog.require('goog.style');
goog.require('goog.ui.Component');
/**
* Displays and edits the value of a cookie.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @constructor
* @extends {goog.ui.Component}
*/
goog.ui.CookieEditor = function(opt_domHelper) {
goog.base(this, opt_domHelper);
};
goog.inherits(goog.ui.CookieEditor, goog.ui.Component);
/**
* Cookie key.
* @type {?string}
* @private
*/
goog.ui.CookieEditor.prototype.cookieKey_;
/**
* Text area.
* @type {HTMLTextAreaElement}
* @private
*/
goog.ui.CookieEditor.prototype.textAreaElem_;
/**
* Clear button.
* @type {HTMLButtonElement}
* @private
*/
goog.ui.CookieEditor.prototype.clearButtonElem_;
/**
* Invalid value warning text.
* @type {HTMLSpanElement}
* @private
*/
goog.ui.CookieEditor.prototype.valueWarningElem_;
/**
* Update button.
* @type {HTMLButtonElement}
* @private
*/
goog.ui.CookieEditor.prototype.updateButtonElem_;
// TODO(user): add combobox for user to select different cookies
/**
* Sets the cookie which this component will edit.
* @param {string} cookieKey Cookie key.
*/
goog.ui.CookieEditor.prototype.selectCookie = function(cookieKey) {
goog.asserts.assert(goog.net.cookies.isValidName(cookieKey));
this.cookieKey_ = cookieKey;
if (this.textAreaElem_) {
this.textAreaElem_.value = goog.net.cookies.get(cookieKey) || '';
}
};
/** @override */
goog.ui.CookieEditor.prototype.canDecorate = function() {
return false;
};
/** @override */
goog.ui.CookieEditor.prototype.createDom = function() {
// Debug-only, so we don't need i18n.
this.clearButtonElem_ = /** @type {HTMLButtonElement} */ (goog.dom.createDom(
goog.dom.TagName.BUTTON, /* attributes */ null, 'Clear'));
this.updateButtonElem_ = /** @type {HTMLButtonElement} */ (goog.dom.createDom(
goog.dom.TagName.BUTTON, /* attributes */ null, 'Update'));
var value = this.cookieKey_ && goog.net.cookies.get(this.cookieKey_);
this.textAreaElem_ = /** @type {HTMLTextAreaElement} */ (goog.dom.createDom(
goog.dom.TagName.TEXTAREA, /* attibutes */ null, value || ''));
this.valueWarningElem_ = /** @type {HTMLSpanElement} */ (goog.dom.createDom(
goog.dom.TagName.SPAN, /* attibutes */ {
'style': 'display:none;color:red'
}, 'Invalid cookie value.'));
this.setElementInternal(goog.dom.createDom(goog.dom.TagName.DIV,
/* attibutes */ null,
this.valueWarningElem_,
goog.dom.createDom(goog.dom.TagName.BR),
this.textAreaElem_,
goog.dom.createDom(goog.dom.TagName.BR),
this.clearButtonElem_,
this.updateButtonElem_));
};
/** @override */
goog.ui.CookieEditor.prototype.enterDocument = function() {
goog.base(this, 'enterDocument');
this.getHandler().listen(this.clearButtonElem_,
goog.events.EventType.CLICK,
this.handleClear_);
this.getHandler().listen(this.updateButtonElem_,
goog.events.EventType.CLICK,
this.handleUpdate_);
};
/**
* Handles user clicking clear button.
* @param {!goog.events.Event} e The click event.
* @private
*/
goog.ui.CookieEditor.prototype.handleClear_ = function(e) {
if (this.cookieKey_) {
goog.net.cookies.remove(this.cookieKey_);
}
this.textAreaElem_.value = '';
};
/**
* Handles user clicking update button.
* @param {!goog.events.Event} e The click event.
* @private
*/
goog.ui.CookieEditor.prototype.handleUpdate_ = function(e) {
if (this.cookieKey_) {
var value = this.textAreaElem_.value;
if (value) {
// Strip line breaks.
value = goog.string.stripNewlines(value);
}
if (goog.net.cookies.isValidValue(value)) {
goog.net.cookies.set(this.cookieKey_, value);
goog.style.setElementShown(this.valueWarningElem_, false);
} else {
goog.style.setElementShown(this.valueWarningElem_, true);
}
}
};
/** @override */
goog.ui.CookieEditor.prototype.disposeInternal = function() {
this.clearButtonElem_ = null;
this.cookieKey_ = null;
this.textAreaElem_ = null;
this.updateButtonElem_ = null;
this.valueWarningElem_ = null;
};

View File

@@ -0,0 +1,152 @@
// 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 An alternative imageless button renderer that uses CSS3 rather
* than voodoo to render custom buttons with rounded corners and dimensionality
* (via a subtle flat shadow on the bottom half of the button) without the use
* of images.
*
* Based on the Custom Buttons 3.1 visual specification, see
* http://go/custombuttons
*
* Tested and verified to work in Gecko 1.9.2+ and WebKit 528+.
*
* @author eae@google.com (Emil A Eklund)
* @author slightlyoff@google.com (Alex Russell)
* @see ../demos/css3button.html
*/
goog.provide('goog.ui.Css3ButtonRenderer');
goog.require('goog.dom.TagName');
goog.require('goog.dom.classes');
goog.require('goog.ui.Button');
goog.require('goog.ui.ButtonRenderer');
goog.require('goog.ui.Component');
goog.require('goog.ui.INLINE_BLOCK_CLASSNAME');
goog.require('goog.ui.registry');
/**
* Custom renderer for {@link goog.ui.Button}s. Css3 buttons can contain
* almost arbitrary HTML content, will flow like inline elements, but can be
* styled like block-level elements.
*
* @constructor
* @extends {goog.ui.ButtonRenderer}
*/
goog.ui.Css3ButtonRenderer = function() {
goog.ui.ButtonRenderer.call(this);
};
goog.inherits(goog.ui.Css3ButtonRenderer, goog.ui.ButtonRenderer);
/**
* The singleton instance of this renderer class.
* @type {goog.ui.Css3ButtonRenderer?}
* @private
*/
goog.ui.Css3ButtonRenderer.instance_ = null;
goog.addSingletonGetter(goog.ui.Css3ButtonRenderer);
/**
* Default CSS class to be applied to the root element of components rendered
* by this renderer.
* @type {string}
*/
goog.ui.Css3ButtonRenderer.CSS_CLASS = goog.getCssName('goog-css3-button');
/** @override */
goog.ui.Css3ButtonRenderer.prototype.getContentElement = function(element) {
return /** @type {Element} */ (element);
};
/**
* Returns the button's contents wrapped in the following DOM structure:
* <div class="goog-inline-block goog-css3-button">
* Contents...
* </div>
* Overrides {@link goog.ui.ButtonRenderer#createDom}.
* @param {goog.ui.Control} control goog.ui.Button to render.
* @return {Element} Root element for the button.
* @override
*/
goog.ui.Css3ButtonRenderer.prototype.createDom = function(control) {
var button = /** @type {goog.ui.Button} */ (control);
var classNames = this.getClassNames(button);
var attr = {
'class': goog.ui.INLINE_BLOCK_CLASSNAME + ' ' + classNames.join(' '),
'title': button.getTooltip() || ''
};
return button.getDomHelper().createDom('div', attr, button.getContent());
};
/**
* Returns true if this renderer can decorate the element. Overrides
* {@link goog.ui.ButtonRenderer#canDecorate} by returning true if the
* element is a DIV, false otherwise.
* @param {Element} element Element to decorate.
* @return {boolean} Whether the renderer can decorate the element.
* @override
*/
goog.ui.Css3ButtonRenderer.prototype.canDecorate = function(element) {
return element.tagName == goog.dom.TagName.DIV;
};
/** @override */
goog.ui.Css3ButtonRenderer.prototype.decorate = function(button, element) {
goog.dom.classes.add(element, goog.ui.INLINE_BLOCK_CLASSNAME,
this.getCssClass());
return goog.ui.Css3ButtonRenderer.superClass_.decorate.call(this, button,
element);
};
/**
* Returns the CSS class to be applied to the root element of components
* rendered using this renderer.
* @return {string} Renderer-specific CSS class.
* @override
*/
goog.ui.Css3ButtonRenderer.prototype.getCssClass = function() {
return goog.ui.Css3ButtonRenderer.CSS_CLASS;
};
// Register a decorator factory function for goog.ui.Css3ButtonRenderer.
goog.ui.registry.setDecoratorByClassName(
goog.ui.Css3ButtonRenderer.CSS_CLASS,
function() {
return new goog.ui.Button(null,
goog.ui.Css3ButtonRenderer.getInstance());
});
// Register a decorator factory function for toggle buttons using the
// goog.ui.Css3ButtonRenderer.
goog.ui.registry.setDecoratorByClassName(
goog.getCssName('goog-css3-toggle-button'),
function() {
var button = new goog.ui.Button(null,
goog.ui.Css3ButtonRenderer.getInstance());
button.setSupportedState(goog.ui.Component.State.CHECKED, true);
return button;
});

View File

@@ -0,0 +1,147 @@
// 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 An alternative imageless button renderer that uses CSS3 rather
* than voodoo to render custom buttons with rounded corners and dimensionality
* (via a subtle flat shadow on the bottom half of the button) without the use
* of images.
*
* Based on the Custom Buttons 3.1 visual specification, see
* http://go/custombuttons
*
* Tested and verified to work in Gecko 1.9.2+ and WebKit 528+.
*
* @author eae@google.com (Emil A Eklund)
* @author slightlyoff@google.com (Alex Russell)
* @author dalewis@google.com (Darren Lewis)
* @see ../demos/css3menubutton.html
*/
goog.provide('goog.ui.Css3MenuButtonRenderer');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.ui.INLINE_BLOCK_CLASSNAME');
goog.require('goog.ui.MenuButton');
goog.require('goog.ui.MenuButtonRenderer');
goog.require('goog.ui.registry');
/**
* Custom renderer for {@link goog.ui.MenuButton}s. Css3 buttons can contain
* almost arbitrary HTML content, will flow like inline elements, but can be
* styled like block-level elements.
*
* @constructor
* @extends {goog.ui.MenuButtonRenderer}
*/
goog.ui.Css3MenuButtonRenderer = function() {
goog.ui.MenuButtonRenderer.call(this);
};
goog.inherits(goog.ui.Css3MenuButtonRenderer, goog.ui.MenuButtonRenderer);
/**
* The singleton instance of this renderer class.
* @type {goog.ui.Css3MenuButtonRenderer?}
* @private
*/
goog.ui.Css3MenuButtonRenderer.instance_ = null;
goog.addSingletonGetter(goog.ui.Css3MenuButtonRenderer);
/**
* Default CSS class to be applied to the root element of components rendered
* by this renderer.
* @type {string}
*/
goog.ui.Css3MenuButtonRenderer.CSS_CLASS = goog.getCssName('goog-css3-button');
/** @override */
goog.ui.Css3MenuButtonRenderer.prototype.getContentElement = function(element) {
if (element) {
var captionElem = goog.dom.getElementsByTagNameAndClass(
'*', goog.getCssName(this.getCssClass(), 'caption'), element)[0];
return captionElem;
}
return null;
};
/**
* Returns true if this renderer can decorate the element. Overrides
* {@link goog.ui.MenuButtonRenderer#canDecorate} by returning true if the
* element is a DIV, false otherwise.
* @param {Element} element Element to decorate.
* @return {boolean} Whether the renderer can decorate the element.
* @override
*/
goog.ui.Css3MenuButtonRenderer.prototype.canDecorate = function(element) {
return element.tagName == goog.dom.TagName.DIV;
};
/**
* Takes a text caption or existing DOM structure, and returns the content
* wrapped in a pseudo-rounded-corner box. Creates the following DOM structure:
* <div class="goog-inline-block goog-css3-button goog-css3-menu-button">
* <div class="goog-css3-button-caption">Contents...</div>
* <div class="goog-css3-button-dropdown"></div>
* </div>
*
* Used by both {@link #createDom} and {@link #decorate}. To be overridden
* by subclasses.
* @param {goog.ui.ControlContent} content Text caption or DOM structure to wrap
* in a box.
* @param {goog.dom.DomHelper} dom DOM helper, used for document interaction.
* @return {Element} Pseudo-rounded-corner box containing the content.
* @override
*/
goog.ui.Css3MenuButtonRenderer.prototype.createButton = function(content, dom) {
var baseClass = this.getCssClass();
var inlineBlock = goog.ui.INLINE_BLOCK_CLASSNAME + ' ';
return dom.createDom('div', inlineBlock,
dom.createDom('div', [goog.getCssName(baseClass, 'caption'),
goog.getCssName('goog-inline-block')],
content),
dom.createDom('div', [goog.getCssName(baseClass, 'dropdown'),
goog.getCssName('goog-inline-block')]));
};
/**
* Returns the CSS class to be applied to the root element of components
* rendered using this renderer.
* @return {string} Renderer-specific CSS class.
* @override
*/
goog.ui.Css3MenuButtonRenderer.prototype.getCssClass = function() {
return goog.ui.Css3MenuButtonRenderer.CSS_CLASS;
};
// Register a decorator factory function for goog.ui.Css3MenuButtonRenderer.
// Since we're using goog-css3-button as the base class in order to get the
// same styling as goog.ui.Css3ButtonRenderer, we need to be explicit about
// giving goog-css3-menu-button here.
goog.ui.registry.setDecoratorByClassName(
goog.getCssName('goog-css3-menu-button'),
function() {
return new goog.ui.MenuButton(null, null,
goog.ui.Css3MenuButtonRenderer.getInstance());
});

View File

@@ -0,0 +1,29 @@
// Copyright 2009 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 Common CSS class name constants.
*
* @author mkretzschmar@google.com (Martin Kretzschmar)
*/
goog.provide('goog.ui.INLINE_BLOCK_CLASSNAME');
/**
* CSS class name for applying the "display: inline-block" property in a
* cross-browser way.
* @type {string}
*/
goog.ui.INLINE_BLOCK_CLASSNAME = goog.getCssName('goog-inline-block');

View File

@@ -0,0 +1,58 @@
// Copyright 2007 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 button rendered via {@link goog.ui.CustomButtonRenderer}.
*
* @author attila@google.com (Attila Bodis)
*/
goog.provide('goog.ui.CustomButton');
goog.require('goog.ui.Button');
goog.require('goog.ui.CustomButtonRenderer');
goog.require('goog.ui.registry');
/**
* A custom button control. Identical to {@link goog.ui.Button}, except it
* defaults its renderer to {@link goog.ui.CustomButtonRenderer}. One could
* just as easily pass {@code goog.ui.CustomButtonRenderer.getInstance()} to
* the {@link goog.ui.Button} constructor and get the same result. Provided
* for convenience.
*
* @param {goog.ui.ControlContent} content Text caption or existing DOM
* structure to display as the button's caption.
* @param {goog.ui.ButtonRenderer=} opt_renderer Optional renderer used to
* render or decorate the button; defaults to
* {@link goog.ui.CustomButtonRenderer}.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM hepler, used for
* document interaction.
* @constructor
* @extends {goog.ui.Button}
*/
goog.ui.CustomButton = function(content, opt_renderer, opt_domHelper) {
goog.ui.Button.call(this, content, opt_renderer ||
goog.ui.CustomButtonRenderer.getInstance(), opt_domHelper);
};
goog.inherits(goog.ui.CustomButton, goog.ui.Button);
// Register a decorator factory function for goog.ui.CustomButtons.
goog.ui.registry.setDecoratorByClassName(goog.ui.CustomButtonRenderer.CSS_CLASS,
function() {
// CustomButton defaults to using CustomButtonRenderer.
return new goog.ui.CustomButton(null);
});

View File

@@ -0,0 +1,267 @@
// 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 custom button renderer that uses CSS voodoo to render a
* button-like object with fake rounded corners.
*
* @author attila@google.com (Attila Bodis)
*/
goog.provide('goog.ui.CustomButtonRenderer');
goog.require('goog.a11y.aria.Role');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.classes');
goog.require('goog.string');
goog.require('goog.ui.ButtonRenderer');
goog.require('goog.ui.INLINE_BLOCK_CLASSNAME');
/**
* Custom renderer for {@link goog.ui.Button}s. Custom buttons can contain
* almost arbitrary HTML content, will flow like inline elements, but can be
* styled like block-level elements.
*
* @constructor
* @extends {goog.ui.ButtonRenderer}
*/
goog.ui.CustomButtonRenderer = function() {
goog.ui.ButtonRenderer.call(this);
};
goog.inherits(goog.ui.CustomButtonRenderer, goog.ui.ButtonRenderer);
goog.addSingletonGetter(goog.ui.CustomButtonRenderer);
/**
* Default CSS class to be applied to the root element of components rendered
* by this renderer.
* @type {string}
*/
goog.ui.CustomButtonRenderer.CSS_CLASS = goog.getCssName('goog-custom-button');
/**
* Returns the button's contents wrapped in the following DOM structure:
* <div class="goog-inline-block goog-custom-button">
* <div class="goog-inline-block goog-custom-button-outer-box">
* <div class="goog-inline-block goog-custom-button-inner-box">
* Contents...
* </div>
* </div>
* </div>
* Overrides {@link goog.ui.ButtonRenderer#createDom}.
* @param {goog.ui.Control} control goog.ui.Button to render.
* @return {Element} Root element for the button.
* @override
*/
goog.ui.CustomButtonRenderer.prototype.createDom = function(control) {
var button = /** @type {goog.ui.Button} */ (control);
var classNames = this.getClassNames(button);
var attributes = {
'class': goog.ui.INLINE_BLOCK_CLASSNAME + ' ' + classNames.join(' ')
};
var buttonElement = button.getDomHelper().createDom('div', attributes,
this.createButton(button.getContent(), button.getDomHelper()));
this.setTooltip(
buttonElement, /** @type {!string}*/ (button.getTooltip()));
this.setAriaStates(button, buttonElement);
return buttonElement;
};
/**
* Returns the ARIA role to be applied to custom buttons.
* @return {goog.a11y.aria.Role|undefined} ARIA role.
* @override
*/
goog.ui.CustomButtonRenderer.prototype.getAriaRole = function() {
return goog.a11y.aria.Role.BUTTON;
};
/**
* Takes the button's root element and returns the parent element of the
* button's contents. Overrides the superclass implementation by taking
* the nested DIV structure of custom buttons into account.
* @param {Element} element Root element of the button whose content
* element is to be returned.
* @return {Element} The button's content element (if any).
* @override
*/
goog.ui.CustomButtonRenderer.prototype.getContentElement = function(element) {
return element && /** @type {Element} */ (element.firstChild.firstChild);
};
/**
* Takes a text caption or existing DOM structure, and returns the content
* wrapped in a pseudo-rounded-corner box. Creates the following DOM structure:
* <div class="goog-inline-block goog-custom-button-outer-box">
* <div class="goog-inline-block goog-custom-button-inner-box">
* Contents...
* </div>
* </div>
* Used by both {@link #createDom} and {@link #decorate}. To be overridden
* by subclasses.
* @param {goog.ui.ControlContent} content Text caption or DOM structure to wrap
* in a box.
* @param {goog.dom.DomHelper} dom DOM helper, used for document interaction.
* @return {Element} Pseudo-rounded-corner box containing the content.
*/
goog.ui.CustomButtonRenderer.prototype.createButton = function(content, dom) {
return dom.createDom('div',
goog.ui.INLINE_BLOCK_CLASSNAME + ' ' +
goog.getCssName(this.getCssClass(), 'outer-box'),
dom.createDom('div',
goog.ui.INLINE_BLOCK_CLASSNAME + ' ' +
goog.getCssName(this.getCssClass(), 'inner-box'), content));
};
/**
* Returns true if this renderer can decorate the element. Overrides
* {@link goog.ui.ButtonRenderer#canDecorate} by returning true if the
* element is a DIV, false otherwise.
* @param {Element} element Element to decorate.
* @return {boolean} Whether the renderer can decorate the element.
* @override
*/
goog.ui.CustomButtonRenderer.prototype.canDecorate = function(element) {
return element.tagName == 'DIV';
};
/**
* Check if the button's element has a box structure.
* @param {goog.ui.Button} button Button instance whose structure is being
* checked.
* @param {Element} element Element of the button.
* @return {boolean} Whether the element has a box structure.
* @protected
*/
goog.ui.CustomButtonRenderer.prototype.hasBoxStructure = function(
button, element) {
var outer = button.getDomHelper().getFirstElementChild(element);
var outerClassName = goog.getCssName(this.getCssClass(), 'outer-box');
if (outer && goog.dom.classes.has(outer, outerClassName)) {
var inner = button.getDomHelper().getFirstElementChild(outer);
var innerClassName = goog.getCssName(this.getCssClass(), 'inner-box');
if (inner && goog.dom.classes.has(inner, innerClassName)) {
// We have a proper box structure.
return true;
}
}
return false;
};
/**
* Takes an existing element and decorates it with the custom button control.
* Initializes the control's ID, content, tooltip, value, and state based
* on the ID of the element, its child nodes, and its CSS classes, respectively.
* Returns the element. Overrides {@link goog.ui.ButtonRenderer#decorate}.
* @param {goog.ui.Control} control Button instance to decorate the element.
* @param {Element} element Element to decorate.
* @return {Element} Decorated element.
* @override
*/
goog.ui.CustomButtonRenderer.prototype.decorate = function(control, element) {
var button = /** @type {goog.ui.Button} */ (control);
// Trim text nodes in the element's child node list; otherwise madness
// ensues (i.e. on Gecko, buttons will flicker and shift when moused over).
goog.ui.CustomButtonRenderer.trimTextNodes_(element, true);
goog.ui.CustomButtonRenderer.trimTextNodes_(element, false);
// Create the buttom dom if it has not been created.
if (!this.hasBoxStructure(button, element)) {
element.appendChild(
this.createButton(element.childNodes, button.getDomHelper()));
}
goog.dom.classes.add(element,
goog.ui.INLINE_BLOCK_CLASSNAME, this.getCssClass());
return goog.ui.CustomButtonRenderer.superClass_.decorate.call(this, button,
element);
};
/**
* Returns the CSS class to be applied to the root element of components
* rendered using this renderer.
* @return {string} Renderer-specific CSS class.
* @override
*/
goog.ui.CustomButtonRenderer.prototype.getCssClass = function() {
return goog.ui.CustomButtonRenderer.CSS_CLASS;
};
/**
* Takes an element and removes leading or trailing whitespace from the start
* or the end of its list of child nodes. The Boolean argument determines
* whether to trim from the start or the end of the node list. Empty text
* nodes are removed, and the first non-empty text node is trimmed from the
* left or the right as appropriate. For example,
* <div class="goog-inline-block">
* #text ""
* #text "\n Hello "
* <span>...</span>
* #text " World! \n"
* #text ""
* </div>
* becomes
* <div class="goog-inline-block">
* #text "Hello "
* <span>...</span>
* #text " World!"
* </div>
* This is essential for Gecko, where leading/trailing whitespace messes with
* the layout of elements with -moz-inline-box (used in goog-inline-block), and
* optional but harmless for non-Gecko.
*
* @param {Element} element Element whose child node list is to be trimmed.
* @param {boolean} fromStart Whether to trim from the start or from the end.
* @private
*/
goog.ui.CustomButtonRenderer.trimTextNodes_ = function(element, fromStart) {
if (element) {
var node = fromStart ? element.firstChild : element.lastChild, next;
// Tag soup HTML may result in a DOM where siblings have different parents.
while (node && node.parentNode == element) {
// Get the next/previous sibling here, since the node may be removed.
next = fromStart ? node.nextSibling : node.previousSibling;
if (node.nodeType == goog.dom.NodeType.TEXT) {
// Found a text node.
var text = node.nodeValue;
if (goog.string.trim(text) == '') {
// Found an empty text node; remove it.
element.removeChild(node);
} else {
// Found a non-empty text node; trim from the start/end, then exit.
node.nodeValue = fromStart ?
goog.string.trimLeft(text) : goog.string.trimRight(text);
break;
}
} else {
// Found a non-text node; done.
break;
}
node = next;
}
}
};

View File

@@ -0,0 +1,139 @@
// Copyright 2007 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 color palette with a button for adding additional colors
* manually.
*
*/
goog.provide('goog.ui.CustomColorPalette');
goog.require('goog.color');
goog.require('goog.dom');
goog.require('goog.dom.classes');
goog.require('goog.ui.ColorPalette');
goog.require('goog.ui.Component');
/**
* A custom color palette is a grid of color swatches and a button that allows
* the user to add additional colors to the palette
*
* @param {Array.<string>} initColors Array of initial colors to populate the
* palette with.
* @param {goog.ui.PaletteRenderer=} opt_renderer Renderer used to render or
* decorate the palette; defaults to {@link goog.ui.PaletteRenderer}.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for
* document interaction.
* @constructor
* @extends {goog.ui.ColorPalette}
*/
goog.ui.CustomColorPalette = function(initColors, opt_renderer, opt_domHelper) {
goog.ui.ColorPalette.call(this, initColors, opt_renderer, opt_domHelper);
this.setSupportedState(goog.ui.Component.State.OPENED, true);
};
goog.inherits(goog.ui.CustomColorPalette, goog.ui.ColorPalette);
/**
* Returns an array of DOM nodes for each color, and an additional cell with a
* '+'.
* @return {Array.<Node>} Array of div elements.
* @override
*/
goog.ui.CustomColorPalette.prototype.createColorNodes = function() {
/** @desc Hover caption for the button that allows the user to add a color. */
var MSG_CLOSURE_CUSTOM_COLOR_BUTTON = goog.getMsg('Add a color');
var nl = goog.base(this, 'createColorNodes');
nl.push(goog.dom.createDom('div', {
'class': goog.getCssName('goog-palette-customcolor'),
'title': MSG_CLOSURE_CUSTOM_COLOR_BUTTON
}, '+'));
return nl;
};
/**
* @override
* @param {goog.events.Event} e Mouse or key event that triggered the action.
* @return {boolean} True if the action was allowed to proceed, false otherwise.
*/
goog.ui.CustomColorPalette.prototype.performActionInternal = function(e) {
var item = /** @type {Element} */ (this.getHighlightedItem());
if (item) {
if (goog.dom.classes.has(
item, goog.getCssName('goog-palette-customcolor'))) {
// User activated the special "add custom color" swatch.
this.promptForCustomColor();
} else {
// User activated a normal color swatch.
this.setSelectedItem(item);
return this.dispatchEvent(goog.ui.Component.EventType.ACTION);
}
}
return false;
};
/**
* Prompts the user to enter a custom color. Currently uses a window.prompt
* but could be updated to use a dialog box with a WheelColorPalette.
*/
goog.ui.CustomColorPalette.prototype.promptForCustomColor = function() {
/** @desc Default custom color dialog. */
var MSG_CLOSURE_CUSTOM_COLOR_PROMPT = goog.getMsg(
'Input custom color, i.e. pink, #F00, #D015FF or rgb(100, 50, 25)');
// A CustomColorPalette is considered "open" while the color selection prompt
// is open. Enabling state transition events for the OPENED state and
// listening for OPEN events allows clients to save the selection before
// it is destroyed (see e.g. bug 1064701).
var response = null;
this.setOpen(true);
if (this.isOpen()) {
// The OPEN event wasn't canceled; prompt for custom color.
response = window.prompt(MSG_CLOSURE_CUSTOM_COLOR_PROMPT, '#FFFFFF');
this.setOpen(false);
}
if (!response) {
// The user hit cancel
return;
}
var color;
/** @preserveTry */
try {
color = goog.color.parse(response).hex;
} catch (er) {
/** @desc Alert message sent when the input string is not a valid color. */
var MSG_CLOSURE_CUSTOM_COLOR_INVALID_INPUT = goog.getMsg(
'ERROR: "{$color}" is not a valid color.', {'color': response});
alert(MSG_CLOSURE_CUSTOM_COLOR_INVALID_INPUT);
return;
}
// TODO(user): This is relatively inefficient. Consider adding
// functionality to palette to add individual items after render time.
var colors = this.getColors();
colors.push(color);
this.setColors(colors);
// Set the selected color to the new color and notify listeners of the action.
this.setSelectedColor(color);
this.dispatchEvent(goog.ui.Component.EventType.ACTION);
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
// Copyright 2013 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 The renderer interface for {@link goog.ui.DatePicker}.
*
* @see ../demos/datepicker.html
*/
goog.provide('goog.ui.DatePickerRenderer');
/**
* The renderer for {@link goog.ui.DatePicker}. Renders the date picker's
* navigation header and footer.
* @interface
*/
goog.ui.DatePickerRenderer = function() {};
/**
* Render the navigation row.
*
* @param {!Element} row The parent element to render the component into.
* @param {boolean} simpleNavigation Whether the picker should render a simple
* navigation menu that only contains controls for navigating to the next
* and previous month. The default navigation menu contains controls for
* navigating to the next/previous month, next/previous year, and menus for
* jumping to specific months and years.
* @param {boolean} showWeekNum Whether week numbers should be shown.
* @param {string} fullDateFormat The full date format.
* {@see goog.i18n.DateTimeSymbols}.
*/
goog.ui.DatePickerRenderer.prototype.renderNavigationRow = goog.abstractMethod;
/**
* Render the footer row.
*
* @param {!Element} row The parent element to render the component into.
* @param {boolean} showWeekNum Whether week numbers should be shown.
*/
goog.ui.DatePickerRenderer.prototype.renderFooterRow = goog.abstractMethod;

View File

@@ -0,0 +1,38 @@
// 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 Provides a function that decorates an element based on its CSS
* class name.
* @author attila@google.com (Attila Bodis)
*/
goog.provide('goog.ui.decorate');
goog.require('goog.ui.registry');
/**
* Decorates the element with a suitable {@link goog.ui.Component} instance, if
* a matching decorator is found.
* @param {Element} element Element to decorate.
* @return {goog.ui.Component?} New component instance, decorating the element.
*/
goog.ui.decorate = function(element) {
var decorator = goog.ui.registry.getDecorator(element);
if (decorator) {
decorator.decorate(element);
}
return decorator;
};

View File

@@ -0,0 +1,202 @@
// Copyright 2013 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 The default renderer for {@link goog.ui.DatePicker}.
*
* @see ../demos/datepicker.html
*/
goog.provide('goog.ui.DefaultDatePickerRenderer');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
/** @suppress {extraRequire} Interface. */
goog.require('goog.ui.DatePickerRenderer');
/**
* Default renderer for {@link goog.ui.DatePicker}. Renders the date picker's
* navigation header and footer.
*
* @param {string} baseCssClass Name of base CSS class of the date picker.
* @param {goog.dom.DomHelper=} opt_domHelper DOM helper.
* @constructor
* @implements {goog.ui.DatePickerRenderer}
*/
goog.ui.DefaultDatePickerRenderer = function(baseCssClass, opt_domHelper) {
/**
* Name of base CSS class of datepicker
* @type {string}
* @private
*/
this.baseCssClass_ = baseCssClass;
/**
* @type {!goog.dom.DomHelper}
* @private
*/
this.dom_ = opt_domHelper || goog.dom.getDomHelper();
};
/**
* Returns the dom helper that is being used on this component.
* @return {!goog.dom.DomHelper} The dom helper used on this component.
*/
goog.ui.DefaultDatePickerRenderer.prototype.getDomHelper = function() {
return this.dom_;
};
/**
* Returns base CSS class. This getter is used to get base CSS class part.
* All CSS class names in component are created as:
* goog.getCssName(this.getBaseCssClass(), 'CLASS_NAME')
* @return {string} Base CSS class.
*/
goog.ui.DefaultDatePickerRenderer.prototype.getBaseCssClass = function() {
return this.baseCssClass_;
};
/**
* Render the navigation row (navigating months and maybe years).
*
* @param {!Element} row The parent element to render the component into.
* @param {boolean} simpleNavigation Whether the picker should render a simple
* navigation menu that only contains controls for navigating to the next
* and previous month. The default navigation menu contains controls for
* navigating to the next/previous month, next/previous year, and menus for
* jumping to specific months and years.
* @param {boolean} showWeekNum Whether week numbers should be shown.
* @param {string} fullDateFormat The full date format.
* {@see goog.i18n.DateTimeSymbols}.
* @override
*/
goog.ui.DefaultDatePickerRenderer.prototype.renderNavigationRow =
function(row, simpleNavigation, showWeekNum, fullDateFormat) {
// Populate the navigation row according to the configured navigation mode.
var cell, monthCell, yearCell;
if (simpleNavigation) {
cell = this.getDomHelper().createElement(goog.dom.TagName.TD);
cell.colSpan = showWeekNum ? 1 : 2;
this.createButton_(cell, '\u00AB',
goog.getCssName(this.getBaseCssClass(), 'previousMonth')); // <<
row.appendChild(cell);
cell = this.getDomHelper().createElement(goog.dom.TagName.TD);
cell.colSpan = showWeekNum ? 6 : 5;
cell.className = goog.getCssName(this.getBaseCssClass(), 'monthyear');
row.appendChild(cell);
cell = this.getDomHelper().createElement(goog.dom.TagName.TD);
this.createButton_(cell, '\u00BB',
goog.getCssName(this.getBaseCssClass(), 'nextMonth')); // >>
row.appendChild(cell);
} else {
monthCell = this.getDomHelper().createElement(goog.dom.TagName.TD);
monthCell.colSpan = 5;
this.createButton_(monthCell, '\u00AB',
goog.getCssName(this.getBaseCssClass(), 'previousMonth')); // <<
this.createButton_(monthCell, '',
goog.getCssName(this.getBaseCssClass(), 'month'));
this.createButton_(monthCell, '\u00BB',
goog.getCssName(this.getBaseCssClass(), 'nextMonth')); // >>
yearCell = this.getDomHelper().createElement(goog.dom.TagName.TD);
yearCell.colSpan = 3;
this.createButton_(yearCell, '\u00AB',
goog.getCssName(this.getBaseCssClass(), 'previousYear')); // <<
this.createButton_(yearCell, '',
goog.getCssName(this.getBaseCssClass(), 'year'));
this.createButton_(yearCell, '\u00BB',
goog.getCssName(this.getBaseCssClass(), 'nextYear')); // <<
// If the date format has year ('y') appearing first before month ('m'),
// show the year on the left hand side of the datepicker popup. Otherwise,
// show the month on the left side. This check assumes the data to be
// valid, and that all date formats contain month and year.
if (fullDateFormat.indexOf('y') < fullDateFormat.indexOf('m')) {
row.appendChild(yearCell);
row.appendChild(monthCell);
} else {
row.appendChild(monthCell);
row.appendChild(yearCell);
}
}
};
/**
* Render the footer row (with select buttons).
*
* @param {!Element} row The parent element to render the component into.
* @param {boolean} showWeekNum Whether week numbers should be shown.
* @override
*/
goog.ui.DefaultDatePickerRenderer.prototype.renderFooterRow =
function(row, showWeekNum) {
// Populate the footer row with buttons for Today and None.
var cell = this.getDomHelper().createElement(goog.dom.TagName.TD);
cell.colSpan = showWeekNum ? 2 : 3;
cell.className = goog.getCssName(this.getBaseCssClass(), 'today-cont');
/** @desc Label for button that selects the current date. */
var MSG_DATEPICKER_TODAY_BUTTON_LABEL = goog.getMsg('Today');
this.createButton_(cell, MSG_DATEPICKER_TODAY_BUTTON_LABEL,
goog.getCssName(this.getBaseCssClass(), 'today-btn'));
row.appendChild(cell);
cell = this.getDomHelper().createElement(goog.dom.TagName.TD);
cell.colSpan = showWeekNum ? 4 : 3;
row.appendChild(cell);
cell = this.getDomHelper().createElement(goog.dom.TagName.TD);
cell.colSpan = 2;
cell.className = goog.getCssName(this.getBaseCssClass(), 'none-cont');
/** @desc Label for button that clears the selection. */
var MSG_DATEPICKER_NONE = goog.getMsg('None');
this.createButton_(cell, MSG_DATEPICKER_NONE,
goog.getCssName(this.getBaseCssClass(), 'none-btn'));
row.appendChild(cell);
};
/**
* Support function for button creation.
*
* @param {Element} parentNode Container the button should be added to.
* @param {string} label Button label.
* @param {string=} opt_className Class name for button, which will be used
* in addition to "goog-date-picker-btn".
* @private
* @return {Element} The created button element.
*/
goog.ui.DefaultDatePickerRenderer.prototype.createButton_ =
function(parentNode, label, opt_className) {
var classes = [goog.getCssName(this.getBaseCssClass(), 'btn')];
if (opt_className) {
classes.push(opt_className);
}
var el = this.getDomHelper().createElement(goog.dom.TagName.BUTTON);
el.className = classes.join(' ');
el.appendChild(this.getDomHelper().createTextNode(label));
parentNode.appendChild(el);
return el;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,305 @@
// 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 dimension picker control. A dimension picker allows the
* user to visually select a row and column count.
*
* @author robbyw@google.com (Robby Walker)
* @author abefettig@google.com (Abe Fettig)
* @see ../demos/dimensionpicker.html
* @see ../demos/dimensionpicker_rtl.html
*/
goog.provide('goog.ui.DimensionPicker');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyCodes');
goog.require('goog.math.Size');
goog.require('goog.ui.Component');
goog.require('goog.ui.Control');
goog.require('goog.ui.DimensionPickerRenderer');
goog.require('goog.ui.registry');
/**
* A dimension picker allows the user to visually select a row and column
* count using their mouse and keyboard.
*
* The currently selected dimension is controlled by an ACTION event. Event
* listeners may retrieve the selected item using the
* {@link #getValue} method.
*
* @param {goog.ui.DimensionPickerRenderer=} opt_renderer Renderer used to
* render or decorate the palette; defaults to
* {@link goog.ui.DimensionPickerRenderer}.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for
* document interaction.
* @constructor
* @extends {goog.ui.Control}
*/
goog.ui.DimensionPicker = function(opt_renderer, opt_domHelper) {
goog.ui.Control.call(this, null,
opt_renderer || goog.ui.DimensionPickerRenderer.getInstance(),
opt_domHelper);
this.size_ = new goog.math.Size(this.minColumns, this.minRows);
};
goog.inherits(goog.ui.DimensionPicker, goog.ui.Control);
/**
* Minimum number of columns to show in the grid.
* @type {number}
*/
goog.ui.DimensionPicker.prototype.minColumns = 5;
/**
* Minimum number of rows to show in the grid.
* @type {number}
*/
goog.ui.DimensionPicker.prototype.minRows = 5;
/**
* Maximum number of columns to show in the grid.
* @type {number}
*/
goog.ui.DimensionPicker.prototype.maxColumns = 20;
/**
* Maximum number of rows to show in the grid.
* @type {number}
*/
goog.ui.DimensionPicker.prototype.maxRows = 20;
/**
* Palette dimensions (columns x rows).
* @type {goog.math.Size}
* @private
*/
goog.ui.DimensionPicker.prototype.size_;
/**
* Currently highlighted row count.
* @type {number}
* @private
*/
goog.ui.DimensionPicker.prototype.highlightedRows_ = 1;
/**
* Currently highlighted column count.
* @type {number}
* @private
*/
goog.ui.DimensionPicker.prototype.highlightedColumns_ = 1;
/** @override */
goog.ui.DimensionPicker.prototype.enterDocument = function() {
goog.ui.DimensionPicker.superClass_.enterDocument.call(this);
var handler = this.getHandler();
handler.
listen(this.getRenderer().getMouseMoveElement(this),
goog.events.EventType.MOUSEMOVE, this.handleMouseMove).
listen(this.getDomHelper().getWindow(), goog.events.EventType.RESIZE,
this.handleWindowResize);
var parent = this.getParent();
if (parent) {
handler.listen(parent, goog.ui.Component.EventType.SHOW, this.handleShow_);
}
};
/** @override */
goog.ui.DimensionPicker.prototype.exitDocument = function() {
goog.ui.DimensionPicker.superClass_.exitDocument.call(this);
var handler = this.getHandler();
handler.
unlisten(this.getRenderer().getMouseMoveElement(this),
goog.events.EventType.MOUSEMOVE, this.handleMouseMove).
unlisten(this.getDomHelper().getWindow(), goog.events.EventType.RESIZE,
this.handleWindowResize);
var parent = this.getParent();
if (parent) {
handler.unlisten(parent, goog.ui.Component.EventType.SHOW,
this.handleShow_);
}
};
/**
* Resets the highlighted size when the picker is shown.
* @private
*/
goog.ui.DimensionPicker.prototype.handleShow_ = function() {
if (this.isVisible()) {
this.setValue(1, 1);
}
};
/** @override */
goog.ui.DimensionPicker.prototype.disposeInternal = function() {
goog.ui.DimensionPicker.superClass_.disposeInternal.call(this);
delete this.size_;
};
// Palette event handling.
/**
* Handles mousemove events. Determines which palette size was moused over and
* highlights it.
* @param {goog.events.BrowserEvent} e Mouse event to handle.
* @protected
*/
goog.ui.DimensionPicker.prototype.handleMouseMove = function(e) {
var highlightedSizeX = this.getRenderer().getGridOffsetX(this,
this.isRightToLeft() ? e.target.offsetWidth - e.offsetX : e.offsetX);
var highlightedSizeY = this.getRenderer().getGridOffsetY(this, e.offsetY);
this.setValue(highlightedSizeX, highlightedSizeY);
};
/**
* Handles window resize events. Ensures no scrollbars are introduced by the
* renderer's mouse catcher.
* @param {goog.events.Event} e Resize event to handle.
* @protected
*/
goog.ui.DimensionPicker.prototype.handleWindowResize = function(e) {
this.getRenderer().positionMouseCatcher(this);
};
/**
* Handle key events if supported, so the user can use the keyboard to
* manipulate the highlighted rows and columns.
* @param {goog.events.KeyEvent} e The key event object.
* @return {boolean} Whether the key event was handled.
* @override
*/
goog.ui.DimensionPicker.prototype.handleKeyEvent = function(e) {
var rows = this.highlightedRows_;
var columns = this.highlightedColumns_;
switch (e.keyCode) {
case goog.events.KeyCodes.DOWN:
rows++;
break;
case goog.events.KeyCodes.UP:
rows--;
break;
case goog.events.KeyCodes.LEFT:
if (columns == 1) {
// Delegate to parent.
return false;
} else {
columns--;
}
break;
case goog.events.KeyCodes.RIGHT:
columns++;
break;
default:
return goog.ui.DimensionPicker.superClass_.handleKeyEvent.call(this, e);
}
this.setValue(columns, rows);
return true;
};
// Palette management.
/**
* @return {goog.math.Size} Current table size shown (columns x rows).
*/
goog.ui.DimensionPicker.prototype.getSize = function() {
return this.size_;
};
/**
* @return {!goog.math.Size} size The currently highlighted dimensions.
*/
goog.ui.DimensionPicker.prototype.getValue = function() {
return new goog.math.Size(this.highlightedColumns_, this.highlightedRows_);
};
/**
* Sets the currently highlighted dimensions. If the dimensions are not valid
* (not between 1 and the maximum number of columns/rows to show), they will
* be changed to the closest valid value.
* @param {(number|!goog.math.Size)} columns The number of columns to highlight,
* or a goog.math.Size object containing both.
* @param {number=} opt_rows The number of rows to highlight. Can be
* omitted when columns is a good.math.Size object.
*/
goog.ui.DimensionPicker.prototype.setValue = function(columns,
opt_rows) {
if (!goog.isDef(opt_rows)) {
columns = /** @type {!goog.math.Size} */ (columns);
opt_rows = columns.height;
columns = columns.width;
} else {
columns = /** @type {number} */ (columns);
}
// Ensure that the row and column values are within the minimum value (1) and
// maxmimum values.
columns = Math.max(1, columns);
opt_rows = Math.max(1, opt_rows);
columns = Math.min(this.maxColumns, columns);
opt_rows = Math.min(this.maxRows, opt_rows);
if (this.highlightedColumns_ != columns ||
this.highlightedRows_ != opt_rows) {
var renderer = this.getRenderer();
// Show one more row/column than highlighted so the user understands the
// palette can grow.
this.size_.width = Math.max(
Math.min(columns + 1, this.maxColumns), this.minColumns);
this.size_.height = Math.max(
Math.min(opt_rows + 1, this.maxRows), this.minRows);
renderer.updateSize(this, this.getElement());
this.highlightedColumns_ = columns;
this.highlightedRows_ = opt_rows;
renderer.setHighlightedSize(this, columns, opt_rows);
}
};
/**
* Register this control so it can be created from markup
*/
goog.ui.registry.setDecoratorByClassName(
goog.ui.DimensionPickerRenderer.CSS_CLASS,
function() {
return new goog.ui.DimensionPicker();
});

View File

@@ -0,0 +1,416 @@
// 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 The default renderer for a goog.dom.DimensionPicker. A
* dimension picker allows the user to visually select a row and column count.
* It looks like a palette but in order to minimize DOM load it is rendered.
* using CSS background tiling instead of as a grid of nodes.
*
* @author robbyw@google.com (Robby Walker)
* @author abefettig@google.com (Abe Fettig)
*/
goog.provide('goog.ui.DimensionPickerRenderer');
goog.require('goog.a11y.aria');
goog.require('goog.a11y.aria.State');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.i18n.bidi');
goog.require('goog.style');
goog.require('goog.ui.ControlRenderer');
goog.require('goog.userAgent');
/**
* Default renderer for {@link goog.ui.DimensionPicker}s. Renders the
* palette as two divs, one with the un-highlighted background, and one with the
* highlighted background.
*
* @constructor
* @extends {goog.ui.ControlRenderer}
*/
goog.ui.DimensionPickerRenderer = function() {
goog.ui.ControlRenderer.call(this);
};
goog.inherits(goog.ui.DimensionPickerRenderer, goog.ui.ControlRenderer);
goog.addSingletonGetter(goog.ui.DimensionPickerRenderer);
/**
* Default CSS class to be applied to the root element of components rendered
* by this renderer.
* @type {string}
*/
goog.ui.DimensionPickerRenderer.CSS_CLASS =
goog.getCssName('goog-dimension-picker');
/**
* Return the underlying div for the given outer element.
* @param {Element} element The root element.
* @return {Element} The underlying div.
* @private
*/
goog.ui.DimensionPickerRenderer.prototype.getUnderlyingDiv_ = function(
element) {
return element.firstChild.childNodes[1];
};
/**
* Return the highlight div for the given outer element.
* @param {Element} element The root element.
* @return {Element} The highlight div.
* @private
*/
goog.ui.DimensionPickerRenderer.prototype.getHighlightDiv_ = function(
element) {
return /** @type {Element} */ (element.firstChild.lastChild);
};
/**
* Return the status message div for the given outer element.
* @param {Element} element The root element.
* @return {Element} The status message div.
* @private
*/
goog.ui.DimensionPickerRenderer.prototype.getStatusDiv_ = function(
element) {
return /** @type {Element} */ (element.lastChild);
};
/**
* Return the invisible mouse catching div for the given outer element.
* @param {Element} element The root element.
* @return {Element} The invisible mouse catching div.
* @private
*/
goog.ui.DimensionPickerRenderer.prototype.getMouseCatcher_ = function(
element) {
return /** @type {Element} */ (element.firstChild.firstChild);
};
/**
* Overrides {@link goog.ui.ControlRenderer#canDecorate} to allow decorating
* empty DIVs only.
* @param {Element} element The element to check.
* @return {boolean} Whether if the element is an empty div.
* @override
*/
goog.ui.DimensionPickerRenderer.prototype.canDecorate = function(
element) {
return element.tagName == goog.dom.TagName.DIV && !element.firstChild;
};
/**
* Overrides {@link goog.ui.ControlRenderer#decorate} to decorate empty DIVs.
* @param {goog.ui.Control} control goog.ui.DimensionPicker to decorate.
* @param {Element} element The element to decorate.
* @return {Element} The decorated element.
* @override
*/
goog.ui.DimensionPickerRenderer.prototype.decorate = function(control,
element) {
var palette = /** @type {goog.ui.DimensionPicker} */ (control);
goog.ui.DimensionPickerRenderer.superClass_.decorate.call(this,
palette, element);
this.addElementContents_(palette, element);
this.updateSize(palette, element);
return element;
};
/**
* Scales various elements in order to update the palette's size.
* @param {goog.ui.DimensionPicker} palette The palette object.
* @param {Element} element The element to set the style of.
*/
goog.ui.DimensionPickerRenderer.prototype.updateSize =
function(palette, element) {
var size = palette.getSize();
element.style.width = size.width + 'em';
var underlyingDiv = this.getUnderlyingDiv_(element);
underlyingDiv.style.width = size.width + 'em';
underlyingDiv.style.height = size.height + 'em';
if (palette.isRightToLeft()) {
this.adjustParentDirection_(palette, element);
}
};
/**
* Adds the appropriate content elements to the given outer DIV.
* @param {goog.ui.DimensionPicker} palette The palette object.
* @param {Element} element The element to decorate.
* @private
*/
goog.ui.DimensionPickerRenderer.prototype.addElementContents_ = function(
palette, element) {
// First we create a single div containing three stacked divs. The bottom div
// catches mouse events. We can't use document level mouse move detection as
// we could lose events to iframes. This is especially important in Firefox 2
// in which TrogEdit creates iframes. The middle div uses a css tiled
// background image to represent deselected tiles. The top div uses a
// different css tiled background image to represent selected tiles.
var mouseCatcherDiv = palette.getDomHelper().createDom(goog.dom.TagName.DIV,
goog.getCssName(this.getCssClass(), 'mousecatcher'));
var unhighlightedDiv = palette.getDomHelper().createDom(goog.dom.TagName.DIV,
{
'class': goog.getCssName(this.getCssClass(), 'unhighlighted'),
'style': 'width:100%;height:100%'
});
var highlightedDiv = palette.getDomHelper().createDom(goog.dom.TagName.DIV,
goog.getCssName(this.getCssClass(), 'highlighted'));
element.appendChild(
palette.getDomHelper().createDom(goog.dom.TagName.DIV,
{'style': 'width:100%;height:100%'},
mouseCatcherDiv, unhighlightedDiv, highlightedDiv));
// Lastly we add a div to store the text version of the current state.
element.appendChild(palette.getDomHelper().createDom(goog.dom.TagName.DIV,
goog.getCssName(this.getCssClass(), 'status')));
};
/**
* Creates a div and adds the appropriate contents to it.
* @param {goog.ui.Control} control Picker to render.
* @return {Element} Root element for the palette.
* @override
*/
goog.ui.DimensionPickerRenderer.prototype.createDom = function(control) {
var palette = /** @type {goog.ui.DimensionPicker} */ (control);
var classNames = this.getClassNames(palette);
var element = palette.getDomHelper().createDom(goog.dom.TagName.DIV, {
'class' : classNames ? classNames.join(' ') : ''
});
this.addElementContents_(palette, element);
this.updateSize(palette, element);
return element;
};
/**
* Initializes the control's DOM when the control enters the document. Called
* from {@link goog.ui.Control#enterDocument}.
* @param {goog.ui.Control} control Palette whose DOM is to be
* initialized as it enters the document.
* @override
*/
goog.ui.DimensionPickerRenderer.prototype.initializeDom = function(
control) {
var palette = /** @type {goog.ui.DimensionPicker} */ (control);
goog.ui.DimensionPickerRenderer.superClass_.initializeDom.call(this, palette);
// Make the displayed highlighted size match the dimension picker's value.
var highlightedSize = palette.getValue();
this.setHighlightedSize(palette,
highlightedSize.width, highlightedSize.height);
this.positionMouseCatcher(palette);
};
/**
* Get the element to listen for mouse move events on.
* @param {goog.ui.DimensionPicker} palette The palette to listen on.
* @return {Element} The element to listen for mouse move events on.
*/
goog.ui.DimensionPickerRenderer.prototype.getMouseMoveElement = function(
palette) {
return /** @type {Element} */ (palette.getElement().firstChild);
};
/**
* Returns the x offset in to the grid for the given mouse x position.
* @param {goog.ui.DimensionPicker} palette The table size palette.
* @param {number} x The mouse event x position.
* @return {number} The x offset in to the grid.
*/
goog.ui.DimensionPickerRenderer.prototype.getGridOffsetX = function(
palette, x) {
// TODO(robbyw): Don't rely on magic 18 - measure each palette's em size.
return Math.min(palette.maxColumns, Math.ceil(x / 18));
};
/**
* Returns the y offset in to the grid for the given mouse y position.
* @param {goog.ui.DimensionPicker} palette The table size palette.
* @param {number} y The mouse event y position.
* @return {number} The y offset in to the grid.
*/
goog.ui.DimensionPickerRenderer.prototype.getGridOffsetY = function(
palette, y) {
return Math.min(palette.maxRows, Math.ceil(y / 18));
};
/**
* Sets the highlighted size. Does nothing if the palette hasn't been rendered.
* @param {goog.ui.DimensionPicker} palette The table size palette.
* @param {number} columns The number of columns to highlight.
* @param {number} rows The number of rows to highlight.
*/
goog.ui.DimensionPickerRenderer.prototype.setHighlightedSize = function(
palette, columns, rows) {
var element = palette.getElement();
// Can't update anything if DimensionPicker hasn't been rendered.
if (!element) {
return;
}
// Style the highlight div.
var style = this.getHighlightDiv_(element).style;
style.width = columns + 'em';
style.height = rows + 'em';
// Explicitly set style.right so the element grows to the left when increase
// in width.
if (palette.isRightToLeft()) {
style.right = '0';
}
// Update the aria label.
/**
* @desc The dimension of the columns and rows currently selected in the
* dimension picker, as text that can be spoken by a screen reader.
*/
var MSG_DIMENSION_PICKER_HIGHLIGHTED_DIMENSIONS = goog.getMsg(
'{$numCols} by {$numRows}',
{'numCols': columns, 'numRows': rows});
goog.a11y.aria.setState(element, goog.a11y.aria.State.LABEL,
MSG_DIMENSION_PICKER_HIGHLIGHTED_DIMENSIONS);
// Update the size text.
goog.dom.setTextContent(this.getStatusDiv_(element),
goog.i18n.bidi.enforceLtrInText(columns + ' x ' + rows));
};
/**
* Position the mouse catcher such that it receives mouse events past the
* selectedsize up to the maximum size. Takes care to not introduce scrollbars.
* Should be called on enter document and when the window changes size.
* @param {goog.ui.DimensionPicker} palette The table size palette.
*/
goog.ui.DimensionPickerRenderer.prototype.positionMouseCatcher = function(
palette) {
var mouseCatcher = this.getMouseCatcher_(palette.getElement());
var doc = goog.dom.getOwnerDocument(mouseCatcher);
var body = doc.body;
var position = goog.style.getRelativePosition(mouseCatcher, body);
// Hide the mouse catcher so it doesn't affect the body's scroll size.
mouseCatcher.style.display = 'none';
// Compute the maximum size the catcher can be without introducing scrolling.
var xAvailableEm = (palette.isRightToLeft() && position.x > 0) ?
Math.floor(position.x / 18) :
Math.floor((body.scrollWidth - position.x) / 18);
// Computing available height is more complicated - we need to check the
// window's inner height.
var height;
if (goog.userAgent.IE) {
// Offset 20px to make up for scrollbar size.
height = goog.style.getClientViewportElement(body).scrollHeight - 20;
} else {
var win = goog.dom.getWindow(doc);
// Offset 20px to make up for scrollbar size.
height = Math.max(win.innerHeight, body.scrollHeight) - 20;
}
var yAvailableEm = Math.floor((height - position.y) / 18);
// Resize and display the mouse catcher.
mouseCatcher.style.width = Math.min(palette.maxColumns, xAvailableEm) + 'em';
mouseCatcher.style.height = Math.min(palette.maxRows, yAvailableEm) + 'em';
mouseCatcher.style.display = '';
// Explicitly set style.right so the mouse catcher is positioned on the left
// side instead of right.
if (palette.isRightToLeft()) {
mouseCatcher.style.right = '0';
}
};
/**
* Returns the CSS class to be applied to the root element of components
* rendered using this renderer.
* @return {string} Renderer-specific CSS class.
* @override
*/
goog.ui.DimensionPickerRenderer.prototype.getCssClass = function() {
return goog.ui.DimensionPickerRenderer.CSS_CLASS;
};
/**
* This function adjusts the positioning from 'left' and 'top' to 'right' and
* 'top' as appropriate for RTL control. This is so when the dimensionpicker
* grow in width, the containing element grow to the left instead of right.
* This won't be necessary if goog.ui.SubMenu rendering code would position RTL
* control with 'right' and 'top'.
* @private
*
* @param {goog.ui.DimensionPicker} palette The palette object.
* @param {Element} element The palette's element.
*/
goog.ui.DimensionPickerRenderer.prototype.adjustParentDirection_ =
function(palette, element) {
var parent = palette.getParent();
if (parent) {
var parentElement = parent.getElement();
// Anchors the containing element to the right so it grows to the left
// when it increase in width.
var right = goog.style.getStyle(parentElement, 'right');
if (right == '') {
var parentPos = goog.style.getPosition(parentElement);
var parentSize = goog.style.getSize(parentElement);
if (parentSize.width != 0 && parentPos.x != 0) {
var visibleRect = goog.style.getBounds(
goog.style.getClientViewportElement());
var visibleWidth = visibleRect.width;
right = visibleWidth - parentPos.x - parentSize.width;
goog.style.setStyle(parentElement, 'right', right + 'px');
}
}
// When a table is inserted, the containing elemet's position is
// recalculated the next time it shows, set left back to '' to prevent
// extra white space on the left.
var left = goog.style.getStyle(parentElement, 'left');
if (left != '') {
goog.style.setStyle(parentElement, 'left', '');
}
} else {
goog.style.setStyle(element, 'right', '0px');
}
};

View File

@@ -0,0 +1,642 @@
// Copyright 2007 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 Detects images dragged and dropped on to the window.
*
* @author robbyw@google.com (Robby Walker)
* @author wcrosby@google.com (Wayne Crosby)
*/
goog.provide('goog.ui.DragDropDetector');
goog.provide('goog.ui.DragDropDetector.EventType');
goog.provide('goog.ui.DragDropDetector.ImageDropEvent');
goog.provide('goog.ui.DragDropDetector.LinkDropEvent');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.events.Event');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.math.Coordinate');
goog.require('goog.string');
goog.require('goog.style');
goog.require('goog.userAgent');
/**
* Creates a new drag and drop detector.
* @param {string=} opt_filePath The URL of the page to use for the detector.
* It should contain the same contents as dragdropdetector_target.html in
* the demos directory.
* @constructor
* @extends {goog.events.EventTarget}
*/
goog.ui.DragDropDetector = function(opt_filePath) {
goog.base(this);
var iframe = goog.dom.createDom(goog.dom.TagName.IFRAME, {
'frameborder': 0
});
// In Firefox, we do all drop detection with an IFRAME. In IE, we only use
// the IFRAME to capture copied, non-linked images. (When we don't need it,
// we put a text INPUT before it and push it off screen.)
iframe.className = goog.userAgent.IE ?
goog.getCssName(
goog.ui.DragDropDetector.BASE_CSS_NAME_, 'ie-editable-iframe') :
goog.getCssName(
goog.ui.DragDropDetector.BASE_CSS_NAME_, 'w3c-editable-iframe');
iframe.src = opt_filePath || goog.ui.DragDropDetector.DEFAULT_FILE_PATH_;
this.element_ = /** @type {HTMLIFrameElement} */ (iframe);
this.handler_ = new goog.events.EventHandler(this);
this.handler_.listen(iframe, goog.events.EventType.LOAD, this.initIframe_);
if (goog.userAgent.IE) {
// In IE, we have to bounce between an INPUT for catching links and an
// IFRAME for catching images.
this.textInput_ = goog.dom.createDom(goog.dom.TagName.INPUT, {
'type': 'text',
'className': goog.getCssName(
goog.ui.DragDropDetector.BASE_CSS_NAME_, 'ie-input')
});
this.root_ = goog.dom.createDom(goog.dom.TagName.DIV,
goog.getCssName(goog.ui.DragDropDetector.BASE_CSS_NAME_, 'ie-div'),
this.textInput_, iframe);
} else {
this.root_ = iframe;
}
document.body.appendChild(this.root_);
};
goog.inherits(goog.ui.DragDropDetector, goog.events.EventTarget);
/**
* Drag and drop event types.
* @enum {string}
*/
goog.ui.DragDropDetector.EventType = {
IMAGE_DROPPED: 'onimagedrop',
LINK_DROPPED: 'onlinkdrop'
};
/**
* Browser specific drop event type.
* @type {string}
* @private
*/
goog.ui.DragDropDetector.DROP_EVENT_TYPE_ = goog.userAgent.IE ?
goog.events.EventType.DROP : 'dragdrop';
/**
* Initial value for clientX and clientY indicating that the location has
* never been updated.
*/
goog.ui.DragDropDetector.INIT_POSITION = -10000;
/**
* Prefix for all CSS names.
* @type {string}
* @private
*/
goog.ui.DragDropDetector.BASE_CSS_NAME_ = goog.getCssName('goog-dragdrop');
/**
* @desc Message shown to users to inform them that they can't drag and drop
* local files.
*/
var MSG_DRAG_DROP_LOCAL_FILE_ERROR = goog.getMsg('It is not possible to drag ' +
'and drop image files at this time.\nPlease drag an image from your web ' +
'browser.');
/**
* @desc Message shown to users trying to drag and drop protected images from
* Flickr, etc.
*/
var MSG_DRAG_DROP_PROTECTED_FILE_ERROR = goog.getMsg('The image you are ' +
'trying to drag has been blocked by the hosting site.');
/**
* A map of special case information for URLs that cannot be dropped. Each
* entry is of the form:
* regex: url regex
* message: user visible message about this special case
* @type {Array.<{regex: RegExp, message: string}>}
* @private
*/
goog.ui.DragDropDetector.SPECIAL_CASE_URLS_ = [
{
regex: /^file:\/\/\//,
message: MSG_DRAG_DROP_LOCAL_FILE_ERROR
},
{
regex: /flickr(.*)spaceball.gif$/,
message: MSG_DRAG_DROP_PROTECTED_FILE_ERROR
}
];
/**
* Regex that matches anything that looks kind of like a URL. It matches
* nonspacechars://nonspacechars
* @type {RegExp}
* @private
*/
goog.ui.DragDropDetector.URL_LIKE_REGEX_ = /^\S+:\/\/\S*$/;
/**
* Path to the dragdrop.html file.
* @type {string}
* @private
*/
goog.ui.DragDropDetector.DEFAULT_FILE_PATH_ = 'dragdropdetector_target.html';
/**
* Our event handler object.
* @type {goog.events.EventHandler}
* @private
*/
goog.ui.DragDropDetector.prototype.handler_;
/**
* The root element (the IFRAME on most browsers, the DIV on IE).
* @type {Element}
* @private
*/
goog.ui.DragDropDetector.prototype.root_;
/**
* The text INPUT element used to detect link drops on IE. null on Firefox.
* @type {Element}
* @private
*/
goog.ui.DragDropDetector.prototype.textInput_;
/**
* The iframe element.
* @type {HTMLIFrameElement}
* @private
*/
goog.ui.DragDropDetector.prototype.element_;
/**
* The iframe's window, null if the iframe hasn't loaded yet.
* @type {Window}
* @private
*/
goog.ui.DragDropDetector.prototype.window_ = null;
/**
* The iframe's document, null if the iframe hasn't loaded yet.
* @type {Document}
* @private
*/
goog.ui.DragDropDetector.prototype.document_ = null;
/**
* The iframe's body, null if the iframe hasn't loaded yet.
* @type {HTMLBodyElement}
* @private
*/
goog.ui.DragDropDetector.prototype.body_ = null;
/**
* Whether we are in "screen cover" mode in which the iframe or div is
* covering the entire screen.
* @type {boolean}
* @private
*/
goog.ui.DragDropDetector.prototype.isCoveringScreen_ = false;
/**
* The last position of the mouse while dragging.
* @type {goog.math.Coordinate}
* @private
*/
goog.ui.DragDropDetector.prototype.mousePosition_ = null;
/**
* Initialize the iframe after it has loaded.
* @private
*/
goog.ui.DragDropDetector.prototype.initIframe_ = function() {
// Set up a holder for position data.
this.mousePosition_ = new goog.math.Coordinate(
goog.ui.DragDropDetector.INIT_POSITION,
goog.ui.DragDropDetector.INIT_POSITION);
// Set up pointers to the important parts of the IFrame.
this.window_ = this.element_.contentWindow;
this.document_ = this.window_.document;
this.body_ = this.document_.body;
if (goog.userAgent.GECKO) {
this.document_.designMode = 'on';
} else if (!goog.userAgent.IE) {
// Bug 1667110
// In IE, we only set the IFrame body as content-editable when we bring it
// into view at the top of the page. Otherwise it may take focus when the
// page is loaded, scrolling the user far offscreen.
// Note that this isn't easily unit-testable, since it depends on a
// browser-specific behavior with content-editable areas.
this.body_.contentEditable = true;
}
this.handler_.listen(document.body, goog.events.EventType.DRAGENTER,
this.coverScreen_);
if (goog.userAgent.IE) {
// IE only events.
// Set up events on the IFrame.
this.handler_.
listen(this.body_,
[goog.events.EventType.DRAGENTER, goog.events.EventType.DRAGOVER],
goog.ui.DragDropDetector.enforceCopyEffect_).
listen(this.body_, goog.events.EventType.MOUSEOUT,
this.switchToInput_).
listen(this.body_, goog.events.EventType.DRAGLEAVE,
this.uncoverScreen_).
listen(this.body_, goog.ui.DragDropDetector.DROP_EVENT_TYPE_,
function(e) {
this.trackMouse_(e);
// The drop event occurs before the content is added to the
// iframe. We setTimeout so that handleNodeInserted_ is called
// after the content is in the document.
goog.global.setTimeout(
goog.bind(this.handleNodeInserted_, this, e), 0);
return true;
}).
// Set up events on the DIV.
listen(this.root_,
[goog.events.EventType.DRAGENTER, goog.events.EventType.DRAGOVER],
this.handleNewDrag_).
listen(this.root_,
[
goog.events.EventType.MOUSEMOVE,
goog.events.EventType.KEYPRESS
], this.uncoverScreen_).
// Set up events on the text INPUT.
listen(this.textInput_, goog.events.EventType.DRAGOVER,
goog.events.Event.preventDefault).
listen(this.textInput_, goog.ui.DragDropDetector.DROP_EVENT_TYPE_,
this.handleInputDrop_);
} else {
// W3C events.
this.handler_.
listen(this.body_, goog.ui.DragDropDetector.DROP_EVENT_TYPE_,
function(e) {
this.trackMouse_(e);
this.uncoverScreen_();
}).
listen(this.body_,
[goog.events.EventType.MOUSEMOVE, goog.events.EventType.KEYPRESS],
this.uncoverScreen_).
// Detect content insertion.
listen(this.document_, 'DOMNodeInserted',
this.handleNodeInserted_);
}
};
/**
* Enforce that anything dragged over the IFRAME is copied in to it, rather
* than making it navigate to a different URL.
* @param {goog.events.BrowserEvent} e The event to enforce copying on.
* @private
*/
goog.ui.DragDropDetector.enforceCopyEffect_ = function(e) {
var event = e.getBrowserEvent();
// This function is only called on IE.
if (event.dataTransfer.dropEffect.toLowerCase() != 'copy') {
event.dataTransfer.dropEffect = 'copy';
}
};
/**
* Cover the screen with the iframe.
* @param {goog.events.BrowserEvent} e The event that caused this function call.
* @private
*/
goog.ui.DragDropDetector.prototype.coverScreen_ = function(e) {
// Don't do anything if the drop effect is 'none' and we are in IE.
// It is set to 'none' in cases like dragging text inside a text area.
if (goog.userAgent.IE &&
e.getBrowserEvent().dataTransfer.dropEffect == 'none') {
return;
}
if (!this.isCoveringScreen_) {
this.isCoveringScreen_ = true;
if (goog.userAgent.IE) {
goog.style.setStyle(this.root_, 'top', '0');
this.body_.contentEditable = true;
this.switchToInput_(e);
} else {
goog.style.setStyle(this.root_, 'height', '5000px');
}
}
};
/**
* Uncover the screen.
* @private
*/
goog.ui.DragDropDetector.prototype.uncoverScreen_ = function() {
if (this.isCoveringScreen_) {
this.isCoveringScreen_ = false;
if (goog.userAgent.IE) {
this.body_.contentEditable = false;
goog.style.setStyle(this.root_, 'top', '-5000px');
} else {
goog.style.setStyle(this.root_, 'height', '10px');
}
}
};
/**
* Re-insert the INPUT into the DIV. Does nothing when the DIV is off screen.
* @param {goog.events.BrowserEvent} e The event that caused this function call.
* @private
*/
goog.ui.DragDropDetector.prototype.switchToInput_ = function(e) {
// This is only called on IE.
if (this.isCoveringScreen_) {
goog.style.setElementShown(this.textInput_, true);
}
};
/**
* Remove the text INPUT so the IFRAME is showing. Does nothing when the DIV is
* off screen.
* @param {goog.events.BrowserEvent} e The event that caused this function call.
* @private
*/
goog.ui.DragDropDetector.prototype.switchToIframe_ = function(e) {
// This is only called on IE.
if (this.isCoveringScreen_) {
goog.style.setElementShown(this.textInput_, false);
this.isShowingInput_ = false;
}
};
/**
* Handle a new drag event.
* @param {goog.events.BrowserEvent} e The event object.
* @return {boolean|undefined} Returns false in IE to cancel the event.
* @private
*/
goog.ui.DragDropDetector.prototype.handleNewDrag_ = function(e) {
var event = e.getBrowserEvent();
// This is only called on IE.
if (event.dataTransfer.dropEffect == 'link') {
this.switchToInput_(e);
e.preventDefault();
return false;
}
// Things that aren't links can be placed in the contentEditable iframe.
this.switchToIframe_(e);
// No need to return true since for events return true is the same as no
// return.
};
/**
* Handle mouse tracking.
* @param {goog.events.BrowserEvent} e The event object.
* @private
*/
goog.ui.DragDropDetector.prototype.trackMouse_ = function(e) {
this.mousePosition_.x = e.clientX;
this.mousePosition_.y = e.clientY;
// Check if the event is coming from within the iframe.
if (goog.dom.getOwnerDocument(/** @type {Node} */ (e.target)) != document) {
var iframePosition = goog.style.getClientPosition(this.element_);
this.mousePosition_.x += iframePosition.x;
this.mousePosition_.y += iframePosition.y;
}
};
/**
* Handle a drop on the IE text INPUT.
* @param {goog.events.BrowserEvent} e The event object.
* @private
*/
goog.ui.DragDropDetector.prototype.handleInputDrop_ = function(e) {
this.dispatchEvent(
new goog.ui.DragDropDetector.LinkDropEvent(
e.getBrowserEvent().dataTransfer.getData('Text')));
this.uncoverScreen_();
e.preventDefault();
};
/**
* Clear the contents of the iframe.
* @private
*/
goog.ui.DragDropDetector.prototype.clearContents_ = function() {
if (goog.userAgent.WEBKIT) {
// Since this is called on a mutation event for the nodes we are going to
// clear, calling this right away crashes some versions of WebKit. Wait
// until the events are finished.
goog.global.setTimeout(goog.bind(function() {
this.innerHTML = '';
}, this.body_), 0);
} else {
this.document_.execCommand('selectAll', false, null);
this.document_.execCommand('delete', false, null);
this.document_.execCommand('selectAll', false, null);
}
};
/**
* Event handler called when the content of the iframe changes.
* @param {goog.events.BrowserEvent} e The event that caused this function call.
* @private
*/
goog.ui.DragDropDetector.prototype.handleNodeInserted_ = function(e) {
var uri;
if (this.body_.innerHTML.indexOf('<') == -1) {
// If the document contains no tags (i.e. is just text), try it out.
uri = goog.string.trim(goog.dom.getTextContent(this.body_));
// See if it looks kind of like a url.
if (!uri.match(goog.ui.DragDropDetector.URL_LIKE_REGEX_)) {
uri = null;
}
}
if (!uri) {
var imgs = this.body_.getElementsByTagName(goog.dom.TagName.IMG);
if (imgs && imgs.length) {
// TODO(robbyw): Grab all the images, instead of just the first.
var img = imgs[0];
uri = img.src;
}
}
if (uri) {
var specialCases = goog.ui.DragDropDetector.SPECIAL_CASE_URLS_;
var len = specialCases.length;
for (var i = 0; i < len; i++) {
var specialCase = specialCases[i];
if (uri.match(specialCase.regex)) {
alert(specialCase.message);
break;
}
}
// If no special cases matched, add the image.
if (i == len) {
this.dispatchEvent(
new goog.ui.DragDropDetector.ImageDropEvent(
uri, this.mousePosition_));
return;
}
}
var links = this.body_.getElementsByTagName(goog.dom.TagName.A);
if (links) {
for (i = 0, len = links.length; i < len; i++) {
this.dispatchEvent(
new goog.ui.DragDropDetector.LinkDropEvent(links[i].href));
}
}
this.clearContents_();
this.uncoverScreen_();
};
/** @override */
goog.ui.DragDropDetector.prototype.disposeInternal = function() {
goog.base(this, 'disposeInternal');
this.handler_.dispose();
this.handler_ = null;
};
/**
* Creates a new image drop event object.
* @param {string} url The url of the dropped image.
* @param {goog.math.Coordinate} position The screen position where the drop
* occurred.
* @constructor
* @extends {goog.events.Event}
*/
goog.ui.DragDropDetector.ImageDropEvent = function(url, position) {
goog.base(this, goog.ui.DragDropDetector.EventType.IMAGE_DROPPED);
/**
* The url of the image that was dropped.
* @type {string}
* @private
*/
this.url_ = url;
/**
* The screen position where the drop occurred.
* @type {goog.math.Coordinate}
* @private
*/
this.position_ = position;
};
goog.inherits(goog.ui.DragDropDetector.ImageDropEvent,
goog.events.Event);
/**
* @return {string} The url of the image that was dropped.
*/
goog.ui.DragDropDetector.ImageDropEvent.prototype.getUrl = function() {
return this.url_;
};
/**
* @return {goog.math.Coordinate} The screen position where the drop occurred.
* This may be have x and y of goog.ui.DragDropDetector.INIT_POSITION,
* indicating the drop position is unknown.
*/
goog.ui.DragDropDetector.ImageDropEvent.prototype.getPosition = function() {
return this.position_;
};
/**
* Creates a new link drop event object.
* @param {string} url The url of the dropped link.
* @constructor
* @extends {goog.events.Event}
*/
goog.ui.DragDropDetector.LinkDropEvent = function(url) {
goog.base(this, goog.ui.DragDropDetector.EventType.LINK_DROPPED);
/**
* The url of the link that was dropped.
* @type {string}
* @private
*/
this.url_ = url;
};
goog.inherits(goog.ui.DragDropDetector.LinkDropEvent,
goog.events.Event);
/**
* @return {string} The url of the link that was dropped.
*/
goog.ui.DragDropDetector.LinkDropEvent.prototype.getUrl = function() {
return this.url_;
};

View File

@@ -0,0 +1,486 @@
// Copyright 2007 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 Tree-like drilldown components for HTML tables.
*
* This component supports expanding and collapsing groups of rows in
* HTML tables. The behavior is like typical Tree widgets, but tables
* need special support to enable the tree behaviors.
*
* Any row or rows in an HTML table can be DrilldownRows. The root
* DrilldownRow nodes are always visible in the table, but the rest show
* or hide as input events expand and collapse their ancestors.
*
* Programming them: Top-level DrilldownRows are made by decorating
* a TR element. Children are made with addChild or addChildAt, and
* are entered into the document by the render() method.
*
* A DrilldownRow can have any number of children. If it has no children
* it can be loaded, not loaded, or with a load in progress.
* Top-level DrilldownRows are always displayed (though setting
* style.display on a containing DOM node could make one be not
* visible to the user). A DrilldownRow can be expanded, or not. A
* DrilldownRow displays if all of its ancestors are expanded.
*
* Set up event handlers and style each row for the application in an
* enterDocument method.
*
* Children normally render into the document lazily, at the first
* moment when all ancestors are expanded.
*
* @see ../demos/drilldownrow.html
*/
// TODO(user): Build support for dynamically loading DrilldownRows,
// probably using automplete as an example to follow.
// TODO(user): Make DrilldownRows accessible through the keyboard.
// The render method is redefined in this class because when addChildAt renders
// the new child it assumes that the child's DOM node will be a child
// of the parent component's DOM node, but all DOM nodes of DrilldownRows
// in the same tree of DrilldownRows are siblings to each other.
//
// Arguments (or lack of arguments) to the render methods in Component
// all determine the place of the new DOM node in the DOM tree, but
// the place of a new DrilldownRow in the DOM needs to be determined by
// its position in the tree of DrilldownRows.
goog.provide('goog.ui.DrilldownRow');
goog.require('goog.dom');
goog.require('goog.dom.classes');
goog.require('goog.ui.Component');
/**
* Builds a DrilldownRow component, which can overlay a tree
* structure onto sections of an HTML table.
*
* @param {Object=} opt_properties This parameter can contain:
* contents: if present, user data identifying
* the information loaded into the row and its children.
* loaded: initializes the isLoaded property, defaults to true.
* expanded: DrilldownRow expanded or not, default is true.
* html: String of HTML, relevant and required for DrilldownRows to be
* added as children. Ignored when decorating an existing table row.
* decorator: Function that accepts one DrilldownRow argument, and
* should customize and style the row. The default is to call
* goog.ui.DrilldownRow.decorator.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @constructor
* @extends {goog.ui.Component}
*/
goog.ui.DrilldownRow = function(opt_properties, opt_domHelper) {
goog.ui.Component.call(this, opt_domHelper);
var properties = opt_properties || {};
// Initialize instance variables.
/**
* String of HTML to initialize the DOM structure for the table row.
* Should have the form '<tr attr="etc">Row contents here</tr>'.
* @type {string}
* @private
*/
this.html_ = properties.html;
/**
* Controls whether this component's children will show when it shows.
* @type {boolean}
* @private
*/
this.expanded_ = typeof properties.expanded != 'undefined' ?
properties.expanded : true;
/**
* Is this component loaded? States are true, false, and null for
* 'loading in progress'. For in-memory
* trees of components, this is always true.
* @type {boolean}
* @private
*/
this.loaded_ = typeof properties.loaded != 'undefined' ?
properties.loaded : true;
/**
* If this component's DOM element is created from a string of
* HTML, this is the function to call when it is entered into the DOM tree.
* @type {Function} args are DrilldownRow and goog.events.EventHandler
* of the DrilldownRow.
* @private
*/
this.decoratorFn_ = properties.decorator || goog.ui.DrilldownRow.decorate;
/**
* Is the DrilldownRow to be displayed? If it is rendered, this mirrors
* the style.display of the DrilldownRow's row.
* @type {boolean}
* @private
*/
this.displayed_ = true;
};
goog.inherits(goog.ui.DrilldownRow, goog.ui.Component);
/**
* Example object with properties of the form accepted by the class
* constructor. These are educational and show the compiler that
* these properties can be set so it doesn't emit warnings.
*/
goog.ui.DrilldownRow.sampleProperties = {
'html': '<tr><td>Sample</td><td>Sample</tr>',
'loaded': true,
'decorator': function(selfObj, handler) {
// When the mouse is hovering, add CSS class goog-drilldown-hover.
goog.ui.DrilldownRow.decorate(selfObj);
var row = selfObj.getElement();
handler.listen(row, 'mouseover', function() {
goog.dom.classes.add(row, goog.getCssName('goog-drilldown-hover'));
});
handler.listen(row, 'mouseout', function() {
goog.dom.classes.remove(row, goog.getCssName('goog-drilldown-hover'));
});
}
};
//
// Implementations of Component methods.
//
/**
* The base class method calls its superclass method and this
* drilldown's 'decorator' method as defined in the constructor.
* @override
*/
goog.ui.DrilldownRow.prototype.enterDocument = function() {
goog.ui.DrilldownRow.superClass_.enterDocument.call(this);
this.decoratorFn_(this, this.getHandler());
};
/** @override */
goog.ui.DrilldownRow.prototype.createDom = function() {
this.setElementInternal(goog.ui.DrilldownRow.createRowNode_(
this.html_, this.getDomHelper().getDocument()));
};
/**
* A top-level DrilldownRow decorates a TR element.
*
* @param {Element} node The element to test for decorability.
* @return {boolean} true iff the node is a TR.
* @override
*/
goog.ui.DrilldownRow.prototype.canDecorate = function(node) {
return node.tagName == 'TR';
};
/**
* Child drilldowns are rendered when needed.
*
* @param {goog.ui.Component} child New DrilldownRow child to be added.
* @param {number} index position to be occupied by the child.
* @param {boolean=} opt_render true to force immediate rendering.
* @override
*/
goog.ui.DrilldownRow.prototype.addChildAt = function(child, index, opt_render) {
goog.ui.DrilldownRow.superClass_.addChildAt.call(this, child, index, false);
child.setDisplayable_(this.isVisible_() && this.isExpanded());
if (opt_render && !child.isInDocument()) {
child.render();
}
};
/** @override */
goog.ui.DrilldownRow.prototype.removeChild = function(child) {
goog.dom.removeNode(child.getElement());
return goog.ui.DrilldownRow.superClass_.removeChild.call(this, child);
};
/**
* Rendering of DrilldownRow's is on need, do not call this directly
* from application code.
*
* Rendering a DrilldownRow places it according to its position in its
* tree of DrilldownRows. DrilldownRows cannot be placed any other
* way so this method does not use any arguments. This does not call
* the base class method and does not modify any of this
* DrilldownRow's children.
* @override
*/
goog.ui.DrilldownRow.prototype.render = function() {
if (arguments.length) {
throw Error('A DrilldownRow cannot be placed under a specific parent.');
} else {
var parent = this.getParent();
if (!parent.isInDocument()) {
throw Error('Cannot render child of un-rendered parent');
}
// The new child's TR node needs to go just after the last TR
// of the part of the parent's subtree that is to the left
// of this. The subtree includes the parent.
var previous = parent.previousRenderedChild_(this);
var row;
if (previous) {
row = previous.lastRenderedLeaf_().getElement();
} else {
row = parent.getElement();
}
row = /** @type {Element} */ (row.nextSibling);
// Render the child row component into the document.
if (row) {
this.renderBefore(row);
} else {
// Render at the end of the parent of this DrilldownRow's
// DOM element.
var tbody = /** @type {Element} */ (parent.getElement().parentNode);
goog.ui.DrilldownRow.superClass_.render.call(this, tbody);
}
}
};
/**
* Finds the numeric index of this child within its parent Component.
* Throws an exception if it has no parent.
*
* @return {number} index of this within the children of the parent Component.
*/
goog.ui.DrilldownRow.prototype.findIndex = function() {
var parent = this.getParent();
if (!parent) {
throw Error('Component has no parent');
}
return parent.indexOfChild(this);
};
//
// Type-specific operations
//
/**
* Returns the expanded state of the DrilldownRow.
*
* @return {boolean} true iff this is expanded.
*/
goog.ui.DrilldownRow.prototype.isExpanded = function() {
return this.expanded_;
};
/**
* Sets the expanded state of this DrilldownRow: makes all children
* displayable or not displayable corresponding to the expanded state.
*
* @param {boolean} expanded whether this should be expanded or not.
*/
goog.ui.DrilldownRow.prototype.setExpanded = function(expanded) {
if (expanded != this.expanded_) {
this.expanded_ = expanded;
goog.dom.classes.toggle(this.getElement(),
goog.getCssName('goog-drilldown-expanded'));
goog.dom.classes.toggle(this.getElement(),
goog.getCssName('goog-drilldown-collapsed'));
if (this.isVisible_()) {
this.forEachChild(function(child) {
child.setDisplayable_(expanded);
});
}
}
};
/**
* Returns this DrilldownRow's level in the tree. Top level is 1.
*
* @return {number} depth of this DrilldownRow in its tree of drilldowns.
*/
goog.ui.DrilldownRow.prototype.getDepth = function() {
for (var component = this, depth = 0;
component instanceof goog.ui.DrilldownRow;
component = component.getParent(), depth++) {}
return depth;
};
/**
* This static function is a default decorator that adds HTML at the
* beginning of the first cell to display indentation and an expander
* image; sets up a click handler on the toggler; initializes a class
* for the row: either goog-drilldown-expanded or
* goog-drilldown-collapsed, depending on the initial state of the
* DrilldownRow; and sets up a click event handler on the toggler
* element.
*
* This creates a DIV with class=toggle. Your application can set up
* CSS style rules something like this:
*
* tr.goog-drilldown-expanded .toggle {
* background-image: url('minus.png');
* }
*
* tr.goog-drilldown-collapsed .toggle {
* background-image: url('plus.png');
* }
*
* These background images show whether the DrilldownRow is expanded.
*
* @param {goog.ui.DrilldownRow} selfObj DrilldownRow to be decorated.
*/
goog.ui.DrilldownRow.decorate = function(selfObj) {
var depth = selfObj.getDepth();
var row = selfObj.getElement();
if (!row.cells) {
throw Error('No cells');
}
var cell = row.cells[0];
var html = '<div style="float: left; width: ' + depth +
'em;"><div class=toggle style="width: 1em; float: right;">' +
'&nbsp;</div></div>';
var fragment = selfObj.getDomHelper().htmlToDocumentFragment(html);
cell.insertBefore(fragment, cell.firstChild);
goog.dom.classes.add(row, selfObj.isExpanded() ?
goog.getCssName('goog-drilldown-expanded') :
goog.getCssName('goog-drilldown-collapsed'));
// Default mouse event handling:
var toggler = fragment.getElementsByTagName('div')[0];
var key = selfObj.getHandler().listen(toggler, 'click', function(event) {
selfObj.setExpanded(!selfObj.isExpanded());
});
};
//
// Private methods
//
/**
* Turn display of a DrilldownRow on or off. If the DrilldownRow has not
* yet been rendered, this renders it. This propagates the effect
* of the change recursively as needed -- children displaying iff the
* parent is displayed and expanded.
*
* @param {boolean} display state, true iff display is desired.
* @private
*/
goog.ui.DrilldownRow.prototype.setDisplayable_ = function(display) {
if (display && !this.isInDocument()) {
this.render();
}
if (this.displayed_ == display) {
return;
}
this.displayed_ = display;
if (this.isInDocument()) {
this.getElement().style.display = display ? '' : 'none';
}
var selfObj = this;
this.forEachChild(function(child) {
child.setDisplayable_(display && selfObj.expanded_);
});
};
/**
* True iff this and all its DrilldownRow parents are displayable. The
* value is an approximation to actual visibility, since it does not
* look at whether DOM nodes containing the top-level component have
* display: none, visibility: hidden or are otherwise not displayable.
* So this visibility is relative to the top-level component.
*
* @return {boolean} visibility of this relative to its top-level drilldown.
* @private
*/
goog.ui.DrilldownRow.prototype.isVisible_ = function() {
for (var component = this;
component instanceof goog.ui.DrilldownRow;
component = component.getParent()) {
if (!component.displayed_)
return false;
}
return true;
};
/**
* Create and return a TR element from HTML that looks like
* "<tr> ... </tr>".
*
* @param {string} html for one row.
* @param {Document} doc object to hold the Element.
* @return {Element} table row node created from the HTML.
* @private
*/
goog.ui.DrilldownRow.createRowNode_ = function(html, doc) {
// Note: this may be slow.
var tableHtml = '<table>' + html + '</table>';
var div = doc.createElement('div');
div.innerHTML = tableHtml;
return div.firstChild.rows[0];
};
/**
* Get the recursively rightmost child that is in the document.
*
* @return {goog.ui.DrilldownRow} rightmost child currently entered in
* the document, potentially this DrilldownRow. If this is in the
* document, result is non-null.
* @private
*/
goog.ui.DrilldownRow.prototype.lastRenderedLeaf_ = function() {
var leaf = null;
for (var node = this;
node && node.isInDocument();
// Node will become undefined if parent has no children.
node = node.getChildAt(node.getChildCount() - 1)) {
leaf = node;
}
return /** @type {goog.ui.DrilldownRow} */ (leaf);
};
/**
* Search this node's direct children for the last one that is in the
* document and is before the given child.
* @param {goog.ui.DrilldownRow} child The child to stop the search at.
* @return {goog.ui.Component?} The last child component before the given child
* that is in the document.
* @private
*/
goog.ui.DrilldownRow.prototype.previousRenderedChild_ = function(child) {
for (var i = this.getChildCount() - 1; i >= 0; i--) {
if (this.getChildAt(i) == child) {
for (var j = i - 1; j >= 0; j--) {
var prev = this.getChildAt(j);
if (prev.isInDocument()) {
return prev;
}
}
}
}
return null;
};

View File

@@ -0,0 +1,440 @@
// 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.
*
*/
goog.provide('goog.ui.editor.AbstractDialog');
goog.provide('goog.ui.editor.AbstractDialog.Builder');
goog.provide('goog.ui.editor.AbstractDialog.EventType');
goog.require('goog.dom');
goog.require('goog.dom.classes');
goog.require('goog.events.EventTarget');
goog.require('goog.string');
goog.require('goog.ui.Dialog');
// *** 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_.addEventListener(goog.ui.Dialog.EventType.AFTER_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.classes.add(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_.addEventListener(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);
};

View File

@@ -0,0 +1,525 @@
// 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)
* @author tildahl@google.com (Michael Tildahl)
*/
goog.provide('goog.ui.editor.Bubble');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.dom.ViewportSizeMonitor');
goog.require('goog.dom.classes');
goog.require('goog.editor.style');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
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.base(this);
/**
* 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}
* @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.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} The event handler.
* @protected
*/
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();
}
};
/**
* 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.classes.add(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.classes.remove(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.classes.remove(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);
/**
* 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, goog.ui.editor.Bubble.MARGIN_BOX_, overflow);
};
/**
* 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 + ':'), // TODO(robbyw): Does this work properly 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);
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,123 @@
// Copyright 2011 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.EquationEditorDialog');
goog.require('goog.editor.Command');
goog.require('goog.ui.Dialog');
goog.require('goog.ui.editor.AbstractDialog');
goog.require('goog.ui.editor.EquationEditorOkEvent');
goog.require('goog.ui.equation.TexEditor');
/**
* Equation editor dialog (based on goog.ui.editor.AbstractDialog).
* @param {Object} context The context that this dialog runs in.
* @param {goog.dom.DomHelper} domHelper DomHelper to be used to create the
* dialog's dom structure.
* @param {string} equation Initial equation.
* @param {string} helpUrl URL pointing to help documentation.
* @constructor
* @extends {goog.ui.editor.AbstractDialog}
*/
goog.ui.editor.EquationEditorDialog = function(context, domHelper,
equation, helpUrl) {
goog.ui.editor.AbstractDialog.call(this, domHelper);
this.equationEditor_ =
new goog.ui.equation.TexEditor(context, helpUrl, domHelper);
this.equationEditor_.render();
this.equationEditor_.setEquation(equation);
this.equationEditor_.addEventListener(goog.editor.Command.EQUATION,
this.onChange_, false, this);
};
goog.inherits(goog.ui.editor.EquationEditorDialog,
goog.ui.editor.AbstractDialog);
/**
* The equation editor actual UI.
* @type {goog.ui.equation.TexEditor}
* @private
*/
goog.ui.editor.EquationEditorDialog.prototype.equationEditor_;
/**
* The dialog's OK button element.
* @type {Element?}
* @private
*/
goog.ui.editor.EquationEditorDialog.prototype.okButton_;
/** @override */
goog.ui.editor.EquationEditorDialog.prototype.createDialogControl =
function() {
var builder = new goog.ui.editor.AbstractDialog.Builder(this);
/**
* @desc The title of the equation editor dialog.
*/
var MSG_EE_DIALOG_TITLE = goog.getMsg('Equation Editor');
/**
* @desc Button label for the equation editor dialog for adding
* a new equation.
*/
var MSG_EE_BUTTON_SAVE_NEW = goog.getMsg('Insert equation');
/**
* @desc Button label for the equation editor dialog for saving
* a modified equation.
*/
var MSG_EE_BUTTON_SAVE_MODIFY = goog.getMsg('Save changes');
var okButtonText = this.equationEditor_.getEquation() ?
MSG_EE_BUTTON_SAVE_MODIFY : MSG_EE_BUTTON_SAVE_NEW;
builder.setTitle(MSG_EE_DIALOG_TITLE)
.setContent(this.equationEditor_.getElement())
.addOkButton(okButtonText)
.addCancelButton();
return builder.build();
};
/**
* @override
*/
goog.ui.editor.EquationEditorDialog.prototype.createOkEvent = function(e) {
if (this.equationEditor_.isValid()) {
// Equation is not valid, don't close the dialog.
return null;
}
var equationHtml = this.equationEditor_.getHtml();
return new goog.ui.editor.EquationEditorOkEvent(equationHtml);
};
/**
* Handles CHANGE event fired when user changes equation.
* @param {goog.ui.equation.ChangeEvent} e The event object.
* @private
*/
goog.ui.editor.EquationEditorDialog.prototype.onChange_ = function(e) {
if (!this.okButton_) {
this.okButton_ = this.getButtonElement(
goog.ui.Dialog.DefaultButtonKeys.OK);
}
this.okButton_.disabled = !e.isValid;
};

View File

@@ -0,0 +1,49 @@
// Copyright 2011 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.EquationEditorOkEvent');
goog.require('goog.events.Event');
goog.require('goog.ui.editor.AbstractDialog');
/**
* OK event object for the equation editor dialog.
* @param {string} equationHtml html containing the equation to put in the
* editable field.
* @constructor
* @extends {goog.events.Event}
*/
goog.ui.editor.EquationEditorOkEvent = function(equationHtml) {
this.equationHtml = equationHtml;
};
goog.inherits(goog.ui.editor.EquationEditorOkEvent,
goog.events.Event);
/**
* Event type.
* @type {goog.ui.editor.AbstractDialog.EventType}
* @override
*/
goog.ui.editor.EquationEditorOkEvent.prototype.type =
goog.ui.editor.AbstractDialog.EventType.OK;
/**
* HTML containing the equation to put in the editable field.
* @type {string}
*/
goog.ui.editor.EquationEditorOkEvent.prototype.equationHtml;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,123 @@
// 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');
/** @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>'});
/** @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>'});
/**
* @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');

View File

@@ -0,0 +1,198 @@
// 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.dom.TagName');
goog.require('goog.dom.classes');
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}
*/
goog.ui.editor.TabPane = function(dom, opt_caption) {
goog.base(this, dom);
/**
* The event handler used to register events.
* @type {goog.events.EventHandler}
* @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.base(this, 'enterDocument');
// Get the root element and add a class name to it.
var root = this.getElement();
goog.dom.classes.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;
};

View File

@@ -0,0 +1,295 @@
// 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)
* @author jparent@google.com (Julie Parent)
* @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}
* @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} The event handler object.
* @protected
*/
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());
};

View File

@@ -0,0 +1,440 @@
// 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)
* @author jparent@google.com (Julie Parent)
*/
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);
};

View File

@@ -0,0 +1,72 @@
// Copyright 2007 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 Emoji implementation.
*
*/
goog.provide('goog.ui.emoji.Emoji');
/**
* Creates an emoji.
*
* A simple wrapper for an emoji.
*
* @param {string} url URL pointing to the source image for the emoji.
* @param {string} id The id of the emoji, e.g., 'std.1'.
* @constructor
*/
goog.ui.emoji.Emoji = function(url, id) {
/**
* The URL pointing to the source image for the emoji
*
* @type {string}
* @private
*/
this.url_ = url;
/**
* The id of the emoji
*
* @type {string}
* @private
*/
this.id_ = id;
};
/**
* The name of the goomoji attribute, used for emoji image elements.
* @type {string}
*/
goog.ui.emoji.Emoji.ATTRIBUTE = 'goomoji';
/**
* @return {string} The URL for this emoji.
*/
goog.ui.emoji.Emoji.prototype.getUrl = function() {
return this.url_;
};
/**
* @return {string} The id of this emoji.
*/
goog.ui.emoji.Emoji.prototype.getId = function() {
return this.id_;
};

View File

@@ -0,0 +1,288 @@
// Copyright 2007 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 Emoji Palette implementation. This provides a UI widget for
* choosing an emoji from a palette of possible choices. EmojiPalettes are
* contained within EmojiPickers.
*
* See ../demos/popupemojipicker.html for an example of how to instantiate
* an emoji picker.
*
* Based on goog.ui.ColorPicker (colorpicker.js).
*
*/
goog.provide('goog.ui.emoji.EmojiPalette');
goog.require('goog.events.EventType');
goog.require('goog.net.ImageLoader');
goog.require('goog.ui.Palette');
goog.require('goog.ui.emoji.Emoji');
goog.require('goog.ui.emoji.EmojiPaletteRenderer');
/**
* A page of emoji to be displayed in an EmojiPicker.
*
* @param {Array.<Array>} emoji List of emoji for this page.
* @param {?string=} opt_urlPrefix Prefix that should be prepended to all URL.
* @param {goog.ui.PaletteRenderer=} opt_renderer Renderer used to render or
* decorate the palette; defaults to {@link goog.ui.PaletteRenderer}.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @extends {goog.ui.Palette}
* @constructor
*/
goog.ui.emoji.EmojiPalette = function(emoji,
opt_urlPrefix,
opt_renderer,
opt_domHelper) {
goog.ui.Palette.call(this,
null,
opt_renderer ||
new goog.ui.emoji.EmojiPaletteRenderer(null),
opt_domHelper);
/**
* All the different emoji that this palette can display. Maps emoji ids
* (string) to the goog.ui.emoji.Emoji for that id.
*
* @type {Object}
* @private
*/
this.emojiCells_ = {};
/**
* Map of emoji id to index into this.emojiCells_.
*
* @type {Object}
* @private
*/
this.emojiMap_ = {};
/**
* List of the animated emoji in this palette. Each internal array is of type
* [HTMLDivElement, goog.ui.emoji.Emoji], and represents the palette item
* for that animated emoji, and the Emoji object.
*
* @type {Array.<Array.<(HTMLDivElement|goog.ui.emoji.Emoji)>>}
* @private
*/
this.animatedEmoji_ = [];
this.urlPrefix_ = opt_urlPrefix || '';
/**
* Palette items that are displayed on this page of the emoji picker. Each
* item is a div wrapped around a div or an img.
*
* @type {Array.<HTMLDivElement>}
* @private
*/
this.emoji_ = this.getEmojiArrayFromProperties_(emoji);
this.setContent(this.emoji_);
};
goog.inherits(goog.ui.emoji.EmojiPalette, goog.ui.Palette);
/**
* Indicates a prefix that should be prepended to all URLs of images in this
* emojipalette. This provides an optimization if the URLs are long, so that
* the client does not have to send a long string for each emoji.
*
* @type {string}
* @private
*/
goog.ui.emoji.EmojiPalette.prototype.urlPrefix_ = '';
/**
* Whether the emoji images have been loaded.
*
* @type {boolean}
* @private
*/
goog.ui.emoji.EmojiPalette.prototype.imagesLoaded_ = false;
/**
* Image loader for loading animated emoji.
*
* @type {goog.net.ImageLoader}
* @private
*/
goog.ui.emoji.EmojiPalette.prototype.imageLoader_;
/**
* Helps create an array of emoji palette items from an array of emoji
* properties. Each element will be either a div with background-image set to
* a sprite, or an img element pointing directly to an emoji, and all elements
* are wrapped with an outer div for alignment issues (i.e., this allows
* centering the inner div).
*
* @param {Object} emojiGroup The group of emoji for this page.
* @return {Array.<HTMLDivElement>} The emoji items.
* @private
*/
goog.ui.emoji.EmojiPalette.prototype.getEmojiArrayFromProperties_ =
function(emojiGroup) {
var emojiItems = [];
for (var i = 0; i < emojiGroup.length; i++) {
var url = emojiGroup[i][0];
var id = emojiGroup[i][1];
var spriteInfo = emojiGroup[i][2];
var displayUrl = spriteInfo ? spriteInfo.getUrl() :
this.urlPrefix_ + url;
var item = this.getRenderer().createPaletteItem(
this.getDomHelper(), id, spriteInfo, displayUrl);
emojiItems.push(item);
var emoji = new goog.ui.emoji.Emoji(url, id);
this.emojiCells_[id] = emoji;
this.emojiMap_[id] = i;
// Keep track of sprited emoji that are animated, for later loading.
if (spriteInfo && spriteInfo.isAnimated()) {
this.animatedEmoji_.push([item, emoji]);
}
}
// Create the image loader now so that tests can access it before it has
// started loading images.
if (this.animatedEmoji_.length > 0) {
this.imageLoader_ = new goog.net.ImageLoader();
}
this.imagesLoaded_ = true;
return emojiItems;
};
/**
* Sends off requests for all the animated emoji and replaces their static
* sprites when the images are done downloading.
*/
goog.ui.emoji.EmojiPalette.prototype.loadAnimatedEmoji = function() {
if (this.animatedEmoji_.length > 0) {
for (var i = 0; i < this.animatedEmoji_.length; i++) {
var paletteItem = /** @type {Element} */ (this.animatedEmoji_[i][0]);
var emoji =
/** @type {goog.ui.emoji.Emoji} */ (this.animatedEmoji_[i][1]);
var url = this.urlPrefix_ + emoji.getUrl();
this.imageLoader_.addImage(emoji.getId(), url);
}
this.getHandler().listen(this.imageLoader_, goog.events.EventType.LOAD,
this.handleImageLoad_);
this.imageLoader_.start();
}
};
/**
* Handles image load events from the ImageLoader.
*
* @param {goog.events.Event} e The event object.
* @private
*/
goog.ui.emoji.EmojiPalette.prototype.handleImageLoad_ = function(e) {
var id = e.target.id;
var url = e.target.src;
// Just to be safe, we check to make sure we have an id and src url from
// the event target, which the ImageLoader sets to an Image object.
if (id && url) {
var item = this.emoji_[this.emojiMap_[id]];
if (item) {
this.getRenderer().updateAnimatedPaletteItem(item, e.target);
}
}
};
/**
* Returns the image loader that this palette uses. Used for testing.
*
* @return {goog.net.ImageLoader} the image loader.
*/
goog.ui.emoji.EmojiPalette.prototype.getImageLoader = function() {
return this.imageLoader_;
};
/** @override */
goog.ui.emoji.EmojiPalette.prototype.disposeInternal = function() {
goog.ui.emoji.EmojiPalette.superClass_.disposeInternal.call(this);
if (this.imageLoader_) {
this.imageLoader_.dispose();
this.imageLoader_ = null;
}
this.animatedEmoji_ = null;
this.emojiCells_ = null;
this.emojiMap_ = null;
this.emoji_ = null;
};
/**
* Returns a goomoji id from an img or the containing td, or null if none
* exists for that element.
*
* @param {Element} el The element to get the Goomoji id from.
* @return {?string} A goomoji id from an img or the containing td, or null if
* none exists for that element.
* @private
*/
goog.ui.emoji.EmojiPalette.prototype.getGoomojiIdFromElement_ = function(el) {
if (!el) {
return null;
}
var item = this.getRenderer().getContainingItem(this, el);
return item ? item.getAttribute(goog.ui.emoji.Emoji.ATTRIBUTE) : null;
};
/**
* @return {goog.ui.emoji.Emoji} The currently selected emoji from this palette.
*/
goog.ui.emoji.EmojiPalette.prototype.getSelectedEmoji = function() {
var elem = /** @type {Element} */ (this.getSelectedItem());
var goomojiId = this.getGoomojiIdFromElement_(elem);
return this.emojiCells_[goomojiId];
};
/**
* @return {number} The number of emoji managed by this palette.
*/
goog.ui.emoji.EmojiPalette.prototype.getNumberOfEmoji = function() {
return this.emojiCells_.length;
};
/**
* Returns the index of the specified emoji within this palette.
*
* @param {string} id Id of the emoji to look up.
* @return {number} The index of the specified emoji within this palette.
*/
goog.ui.emoji.EmojiPalette.prototype.getEmojiIndex = function(id) {
return this.emojiMap_[id];
};

View File

@@ -0,0 +1,208 @@
// 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 Emoji Palette renderer implementation.
*
*/
goog.provide('goog.ui.emoji.EmojiPaletteRenderer');
goog.require('goog.a11y.aria');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.classes');
goog.require('goog.style');
goog.require('goog.ui.PaletteRenderer');
goog.require('goog.ui.emoji.Emoji');
/**
* Renders an emoji palette.
*
* @param {?string} defaultImgUrl Url of the img that should be used to fill up
* the cells in the emoji table, to prevent jittering. Will be stretched
* to the emoji cell size. A good image is a transparent dot.
* @constructor
* @extends {goog.ui.PaletteRenderer}
*/
goog.ui.emoji.EmojiPaletteRenderer = function(defaultImgUrl) {
goog.ui.PaletteRenderer.call(this);
this.defaultImgUrl_ = defaultImgUrl;
};
goog.inherits(goog.ui.emoji.EmojiPaletteRenderer, goog.ui.PaletteRenderer);
/**
* Globally unique ID sequence for cells rendered by this renderer class.
* @type {number}
* @private
*/
goog.ui.emoji.EmojiPaletteRenderer.cellId_ = 0;
/**
* Url of the img that should be used for cells in the emoji palette that are
* not filled with emoji, i.e., after all the emoji have already been placed
* on a page.
*
* @type {?string}
* @private
*/
goog.ui.emoji.EmojiPaletteRenderer.prototype.defaultImgUrl_ = null;
/** @override */
goog.ui.emoji.EmojiPaletteRenderer.getCssClass = function() {
return goog.getCssName('goog-ui-emojipalette');
};
/**
* Creates a palette item from the given emoji data.
*
* @param {goog.dom.DomHelper} dom DOM helper for constructing DOM elements.
* @param {string} id Goomoji id for the emoji.
* @param {goog.ui.emoji.SpriteInfo} spriteInfo Spriting info for the emoji.
* @param {string} displayUrl URL of the image served for this cell, whether
* an individual emoji image or a sprite.
* @return {HTMLDivElement} The palette item for this emoji.
*/
goog.ui.emoji.EmojiPaletteRenderer.prototype.createPaletteItem =
function(dom, id, spriteInfo, displayUrl) {
var el;
if (spriteInfo) {
var cssClass = spriteInfo.getCssClass();
if (cssClass) {
el = dom.createDom('div', cssClass);
} else {
el = this.buildElementFromSpriteMetadata(dom, spriteInfo, displayUrl);
}
} else {
el = dom.createDom('img', {'src': displayUrl});
}
var outerdiv =
dom.createDom('div', goog.getCssName('goog-palette-cell-wrapper'), el);
outerdiv.setAttribute(goog.ui.emoji.Emoji.ATTRIBUTE, id);
return /** @type {HTMLDivElement} */ (outerdiv);
};
/**
* Modifies a palette item containing an animated emoji, in response to the
* animated emoji being successfully downloaded.
*
* @param {Element} item The palette item to update.
* @param {Image} animatedImg An Image object containing the animated emoji.
*/
goog.ui.emoji.EmojiPaletteRenderer.prototype.updateAnimatedPaletteItem =
function(item, animatedImg) {
// An animated emoji is one that had sprite info for a static version and is
// now being updated. See createPaletteItem for the structure of the palette
// items we're modifying.
var inner = /** @type {Element} */ (item.firstChild);
// The first case is a palette item with a CSS class representing the sprite,
// and an animated emoji.
var classes = goog.dom.classes.get(inner);
if (classes && classes.length == 1) {
inner.className = '';
}
goog.style.setStyle(inner, {
'width': animatedImg.width,
'height': animatedImg.height,
'background-image': 'url(' + animatedImg.src + ')',
'background-position': '0 0'
});
};
/**
* Builds the inner contents of a palette item out of sprite metadata.
*
* @param {goog.dom.DomHelper} dom DOM helper for constructing DOM elements.
* @param {goog.ui.emoji.SpriteInfo} spriteInfo The metadata to create the css
* for the sprite.
* @param {string} displayUrl The URL of the image for this cell.
* @return {HTMLDivElement} The inner element for a palette item.
*/
goog.ui.emoji.EmojiPaletteRenderer.prototype.buildElementFromSpriteMetadata =
function(dom, spriteInfo, displayUrl) {
var width = spriteInfo.getWidthCssValue();
var height = spriteInfo.getHeightCssValue();
var x = spriteInfo.getXOffsetCssValue();
var y = spriteInfo.getYOffsetCssValue();
var el = dom.createDom('div');
goog.style.setStyle(el, {
'width': width,
'height': height,
'background-image': 'url(' + displayUrl + ')',
'background-repeat': 'no-repeat',
'background-position': x + ' ' + y
});
return /** @type {HTMLDivElement} */ (el);
};
/** @override */
goog.ui.emoji.EmojiPaletteRenderer.prototype.createCell = function(node, dom) {
// Create a cell with the default img if we're out of items, in order to
// prevent jitter in the table. If there's no default img url, just create an
// empty div, to prevent trying to fetch a null url.
if (!node) {
var elem = this.defaultImgUrl_ ?
dom.createDom('img', {'src': this.defaultImgUrl_}) :
dom.createDom('div');
node = dom.createDom('div', goog.getCssName('goog-palette-cell-wrapper'),
elem);
}
var cell = dom.createDom('td', {
'class': goog.getCssName(this.getCssClass(), 'cell'),
// Cells must have an ID, for accessibility, so we generate one here.
'id': this.getCssClass() + '-cell-' +
goog.ui.emoji.EmojiPaletteRenderer.cellId_++
}, node);
goog.a11y.aria.setRole(cell, 'gridcell');
return cell;
};
/**
* Returns the item corresponding to the given node, or null if the node is
* neither a palette cell nor part of a palette item.
* @param {goog.ui.Palette} palette Palette in which to look for the item.
* @param {Node} node Node to look for.
* @return {Node} The corresponding palette item (null if not found).
* @override
*/
goog.ui.emoji.EmojiPaletteRenderer.prototype.getContainingItem =
function(palette, node) {
var root = palette.getElement();
while (node && node.nodeType == goog.dom.NodeType.ELEMENT && node != root) {
if (node.tagName == 'TD') {
return node.firstChild;
}
node = node.parentNode;
}
return null;
};

View File

@@ -0,0 +1,804 @@
// Copyright 2007 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 Emoji Picker implementation. This provides a UI widget for
* choosing an emoji from a grid of possible choices.
*
* @see ../demos/popupemojipicker.html for an example of how to instantiate
* an emoji picker.
*
* Based on goog.ui.ColorPicker (colorpicker.js).
*
* @see ../../demos/popupemojipicker.html
*/
goog.provide('goog.ui.emoji.EmojiPicker');
goog.require('goog.log');
goog.require('goog.style');
goog.require('goog.ui.Component');
goog.require('goog.ui.TabPane');
goog.require('goog.ui.emoji.Emoji');
goog.require('goog.ui.emoji.EmojiPalette');
goog.require('goog.ui.emoji.EmojiPaletteRenderer');
goog.require('goog.ui.emoji.ProgressiveEmojiPaletteRenderer');
/**
* Creates a new, empty emoji picker. An emoji picker is a grid of emoji, each
* cell of the grid containing a single emoji. The picker may contain multiple
* pages of emoji.
*
* When a user selects an emoji, by either clicking or pressing enter, the
* picker fires a goog.ui.Component.EventType.ACTION event with the id. The
* client listens on this event and in the handler can retrieve the id of the
* selected emoji and do something with it, for instance, inserting an image
* tag into a rich text control. An emoji picker does not maintain state. That
* is, once an emoji is selected, the emoji picker does not remember which emoji
* was selected.
*
* The emoji picker is implemented as a tabpane with each tabpage being a table.
* Each of the tables are the same size to prevent jittering when switching
* between pages.
*
* @param {string} defaultImgUrl Url of the img that should be used to fill up
* the cells in the emoji table, to prevent jittering. Should be the same
* size as the emoji.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @extends {goog.ui.Component}
* @constructor
*/
goog.ui.emoji.EmojiPicker = function(defaultImgUrl, opt_domHelper) {
goog.ui.Component.call(this, opt_domHelper);
this.defaultImgUrl_ = defaultImgUrl;
/**
* Emoji that this picker displays.
*
* @type {Array.<Object>}
* @private
*/
this.emoji_ = [];
/**
* Pages of this emoji picker.
*
* @type {Array.<goog.ui.emoji.EmojiPalette>}
* @private
*/
this.pages_ = [];
/**
* Keeps track of which pages in the picker have been loaded. Used for delayed
* loading of tabs.
*
* @type {Array.<boolean>}
* @private
*/
this.pageLoadStatus_ = [];
/**
* Tabpane to hold the pages of this emojipicker.
*
* @type {goog.ui.TabPane}
* @private
*/
this.tabPane_ = null;
this.getHandler().listen(this, goog.ui.Component.EventType.ACTION,
this.onEmojiPaletteAction_);
};
goog.inherits(goog.ui.emoji.EmojiPicker, goog.ui.Component);
/**
* Default number of rows per grid of emoji.
*
* @type {number}
*/
goog.ui.emoji.EmojiPicker.DEFAULT_NUM_ROWS = 5;
/**
* Default number of columns per grid of emoji.
*
* @type {number}
*/
goog.ui.emoji.EmojiPicker.DEFAULT_NUM_COLS = 10;
/**
* Default location of the tabs in relation to the emoji grids.
*
* @type {goog.ui.TabPane.TabLocation}
*/
goog.ui.emoji.EmojiPicker.DEFAULT_TAB_LOCATION =
goog.ui.TabPane.TabLocation.TOP;
/**
* Number of rows per grid of emoji.
*
* @type {number}
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.numRows_ =
goog.ui.emoji.EmojiPicker.DEFAULT_NUM_ROWS;
/**
* Number of columns per grid of emoji.
*
* @type {number}
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.numCols_ =
goog.ui.emoji.EmojiPicker.DEFAULT_NUM_COLS;
/**
* Whether the number of rows in the picker should be automatically determined
* by the specified number of columns so as to minimize/eliminate jitter when
* switching between tabs.
*
* @type {boolean}
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.autoSizeByColumnCount_ = true;
/**
* Location of the tabs for the picker tabpane.
*
* @type {goog.ui.TabPane.TabLocation}
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.tabLocation_ =
goog.ui.emoji.EmojiPicker.DEFAULT_TAB_LOCATION;
/**
* Whether the component is focusable.
* @type {boolean}
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.focusable_ = true;
/**
* Url of the img that should be used for cells in the emoji picker that are
* not filled with emoji, i.e., after all the emoji have already been placed
* on a page.
*
* @type {string}
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.defaultImgUrl_;
/**
* If present, indicates a prefix that should be prepended to all URLs
* of images in this emojipicker. This provides an optimization if the URLs
* are long, so that the client does not have to send a long string for each
* emoji.
*
* @type {string|undefined}
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.urlPrefix_;
/**
* If true, delay loading the images for the emojipalettes until after
* construction. This gives a better user experience before the images are in
* the cache, since other widgets waiting for construction of the emojipalettes
* won't have to wait for all the images (which may be a substantial amount) to
* load.
*
* @type {boolean}
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.delayedLoad_ = false;
/**
* Whether to use progressive rendering in the emojipicker's palette, if using
* sprited imgs. If true, then uses img tags, which most browsers render
* progressively (i.e., as the data comes in). If false, then uses div tags
* with the background-image, which some newer browsers render progressively
* but older ones do not.
*
* @type {boolean}
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.progressiveRender_ = false;
/**
* Whether to require the caller to manually specify when to start loading
* animated emoji. This is primarily for unittests to be able to test the
* structure of the emojipicker palettes before and after the animated emoji
* have been loaded.
*
* @type {boolean}
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.manualLoadOfAnimatedEmoji_ = false;
/**
* Index of the active page in the picker.
*
* @type {number}
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.activePage_ = -1;
/**
* Adds a group of emoji to the picker.
*
* @param {string|Element} title Title for the group.
* @param {Array.<Array.<string>>} emojiGroup A new group of emoji to be added
* Each internal array contains [emojiUrl, emojiId].
*/
goog.ui.emoji.EmojiPicker.prototype.addEmojiGroup =
function(title, emojiGroup) {
this.emoji_.push({title: title, emoji: emojiGroup});
};
/**
* Gets the number of rows per grid in the emoji picker.
*
* @return {number} number of rows per grid.
*/
goog.ui.emoji.EmojiPicker.prototype.getNumRows = function() {
return this.numRows_;
};
/**
* Gets the number of columns per grid in the emoji picker.
*
* @return {number} number of columns per grid.
*/
goog.ui.emoji.EmojiPicker.prototype.getNumColumns = function() {
return this.numCols_;
};
/**
* Sets the number of rows per grid in the emoji picker. This should only be
* called before the picker has been rendered.
*
* @param {number} numRows Number of rows per grid.
*/
goog.ui.emoji.EmojiPicker.prototype.setNumRows = function(numRows) {
this.numRows_ = numRows;
};
/**
* Sets the number of columns per grid in the emoji picker. This should only be
* called before the picker has been rendered.
*
* @param {number} numCols Number of columns per grid.
*/
goog.ui.emoji.EmojiPicker.prototype.setNumColumns = function(numCols) {
this.numCols_ = numCols;
};
/**
* Sets whether to automatically size the emojipicker based on the number of
* columns and the number of emoji in each group, so as to reduce jitter.
*
* @param {boolean} autoSize Whether to automatically size the picker.
*/
goog.ui.emoji.EmojiPicker.prototype.setAutoSizeByColumnCount =
function(autoSize) {
this.autoSizeByColumnCount_ = autoSize;
};
/**
* Sets the location of the tabs in relation to the emoji grids. This should
* only be called before the picker has been rendered.
*
* @param {goog.ui.TabPane.TabLocation} tabLocation The location of the tabs.
*/
goog.ui.emoji.EmojiPicker.prototype.setTabLocation = function(tabLocation) {
this.tabLocation_ = tabLocation;
};
/**
* Sets whether loading of images should be delayed until after dom creation.
* Thus, this function must be called before {@link #createDom}. If set to true,
* the client must call {@link #loadImages} when they wish the images to be
* loaded.
*
* @param {boolean} shouldDelay Whether to delay loading the images.
*/
goog.ui.emoji.EmojiPicker.prototype.setDelayedLoad = function(shouldDelay) {
this.delayedLoad_ = shouldDelay;
};
/**
* Sets whether to require the caller to manually specify when to start loading
* animated emoji. This is primarily for unittests to be able to test the
* structure of the emojipicker palettes before and after the animated emoji
* have been loaded. This only affects sprited emojipickers with sprite data
* for animated emoji.
*
* @param {boolean} manual Whether to load animated emoji manually.
*/
goog.ui.emoji.EmojiPicker.prototype.setManualLoadOfAnimatedEmoji =
function(manual) {
this.manualLoadOfAnimatedEmoji_ = manual;
};
/**
* Returns true if the component is focusable, false otherwise. The default
* is true. Focusable components always have a tab index and allocate a key
* handler to handle keyboard events while focused.
* @return {boolean} Whether the component is focusable.
*/
goog.ui.emoji.EmojiPicker.prototype.isFocusable = function() {
return this.focusable_;
};
/**
* Sets whether the component is focusable. The default is true.
* Focusable components always have a tab index and allocate a key handler to
* handle keyboard events while focused.
* @param {boolean} focusable Whether the component is focusable.
*/
goog.ui.emoji.EmojiPicker.prototype.setFocusable = function(focusable) {
this.focusable_ = focusable;
for (var i = 0; i < this.pages_.length; i++) {
if (this.pages_[i]) {
this.pages_[i].setSupportedState(goog.ui.Component.State.FOCUSED,
focusable);
}
}
};
/**
* Sets the URL prefix for the emoji URLs.
*
* @param {string} urlPrefix Prefix that should be prepended to all URLs.
*/
goog.ui.emoji.EmojiPicker.prototype.setUrlPrefix = function(urlPrefix) {
this.urlPrefix_ = urlPrefix;
};
/**
* Sets the progressive rendering aspect of this emojipicker. Must be called
* before createDom to have an effect.
*
* @param {boolean} progressive Whether this picker should render progressively.
*/
goog.ui.emoji.EmojiPicker.prototype.setProgressiveRender =
function(progressive) {
this.progressiveRender_ = progressive;
};
/**
* Logger for the emoji picker.
*
* @type {goog.log.Logger}
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.logger_ =
goog.log.getLogger('goog.ui.emoji.EmojiPicker');
/**
* Adjusts the number of rows to be the maximum row count out of all the emoji
* groups, in order to prevent jitter in switching among the tabs.
*
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.adjustNumRowsIfNecessary_ = function() {
var currentMax = 0;
for (var i = 0; i < this.emoji_.length; i++) {
var numEmoji = this.emoji_[i].emoji.length;
var rowsNeeded = Math.ceil(numEmoji / this.numCols_);
if (rowsNeeded > currentMax) {
currentMax = rowsNeeded;
}
}
this.setNumRows(currentMax);
};
/**
* Causes the emoji imgs to be loaded into the picker. Used for delayed loading.
* No-op if delayed loading is not set.
*/
goog.ui.emoji.EmojiPicker.prototype.loadImages = function() {
if (!this.delayedLoad_) {
return;
}
// Load the first page only
this.loadPage_(0);
this.activePage_ = 0;
};
/**
* @override
* @suppress {deprecated} Using deprecated goog.ui.TabPane.
*/
goog.ui.emoji.EmojiPicker.prototype.createDom = function() {
this.setElementInternal(this.getDomHelper().createDom('div'));
if (this.autoSizeByColumnCount_) {
this.adjustNumRowsIfNecessary_();
}
if (this.emoji_.length == 0) {
throw Error('Must add some emoji to the picker');
}
// If there is more than one group of emoji, we construct a tabpane
if (this.emoji_.length > 1) {
// Give the tabpane a div to use as its content element, since tabpane
// overwrites the CSS class of the element it's passed
var div = this.getDomHelper().createDom('div');
this.getElement().appendChild(div);
this.tabPane_ = new goog.ui.TabPane(div,
this.tabLocation_,
this.getDomHelper(),
true /* use MOUSEDOWN */);
}
this.renderer_ = this.progressiveRender_ ?
new goog.ui.emoji.ProgressiveEmojiPaletteRenderer(this.defaultImgUrl_) :
new goog.ui.emoji.EmojiPaletteRenderer(this.defaultImgUrl_);
for (var i = 0; i < this.emoji_.length; i++) {
var emoji = this.emoji_[i].emoji;
var page = this.delayedLoad_ ?
this.createPlaceholderEmojiPage_(emoji) :
this.createEmojiPage_(emoji, i);
this.pages_.push(page);
}
this.activePage_ = 0;
this.getElement().tabIndex = 0;
};
/**
* Used by unittests to manually load the animated emoji for this picker.
*/
goog.ui.emoji.EmojiPicker.prototype.manuallyLoadAnimatedEmoji = function() {
for (var i = 0; i < this.pages_.length; i++) {
this.pages_[i].loadAnimatedEmoji();
}
};
/**
* Creates a page if it has not already been loaded. This has the side effects
* of setting the load status of the page to true.
*
* @param {Array.<Array.<string>>} emoji Emoji for this page. See
* {@link addEmojiGroup} for more details.
* @param {number} index Index of the page in the emojipicker.
* @return {goog.ui.emoji.EmojiPalette} the emoji page.
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.createEmojiPage_ = function(emoji, index) {
// Safeguard against trying to create the same page twice
if (this.pageLoadStatus_[index]) {
return null;
}
var palette = new goog.ui.emoji.EmojiPalette(emoji,
this.urlPrefix_,
this.renderer_,
this.getDomHelper());
if (!this.manualLoadOfAnimatedEmoji_) {
palette.loadAnimatedEmoji();
}
palette.setSize(this.numCols_, this.numRows_);
palette.setSupportedState(goog.ui.Component.State.FOCUSED, this.focusable_);
palette.createDom();
palette.setParent(this);
this.pageLoadStatus_[index] = true;
return palette;
};
/**
* Returns an array of emoji whose real URLs have been replaced with the
* default img URL. Used for delayed loading.
*
* @param {Array.<Array.<string>>} emoji Original emoji array.
* @return {Array.<Array.<string>>} emoji array with all emoji pointing to the
* default img.
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.getPlaceholderEmoji_ = function(emoji) {
var placeholderEmoji = [];
for (var i = 0; i < emoji.length; i++) {
placeholderEmoji.push([this.defaultImgUrl_, emoji[i][1]]);
}
return placeholderEmoji;
};
/**
* Creates an emoji page using placeholder emoji pointing to the default
* img instead of the real emoji. Used for delayed loading.
*
* @param {Array.<Array.<string>>} emoji Emoji for this page. See
* {@link addEmojiGroup} for more details.
* @return {goog.ui.emoji.EmojiPalette} the emoji page.
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.createPlaceholderEmojiPage_ =
function(emoji) {
var placeholderEmoji = this.getPlaceholderEmoji_(emoji);
var palette = new goog.ui.emoji.EmojiPalette(placeholderEmoji,
null, // no url prefix
this.renderer_,
this.getDomHelper());
palette.setSize(this.numCols_, this.numRows_);
palette.setSupportedState(goog.ui.Component.State.FOCUSED, this.focusable_);
palette.createDom();
palette.setParent(this);
return palette;
};
/**
* EmojiPickers cannot be used to decorate pre-existing html, since the
* structure they build is fairly complicated.
* @param {Element} element Element to decorate.
* @return {boolean} Returns always false.
* @override
*/
goog.ui.emoji.EmojiPicker.prototype.canDecorate = function(element) {
return false;
};
/**
* @override
* @suppress {deprecated} Using deprecated goog.ui.TabPane.
*/
goog.ui.emoji.EmojiPicker.prototype.enterDocument = function() {
goog.ui.emoji.EmojiPicker.superClass_.enterDocument.call(this);
for (var i = 0; i < this.pages_.length; i++) {
this.pages_[i].enterDocument();
var pageElement = this.pages_[i].getElement();
// Add a new tab to the tabpane if there's more than one group of emoji.
// If there is just one group of emoji, then we simply use the single
// page's element as the content for the picker
if (this.pages_.length > 1) {
// Create a simple default title containg the page number if the title
// was not provided in the emoji group params
var title = this.emoji_[i].title || (i + 1);
this.tabPane_.addPage(new goog.ui.TabPane.TabPage(
pageElement, title, this.getDomHelper()));
} else {
this.getElement().appendChild(pageElement);
}
}
// Initialize listeners. Note that we need to initialize this listener
// after createDom, because addPage causes the goog.ui.TabPane.Events.CHANGE
// event to fire, but we only want the handler (which loads delayed images)
// to run after the picker has been constructed.
if (this.tabPane_) {
this.getHandler().listen(
this.tabPane_, goog.ui.TabPane.Events.CHANGE, this.onPageChanged_);
// Make the tabpane unselectable so that changing tabs doesn't disturb the
// cursor
goog.style.setUnselectable(this.tabPane_.getElement(), true);
}
this.getElement().unselectable = 'on';
};
/** @override */
goog.ui.emoji.EmojiPicker.prototype.exitDocument = function() {
goog.ui.emoji.EmojiPicker.superClass_.exitDocument.call(this);
for (var i = 0; i < this.pages_.length; i++) {
this.pages_[i].exitDocument();
}
};
/** @override */
goog.ui.emoji.EmojiPicker.prototype.disposeInternal = function() {
goog.ui.emoji.EmojiPicker.superClass_.disposeInternal.call(this);
if (this.tabPane_) {
this.tabPane_.dispose();
this.tabPane_ = null;
}
for (var i = 0; i < this.pages_.length; i++) {
this.pages_[i].dispose();
}
this.pages_.length = 0;
};
/**
* @return {string} CSS class for the root element of EmojiPicker.
*/
goog.ui.emoji.EmojiPicker.prototype.getCssClass = function() {
return goog.getCssName('goog-ui-emojipicker');
};
/**
* Returns the currently selected emoji from this picker. If the picker is
* using the URL prefix optimization, allocates a new emoji object with the
* full URL. This method is meant to be used by clients of the emojipicker,
* e.g., in a listener on goog.ui.component.EventType.ACTION that wants to use
* the just-selected emoji.
*
* @return {goog.ui.emoji.Emoji} The currently selected emoji from this picker.
*/
goog.ui.emoji.EmojiPicker.prototype.getSelectedEmoji = function() {
return this.urlPrefix_ ?
new goog.ui.emoji.Emoji(this.urlPrefix_ + this.selectedEmoji_.getId(),
this.selectedEmoji_.getId()) :
this.selectedEmoji_;
};
/**
* Returns the number of emoji groups in this picker.
*
* @return {number} The number of emoji groups in this picker.
*/
goog.ui.emoji.EmojiPicker.prototype.getNumEmojiGroups = function() {
return this.emoji_.length;
};
/**
* Returns a page from the picker. This should be considered protected, and is
* ONLY FOR TESTING.
*
* @param {number} index Index of the page to return.
* @return {goog.ui.emoji.EmojiPalette?} the page at the specified index or null
* if none exists.
*/
goog.ui.emoji.EmojiPicker.prototype.getPage = function(index) {
return this.pages_[index];
};
/**
* Returns all the pages from the picker. This should be considered protected,
* and is ONLY FOR TESTING.
*
* @return {Array.<goog.ui.emoji.EmojiPalette>?} the pages in the picker or
* null if none exist.
*/
goog.ui.emoji.EmojiPicker.prototype.getPages = function() {
return this.pages_;
};
/**
* Returns the tabpane if this is a multipage picker. This should be considered
* protected, and is ONLY FOR TESTING.
*
* @return {goog.ui.TabPane} the tabpane if it is a multipage picker,
* or null if it does not exist or is a single page picker.
*/
goog.ui.emoji.EmojiPicker.prototype.getTabPane = function() {
return this.tabPane_;
};
/**
* @return {goog.ui.emoji.EmojiPalette} The active page of the emoji picker.
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.getActivePage_ = function() {
return this.pages_[this.activePage_];
};
/**
* Handles actions from the EmojiPalettes that this picker contains.
*
* @param {goog.ui.Component.EventType} e The event object.
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.onEmojiPaletteAction_ = function(e) {
this.selectedEmoji_ = this.getActivePage_().getSelectedEmoji();
};
/**
* Handles changes in the active page in the tabpane.
*
* @param {goog.ui.TabPaneEvent} e The event object.
* @private
*/
goog.ui.emoji.EmojiPicker.prototype.onPageChanged_ = function(e) {
var index = /** @type {number} */ (e.page.getIndex());
this.loadPage_(index);
this.activePage_ = index;
};
/**
* Loads a page into the picker if it has not yet been loaded.
*
* @param {number} index Index of the page to load.
* @private
* @suppress {deprecated} Using deprecated goog.ui.TabPane.
*/
goog.ui.emoji.EmojiPicker.prototype.loadPage_ = function(index) {
if (index < 0 || index > this.pages_.length) {
throw Error('Index out of bounds');
}
if (!this.pageLoadStatus_[index]) {
var oldPage = this.pages_[index];
this.pages_[index] = this.createEmojiPage_(this.emoji_[index].emoji,
index);
this.pages_[index].enterDocument();
var pageElement = this.pages_[index].getElement();
if (this.pages_.length > 1) {
this.tabPane_.removePage(index);
var title = this.emoji_[index].title || (index + 1);
this.tabPane_.addPage(new goog.ui.TabPane.TabPage(
pageElement, title, this.getDomHelper()), index);
this.tabPane_.setSelectedIndex(index);
} else {
var el = this.getElement();
el.appendChild(pageElement);
}
if (oldPage) {
oldPage.dispose();
}
}
};

View File

@@ -0,0 +1,410 @@
// Copyright 2007 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 Popup Emoji Picker implementation. This provides a UI widget
* for choosing an emoji from a grid of possible choices. The widget is a popup,
* so it is suitable for a toolbar, for instance the TrogEdit toolbar.
*
* @see ../demos/popupemojipicker.html for an example of how to instantiate
* an emoji picker.
*
* See goog.ui.emoji.EmojiPicker in emojipicker.js for more details.
*
* Based on goog.ui.PopupColorPicker (popupcolorpicker.js).
*
* @see ../../demos/popupemojipicker.html
*/
goog.provide('goog.ui.emoji.PopupEmojiPicker');
goog.require('goog.events.EventType');
goog.require('goog.positioning.AnchoredPosition');
goog.require('goog.positioning.Corner');
goog.require('goog.ui.Component');
goog.require('goog.ui.Popup');
goog.require('goog.ui.emoji.EmojiPicker');
/**
* Constructs a popup emoji picker widget.
*
* @param {string} defaultImgUrl Url of the img that should be used to fill up
* the cells in the emoji table, to prevent jittering. Should be the same
* size as the emoji.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @extends {goog.ui.Component}
* @constructor
*/
goog.ui.emoji.PopupEmojiPicker =
function(defaultImgUrl, opt_domHelper) {
goog.ui.Component.call(this, opt_domHelper);
this.emojiPicker_ = new goog.ui.emoji.EmojiPicker(defaultImgUrl,
opt_domHelper);
this.addChild(this.emojiPicker_);
this.getHandler().listen(this.emojiPicker_,
goog.ui.Component.EventType.ACTION, this.onEmojiPicked_);
};
goog.inherits(goog.ui.emoji.PopupEmojiPicker, goog.ui.Component);
/**
* Instance of an emoji picker control.
* @type {goog.ui.emoji.EmojiPicker}
* @private
*/
goog.ui.emoji.PopupEmojiPicker.prototype.emojiPicker_ = null;
/**
* Instance of goog.ui.Popup used to manage the behavior of the emoji picker.
* @type {goog.ui.Popup}
* @private
*/
goog.ui.emoji.PopupEmojiPicker.prototype.popup_ = null;
/**
* Reference to the element that triggered the last popup.
* @type {Element}
* @private
*/
goog.ui.emoji.PopupEmojiPicker.prototype.lastTarget_ = null;
/**
* Whether the emoji picker can accept focus.
* @type {boolean}
* @private
*/
goog.ui.emoji.PopupEmojiPicker.prototype.focusable_ = true;
/**
* If true, then the emojipicker will toggle off if it is already visible.
* Default is true.
* @type {boolean}
* @private
*/
goog.ui.emoji.PopupEmojiPicker.prototype.toggleMode_ = true;
/**
* Adds a group of emoji to the picker.
*
* @param {string|Element} title Title for the group.
* @param {Array.<Array>} emojiGroup A new group of emoji to be added. Each
* internal array contains [emojiUrl, emojiId].
*/
goog.ui.emoji.PopupEmojiPicker.prototype.addEmojiGroup =
function(title, emojiGroup) {
this.emojiPicker_.addEmojiGroup(title, emojiGroup);
};
/**
* Sets whether the emoji picker should toggle if it is already open.
* @param {boolean} toggle The toggle mode to use.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.setToggleMode = function(toggle) {
this.toggleMode_ = toggle;
};
/**
* Gets whether the emojipicker is in toggle mode
* @return {boolean} toggle.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.getToggleMode = function() {
return this.toggleMode_;
};
/**
* Sets whether loading of images should be delayed until after dom creation.
* Thus, this function must be called before {@link #createDom}. If set to true,
* the client must call {@link #loadImages} when they wish the images to be
* loaded.
*
* @param {boolean} shouldDelay Whether to delay loading the images.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.setDelayedLoad =
function(shouldDelay) {
if (this.emojiPicker_) {
this.emojiPicker_.setDelayedLoad(shouldDelay);
}
};
/**
* Sets whether the emoji picker can accept focus.
* @param {boolean} focusable Whether the emoji picker should accept focus.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.setFocusable = function(focusable) {
this.focusable_ = focusable;
if (this.emojiPicker_) {
// TODO(user): In next revision sort the behavior of passing state to
// children correctly
this.emojiPicker_.setFocusable(focusable);
}
};
/**
* Sets the URL prefix for the emoji URLs.
*
* @param {string} urlPrefix Prefix that should be prepended to all URLs.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.setUrlPrefix = function(urlPrefix) {
this.emojiPicker_.setUrlPrefix(urlPrefix);
};
/**
* Sets the location of the tabs in relation to the emoji grids. This should
* only be called before the picker has been rendered.
*
* @param {goog.ui.TabPane.TabLocation} tabLocation The location of the tabs.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.setTabLocation =
function(tabLocation) {
this.emojiPicker_.setTabLocation(tabLocation);
};
/**
* Sets the number of rows per grid in the emoji picker. This should only be
* called before the picker has been rendered.
*
* @param {number} numRows Number of rows per grid.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.setNumRows = function(numRows) {
this.emojiPicker_.setNumRows(numRows);
};
/**
* Sets the number of columns per grid in the emoji picker. This should only be
* called before the picker has been rendered.
*
* @param {number} numCols Number of columns per grid.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.setNumColumns = function(numCols) {
this.emojiPicker_.setNumColumns(numCols);
};
/**
* Sets the progressive rendering aspect of this emojipicker. Must be called
* before createDom to have an effect.
*
* @param {boolean} progressive Whether the picker should render progressively.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.setProgressiveRender =
function(progressive) {
if (this.emojiPicker_) {
this.emojiPicker_.setProgressiveRender(progressive);
}
};
/**
* Returns the number of emoji groups in this picker.
*
* @return {number} The number of emoji groups in this picker.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.getNumEmojiGroups = function() {
return this.emojiPicker_.getNumEmojiGroups();
};
/**
* Causes the emoji imgs to be loaded into the picker. Used for delayed loading.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.loadImages = function() {
if (this.emojiPicker_) {
this.emojiPicker_.loadImages();
}
};
/** @override */
goog.ui.emoji.PopupEmojiPicker.prototype.createDom = function() {
goog.ui.emoji.PopupEmojiPicker.superClass_.createDom.call(this);
this.emojiPicker_.createDom();
this.getElement().className = goog.getCssName('goog-ui-popupemojipicker');
this.getElement().appendChild(this.emojiPicker_.getElement());
this.popup_ = new goog.ui.Popup(this.getElement());
this.getElement().unselectable = 'on';
};
/** @override */
goog.ui.emoji.PopupEmojiPicker.prototype.disposeInternal = function() {
goog.ui.emoji.PopupEmojiPicker.superClass_.disposeInternal.call(this);
this.emojiPicker_ = null;
this.lastTarget_ = null;
if (this.popup_) {
this.popup_.dispose();
this.popup_ = null;
}
};
/**
* Attaches the popup emoji picker to an element.
*
* @param {Element} element The element to attach to.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.attach = function(element) {
// TODO(user): standardize event type, popups should use MOUSEDOWN, but
// currently apps are using click.
this.getHandler().listen(element, goog.events.EventType.CLICK, this.show_);
};
/**
* Detatches the popup emoji picker from an element.
*
* @param {Element} element The element to detach from.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.detach = function(element) {
this.getHandler().unlisten(element, goog.events.EventType.CLICK, this.show_);
};
/**
* @return {goog.ui.emoji.EmojiPicker} The emoji picker instance.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.getEmojiPicker = function() {
return this.emojiPicker_;
};
/**
* Returns whether the Popup dismisses itself when the user clicks outside of
* it.
* @return {boolean} Whether the Popup autohides on an external click.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.getAutoHide = function() {
return !!this.popup_ && this.popup_.getAutoHide();
};
/**
* Sets whether the Popup dismisses itself when the user clicks outside of it -
* must be called after the Popup has been created (in createDom()),
* otherwise it does nothing.
*
* @param {boolean} autoHide Whether to autohide on an external click.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.setAutoHide = function(autoHide) {
if (this.popup_) {
this.popup_.setAutoHide(autoHide);
}
};
/**
* Returns the region inside which the Popup dismisses itself when the user
* clicks, or null if it was not set. Null indicates the entire document is
* the autohide region.
* @return {Element} The DOM element for autohide, or null if it hasn't been
* set.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.getAutoHideRegion = function() {
return this.popup_ && this.popup_.getAutoHideRegion();
};
/**
* Sets the region inside which the Popup dismisses itself when the user
* clicks - must be called after the Popup has been created (in createDom()),
* otherwise it does nothing.
*
* @param {Element} element The DOM element for autohide.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.setAutoHideRegion = function(element) {
if (this.popup_) {
this.popup_.setAutoHideRegion(element);
}
};
/**
* Returns the {@link goog.ui.PopupBase} from this picker. Returns null if the
* popup has not yet been created.
*
* NOTE: This should *ONLY* be called from tests. If called before createDom(),
* this should return null.
*
* @return {goog.ui.PopupBase?} The popup, or null if it hasn't been created.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.getPopup = function() {
return this.popup_;
};
/**
* @return {Element} The last element that triggered the popup.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.getLastTarget = function() {
return this.lastTarget_;
};
/**
* @return {goog.ui.emoji.Emoji} The currently selected emoji.
*/
goog.ui.emoji.PopupEmojiPicker.prototype.getSelectedEmoji = function() {
return this.emojiPicker_.getSelectedEmoji();
};
/**
* Handles click events on the element this picker is attached to and shows the
* emoji picker in a popup.
*
* @param {goog.events.BrowserEvent} e The browser event.
* @private
*/
goog.ui.emoji.PopupEmojiPicker.prototype.show_ = function(e) {
if (this.popup_.isOrWasRecentlyVisible() && this.toggleMode_ &&
this.lastTarget_ == e.currentTarget) {
this.popup_.setVisible(false);
return;
}
this.lastTarget_ = /** @type {Element} */ (e.currentTarget);
this.popup_.setPosition(new goog.positioning.AnchoredPosition(
this.lastTarget_, goog.positioning.Corner.BOTTOM_LEFT));
this.popup_.setVisible(true);
};
/**
* Handles selection of an emoji.
*
* @param {goog.events.Event} e The event object.
* @private
*/
goog.ui.emoji.PopupEmojiPicker.prototype.onEmojiPicked_ = function(e) {
this.popup_.setVisible(false);
};

View File

@@ -0,0 +1,98 @@
// 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 Progressive Emoji Palette renderer implementation.
*
*/
goog.provide('goog.ui.emoji.ProgressiveEmojiPaletteRenderer');
goog.require('goog.style');
goog.require('goog.ui.emoji.EmojiPaletteRenderer');
/**
* Progressively renders an emoji palette. The progressive renderer tries to
* use img tags instead of background-image for sprited emoji, since most
* browsers render img tags progressively (i.e., as the data comes in), while
* only very new browsers render background-image progressively.
*
* @param {string} defaultImgUrl Url of the img that should be used to fill up
* the cells in the emoji table, to prevent jittering. Will be stretched
* to the emoji cell size. A good image is a transparent dot.
* @constructor
* @extends {goog.ui.emoji.EmojiPaletteRenderer}
*/
goog.ui.emoji.ProgressiveEmojiPaletteRenderer = function(defaultImgUrl) {
goog.ui.emoji.EmojiPaletteRenderer.call(this, defaultImgUrl);
};
goog.inherits(goog.ui.emoji.ProgressiveEmojiPaletteRenderer,
goog.ui.emoji.EmojiPaletteRenderer);
/** @override */
goog.ui.emoji.ProgressiveEmojiPaletteRenderer.prototype.
buildElementFromSpriteMetadata = function(dom, spriteInfo, displayUrl) {
var width = spriteInfo.getWidthCssValue();
var height = spriteInfo.getHeightCssValue();
var x = spriteInfo.getXOffsetCssValue();
var y = spriteInfo.getYOffsetCssValue();
// Need this extra div for proper vertical centering.
var inner = dom.createDom('img', {'src': displayUrl});
var el = /** @type {HTMLDivElement} */ (dom.createDom('div',
goog.getCssName('goog-palette-cell-extra'), inner));
goog.style.setStyle(el, {
'width': width,
'height': height,
'overflow': 'hidden',
'position': 'relative'
});
goog.style.setStyle(inner, {
'left': x,
'top': y,
'position': 'absolute'
});
return el;
};
/** @override */
goog.ui.emoji.ProgressiveEmojiPaletteRenderer.prototype.
updateAnimatedPaletteItem = function(item, animatedImg) {
// Just to be safe, we check for the existence of the img element within this
// palette item before attempting to modify it.
var img;
var el = item.firstChild;
while (el) {
if ('IMG' == el.tagName) {
img = /** @type {Element} */ (el);
break;
}
el = el.firstChild;
}
if (!el) {
return;
}
img.width = animatedImg.width;
img.height = animatedImg.height;
goog.style.setStyle(img, {
'left': 0,
'top': 0
});
img.src = animatedImg.src;
};

View File

@@ -0,0 +1,212 @@
// 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 SpriteInfo implementation. This is a simple wrapper class to
* hold CSS metadata needed for sprited emoji.
*
* @see ../demos/popupemojipicker.html or emojipicker_test.html for examples
* of how to use this class.
*
*/
goog.provide('goog.ui.emoji.SpriteInfo');
/**
* Creates a SpriteInfo object with the specified properties. If the image is
* sprited via CSS, then only the first parameter needs a value. If the image
* is sprited via metadata, then the first parameter should be left null.
*
* @param {?string} cssClass CSS class to properly display the sprited image.
* @param {string=} opt_url Url of the sprite image.
* @param {number=} opt_width Width of the image being sprited.
* @param {number=} opt_height Height of the image being sprited.
* @param {number=} opt_xOffset Positive x offset of the image being sprited
* within the sprite.
* @param {number=} opt_yOffset Positive y offset of the image being sprited
* within the sprite.
* @param {boolean=} opt_animated Whether the sprite is animated.
* @constructor
*/
goog.ui.emoji.SpriteInfo = function(cssClass, opt_url, opt_width, opt_height,
opt_xOffset, opt_yOffset, opt_animated) {
if (cssClass != null) {
this.cssClass_ = cssClass;
} else {
if (opt_url == undefined || opt_width === undefined ||
opt_height === undefined || opt_xOffset == undefined ||
opt_yOffset === undefined) {
throw Error('Sprite info is not fully specified');
}
this.url_ = opt_url;
this.width_ = opt_width;
this.height_ = opt_height;
this.xOffset_ = opt_xOffset;
this.yOffset_ = opt_yOffset;
}
this.animated_ = !!opt_animated;
};
/**
* Name of the CSS class to properly display the sprited image.
* @type {string}
* @private
*/
goog.ui.emoji.SpriteInfo.prototype.cssClass_;
/**
* Url of the sprite image.
* @type {string|undefined}
* @private
*/
goog.ui.emoji.SpriteInfo.prototype.url_;
/**
* Width of the image being sprited.
* @type {number|undefined}
* @private
*/
goog.ui.emoji.SpriteInfo.prototype.width_;
/**
* Height of the image being sprited.
* @type {number|undefined}
* @private
*/
goog.ui.emoji.SpriteInfo.prototype.height_;
/**
* Positive x offset of the image being sprited within the sprite.
* @type {number|undefined}
* @private
*/
goog.ui.emoji.SpriteInfo.prototype.xOffset_;
/**
* Positive y offset of the image being sprited within the sprite.
* @type {number|undefined}
* @private
*/
goog.ui.emoji.SpriteInfo.prototype.yOffset_;
/**
* Whether the emoji specified by the sprite is animated.
* @type {boolean}
* @private
*/
goog.ui.emoji.SpriteInfo.prototype.animated_;
/**
* Returns the css class of the sprited image.
* @return {?string} Name of the CSS class to properly display the sprited
* image.
*/
goog.ui.emoji.SpriteInfo.prototype.getCssClass = function() {
return this.cssClass_ || null;
};
/**
* Returns the url of the sprite image.
* @return {?string} Url of the sprite image.
*/
goog.ui.emoji.SpriteInfo.prototype.getUrl = function() {
return this.url_ || null;
};
/**
* Returns whether the emoji specified by this sprite is animated.
* @return {boolean} Whether the emoji is animated.
*/
goog.ui.emoji.SpriteInfo.prototype.isAnimated = function() {
return this.animated_;
};
/**
* Returns the width of the image being sprited, appropriate for a CSS value.
* @return {string} The width of the image being sprited.
*/
goog.ui.emoji.SpriteInfo.prototype.getWidthCssValue = function() {
return goog.ui.emoji.SpriteInfo.getCssPixelValue_(this.width_);
};
/**
* Returns the height of the image being sprited, appropriate for a CSS value.
* @return {string} The height of the image being sprited.
*/
goog.ui.emoji.SpriteInfo.prototype.getHeightCssValue = function() {
return goog.ui.emoji.SpriteInfo.getCssPixelValue_(this.height_);
};
/**
* Returns the x offset of the image being sprited within the sprite,
* appropriate for a CSS value.
* @return {string} The x offset of the image being sprited within the sprite.
*/
goog.ui.emoji.SpriteInfo.prototype.getXOffsetCssValue = function() {
return goog.ui.emoji.SpriteInfo.getOffsetCssValue_(this.xOffset_);
};
/**
* Returns the positive y offset of the image being sprited within the sprite,
* appropriate for a CSS value.
* @return {string} The y offset of the image being sprited within the sprite.
*/
goog.ui.emoji.SpriteInfo.prototype.getYOffsetCssValue = function() {
return goog.ui.emoji.SpriteInfo.getOffsetCssValue_(this.yOffset_);
};
/**
* Returns a string appropriate for use as a CSS value. If the value is zero,
* then there is no unit appended.
*
* @param {number|undefined} value A number to be turned into a
* CSS size/location value.
* @return {string} A string appropriate for use as a CSS value.
* @private
*/
goog.ui.emoji.SpriteInfo.getCssPixelValue_ = function(value) {
return !value ? '0' : value + 'px';
};
/**
* Returns a string appropriate for use as a CSS value for a position offset,
* such as the position argument for sprites.
*
* @param {number|undefined} posOffset A positive offset for a position.
* @return {string} A string appropriate for use as a CSS value.
* @private
*/
goog.ui.emoji.SpriteInfo.getOffsetCssValue_ = function(posOffset) {
var offset = goog.ui.emoji.SpriteInfo.getCssPixelValue_(posOffset);
return offset == '0' ? offset : '-' + offset;
};

View File

@@ -0,0 +1,47 @@
// Copyright 2009 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.equation.ArrowPalette');
goog.require('goog.math.Size');
goog.require('goog.ui.equation.Palette');
/**
* Constructs a new arrows palette.
* @param {goog.ui.equation.PaletteManager} paletteManager The
* manager of the palette.
* @extends {goog.ui.equation.Palette}
* @constructor
*/
goog.ui.equation.ArrowPalette = function(paletteManager) {
goog.ui.equation.Palette.call(this, paletteManager,
goog.ui.equation.Palette.Type.ARROW,
0, 150, 18, 18,
['\\leftarrow',
'\\rightarrow',
'\\leftrightarrow',
'\\Leftarrow',
'\\Rightarrow',
'\\Leftrightarrow',
'\\uparrow',
'\\downarrow',
'\\updownarrow',
'\\Uparrow',
'\\Downarrow',
'\\Updownarrow']);
this.setSize(new goog.math.Size(12, 1));
};
goog.inherits(goog.ui.equation.ArrowPalette, goog.ui.equation.Palette);

View File

@@ -0,0 +1,37 @@
// Copyright 2011 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.equation.ChangeEvent');
goog.require('goog.events.Event');
/**
* Event fired when equation changes.
* @constructor
* @param {boolean} isValid Whether the equation is valid.
* @extends {goog.events.Event}
*/
goog.ui.equation.ChangeEvent = function(isValid) {
goog.events.Event.call(this, 'change');
/**
* Whether equation is valid.
* @type {boolean}
*/
this.isValid = isValid;
};
goog.inherits(goog.ui.equation.ChangeEvent, goog.events.Event);

View File

@@ -0,0 +1,56 @@
// Copyright 2009 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.equation.ComparisonPalette');
goog.require('goog.math.Size');
goog.require('goog.ui.equation.Palette');
/**
* Constructs a new comparison palette.
* @param {goog.ui.equation.PaletteManager} paletteManager The
* manager of the palette.
* @extends {goog.ui.equation.Palette}
* @constructor
*/
goog.ui.equation.ComparisonPalette = function(paletteManager) {
goog.ui.equation.Palette.call(this, paletteManager,
goog.ui.equation.Palette.Type.COMPARISON,
0, 70, 18, 18,
['\\leq',
'\\geq',
'\\prec',
'\\succ',
'\\preceq',
'\\succeq',
'\\ll',
'\\gg',
'\\equiv',
'\\sim',
'\\\simeq',
'\\\asymp',
'\\approx',
'\\ne',
'\\\subset',
'\\supset',
'\\subseteq',
'\\supseteq',
'\\in',
'\\ni',
'\\notin']);
this.setSize(new goog.math.Size(7, 3));
};
goog.inherits(goog.ui.equation.ComparisonPalette, goog.ui.equation.Palette);

View File

@@ -0,0 +1,93 @@
// 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.equation.EditorPane');
goog.require('goog.style');
goog.require('goog.ui.Component');
/**
* An abstract equation editor tab pane.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @constructor
* @extends {goog.ui.Component}
*/
goog.ui.equation.EditorPane = function(opt_domHelper) {
goog.ui.Component.call(this, opt_domHelper);
};
goog.inherits(goog.ui.equation.EditorPane, goog.ui.Component);
/**
* A link to any available help documentation to be displayed in a "Learn more"
* link. If not set through the equationeditor plugin constructor, the link
* will be omitted.
* @type {string}
* @private
*/
goog.ui.equation.EditorPane.prototype.helpUrl_ = '';
/**
* Sets the visibility of this tab pane.
* @param {boolean} visible Whether this tab should become visible.
*/
goog.ui.equation.EditorPane.prototype.setVisible =
function(visible) {
goog.style.setElementShown(this.getElement(), visible);
};
/**
* Sets the equation to show in this tab pane.
* @param {string} equation The equation.
* @protected
*/
goog.ui.equation.EditorPane.prototype.setEquation = goog.abstractMethod;
/**
* @return {string} The equation shown in this tab pane.
* @protected
*/
goog.ui.equation.EditorPane.prototype.getEquation = goog.abstractMethod;
/**
* Sets the help link URL to show in this tab pane.
* @param {string} url The help link URL.
* @protected
*/
goog.ui.equation.EditorPane.prototype.setHelpUrl = function(url) {
this.helpUrl_ = url;
};
/**
* @return {string} The help link URL.
* @protected
*/
goog.ui.equation.EditorPane.prototype.getHelpUrl = function() {
return this.helpUrl_;
};
/**
* @return {boolean} Whether the equation was modified.
* @protected
*/
goog.ui.equation.EditorPane.prototype.isModified = goog.abstractMethod;

View File

@@ -0,0 +1,220 @@
// 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.equation.EquationEditor');
goog.require('goog.events');
goog.require('goog.ui.Component');
goog.require('goog.ui.TabBar');
goog.require('goog.ui.equation.ImageRenderer');
goog.require('goog.ui.equation.TexPane');
/**
* User interface for equation editor plugin.
* @constructor
* @param {Object} context The context that this equation editor runs in.
* @param {goog.dom.DomHelper=} opt_domHelper DomHelper to use.
* @param {string=} opt_helpUrl Help document URL to use in the "Learn more"
* link.
* @extends {goog.ui.Component}
*/
goog.ui.equation.EquationEditor = function(context, opt_domHelper,
opt_helpUrl) {
goog.base(this, opt_domHelper);
/**
* The context this editor runs in.
* @type {Object}
* @private
*/
this.context_ = context;
/**
* Help document URL to use in the "Learn more" link.
* @type {string}
* @private
*/
this.helpUrl_ = opt_helpUrl || '';
};
goog.inherits(goog.ui.equation.EquationEditor, goog.ui.Component);
/**
* Constants for event names.
* @enum {string}
*/
goog.ui.equation.EquationEditor.EventType = {
/**
* Dispatched when equation changes.
*/
CHANGE: 'change'
};
/**
* The index of the last active tab. Zero means first tab.
* @type {number}
* @private
*/
goog.ui.equation.EquationEditor.prototype.activeTabIndex_ = 0;
/** @override */
goog.ui.equation.EquationEditor.prototype.createDom = function() {
goog.base(this, 'createDom');
this.createDom_();
};
/**
* Creates main editor contents.
* @private
*/
goog.ui.equation.EquationEditor.prototype.createDom_ = function() {
var contentElement = this.getElement();
/** @desc Title of the visual equation editor tab. */
var MSG_VISUAL_EDITOR = goog.getMsg('Editor');
/** @desc Title of the TeX equation editor tab. */
var MSG_TEX_EDITOR = goog.getMsg('TeX');
// Create the main tabs
var dom = this.dom_;
var tabTop = dom.createDom('div',
{'class': 'goog-tab-bar goog-tab-bar-top'},
dom.createDom('div',
{'class': 'goog-tab goog-tab-selected'}, MSG_VISUAL_EDITOR),
dom.createDom('div', {'class': 'goog-tab'}, MSG_TEX_EDITOR));
var tabClear = dom.createDom('div', {'class': 'goog-tab-bar-clear'});
var tabContent = dom.createDom('div', {'class': 'ee-content'});
dom.appendChild(contentElement, tabTop);
dom.appendChild(contentElement, tabClear);
dom.appendChild(contentElement, tabContent);
var tabBar = new goog.ui.TabBar();
tabBar.decorate(tabTop);
/**
* The tab bar.
* @type {!goog.ui.TabBar}
* @private
*/
this.tabBar_ = tabBar;
goog.events.listen(tabBar, goog.ui.Component.EventType.SELECT,
goog.bind(this.handleTabSelect_, this));
var texEditor = new goog.ui.equation.TexPane(this.context_,
this.helpUrl_, this.dom_);
this.addChild(texEditor);
texEditor.render(tabContent);
this.setVisibleTab_(0); // Make first tab visible
};
/**
* Sets the visibility of the editor.
* @param {boolean} visible Whether the editor should be visible.
*/
goog.ui.equation.EquationEditor.prototype.setVisible = function(visible) {
// Show active tab if visible, or none if not
this.setVisibleTab_(visible ? this.activeTabIndex_ : -1);
};
/**
* Sets the tab at the selected index as visible and all the rest as not
* visible.
* @param {number} tabIndex The tab index that is visible. -1 means no
* tab is visible.
* @private
*/
goog.ui.equation.EquationEditor.prototype.setVisibleTab_ = function(tabIndex) {
for (var i = 0; i < this.getChildCount(); i++) {
this.getChildAt(i).setVisible(i == tabIndex);
}
};
/** @override */
goog.ui.equation.EquationEditor.prototype.decorateInternal = function(element) {
this.setElementInternal(element);
this.createDom_();
};
/**
* Returns the encoded equation.
* @return {string} The encoded equation.
*/
goog.ui.equation.EquationEditor.prototype.getEquation = function() {
var sel = this.tabBar_.getSelectedTabIndex();
return this.getChildAt(sel).getEquation();
};
/**
* @return {string} The html code to embed in the document.
*/
goog.ui.equation.EquationEditor.prototype.getHtml = function() {
return goog.ui.equation.ImageRenderer.getHtml(this.getEquation());
};
/**
* Checks whether the current equation is valid and can be used in a document.
* @return {boolean} Whether the equation is valid.
*/
goog.ui.equation.EquationEditor.prototype.isValid = function() {
return goog.ui.equation.ImageRenderer.isEquationTooLong(
this.getEquation());
};
/**
* Handles a tab selection by the user.
* @param {goog.events.Event} e The event.
* @private
*/
goog.ui.equation.EquationEditor.prototype.handleTabSelect_ = function(e) {
var sel = this.tabBar_.getSelectedTabIndex();
if (sel != this.activeTabIndex_) {
this.activeTabIndex_ = sel;
this.setVisibleTab_(sel);
}
// TODO(user) Pass equation from the tab to the other is modified
};
/**
* Parse an equation and draw it.
* Clears any previous displayed equation.
* @param {string} equation The equation text to parse.
*/
goog.ui.equation.EquationEditor.prototype.setEquation = function(equation) {
var sel = this.tabBar_.getSelectedTabIndex();
this.getChildAt(sel).setEquation(equation);
};
/** @override */
goog.ui.equation.EquationEditor.prototype.disposeInternal = function() {
this.context_ = null;
goog.base(this, 'disposeInternal');
};

View File

@@ -0,0 +1,137 @@
// 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.equation.EquationEditorDialog');
goog.require('goog.dom');
goog.require('goog.dom.classes');
goog.require('goog.ui.Dialog');
goog.require('goog.ui.equation.EquationEditor');
goog.require('goog.ui.equation.PaletteManager');
goog.require('goog.ui.equation.TexEditor');
/**
* User interface for equation editor plugin standalone tests.
* @constructor
* @param {string=} opt_equation Encoded equation. If not specified, starts with
* an empty equation.
* @extends {goog.ui.Dialog}
*/
goog.ui.equation.EquationEditorDialog = function(opt_equation) {
goog.ui.Dialog.call(this);
this.setTitle('Equation Editor');
var buttonSet = new goog.ui.Dialog.ButtonSet();
buttonSet.set(goog.ui.Dialog.DefaultButtonKeys.OK,
opt_equation ? 'Save changes' : 'Insert equation',
true);
buttonSet.set(goog.ui.Dialog.DefaultButtonKeys.CANCEL,
'Cancel', false, true);
this.setButtonSet(buttonSet);
// Create the main editor contents.
var contentElement = this.getContentElement();
var domHelper = goog.dom.getDomHelper(contentElement);
var context = this.populateContext_();
/**
* The equation editor main API.
* @type {goog.ui.equation.TexEditor}
* @private
*/
this.equationEditor_ =
new goog.ui.equation.TexEditor(context, '', domHelper);
this.equationEditor_.addEventListener(
goog.ui.equation.EquationEditor.EventType.CHANGE,
this.onChange_, false, this);
this.equationEditor_.render(this.getContentElement());
this.setEquation(opt_equation || '');
goog.dom.classes.add(this.getDialogElement(), 'ee-modal-dialog');
};
goog.inherits(goog.ui.equation.EquationEditorDialog, goog.ui.Dialog);
/**
* The dialog's OK button element.
* @type {Element?}
* @private
*/
goog.ui.equation.EquationEditorDialog.prototype.okButton_;
/** @override */
goog.ui.equation.EquationEditorDialog.prototype.setVisible = function(visible) {
goog.base(this, 'setVisible', visible);
this.equationEditor_.setVisible(visible);
};
/**
* Populates the context of this dialog.
* @return {Object} The context that this dialog runs in.
* @private
*/
goog.ui.equation.EquationEditorDialog.prototype.populateContext_ = function() {
var context = {};
context.paletteManager = new goog.ui.equation.PaletteManager(
this.getDomHelper());
return context;
};
/**
* Handles CHANGE event fired when user changes equation.
* @param {goog.ui.equation.ChangeEvent} e The event object.
* @private
*/
goog.ui.equation.EquationEditorDialog.prototype.onChange_ = function(e) {
if (!this.okButton_) {
this.okButton_ = this.getButtonSet().getButton(
goog.ui.Dialog.DefaultButtonKeys.OK);
}
this.okButton_.disabled = !e.isValid;
};
/**
* Returns the encoded equation.
* @return {string} The encoded equation.
*/
goog.ui.equation.EquationEditorDialog.prototype.getEquation = function() {
return this.equationEditor_.getEquation();
};
/**
* Sets the encoded equation.
* @param {string} equation The encoded equation.
*/
goog.ui.equation.EquationEditorDialog.prototype.setEquation =
function(equation) {
this.equationEditor_.setEquation(equation);
};
/**
* @return {string} The html code to embed in the document.
*/
goog.ui.equation.EquationEditorDialog.prototype.getHtml = function() {
return this.equationEditor_.getHtml();
};

View File

@@ -0,0 +1,75 @@
// Copyright 2009 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.equation.GreekPalette');
goog.require('goog.math.Size');
goog.require('goog.ui.equation.Palette');
/**
* Constructs a new Greek symbols palette.
* @param {goog.ui.equation.PaletteManager} paletteManager The
* manager of the palette.
* @extends {goog.ui.equation.Palette}
* @constructor
*/
goog.ui.equation.GreekPalette = function(paletteManager) {
goog.ui.equation.Palette.call(this, paletteManager,
goog.ui.equation.Palette.Type.GREEK,
0, 30, 18, 18,
['\\alpha',
'\\beta',
'\\gamma',
'\\delta',
'\\epsilon',
'\\varepsilon',
'\\zeta',
'\\eta',
'\\theta',
'\\vartheta',
'\\iota',
'\\kappa',
'\\lambda',
'\\mu',
'\\nu',
'\\xi',
'\\pi',
'\\varpi',
'\\rho',
'\\varrho',
'\\sigma',
'\\varsigma',
'\\tau',
'\\upsilon',
'\\phi',
'\\varphi',
'\\chi',
'\\psi',
'\\omega',
'\\Gamma',
'\\Delta',
'\\Theta',
'\\Lambda',
'\\Xi',
'\\Pi',
'\\Sigma',
'\\Upsilon',
'\\Phi',
'\\Psi',
'\\Omega']);
this.setSize(new goog.math.Size(7, 6));
};
goog.inherits(goog.ui.equation.GreekPalette, goog.ui.equation.Palette);

View File

@@ -0,0 +1,190 @@
// Copyright 2009 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 Functions for rendering the equation images.
*
*/
goog.provide('goog.ui.equation.ImageRenderer');
goog.require('goog.dom.TagName');
goog.require('goog.dom.classes');
goog.require('goog.string');
goog.require('goog.uri.utils');
/**
* The server name which renders the equations.
* We use https as equations may be embedded in https pages
* and using https prevents mixed content warnings. Note that
* https equations work only on google.com domains.
* @type {string}
* @private
*/
goog.ui.equation.ImageRenderer.SERVER_NAME_ =
'https://www.google.com';
/**
* The longest equation which may be displayed, in characters.
* @type {number}
*/
goog.ui.equation.ImageRenderer.MAX_EQUATION_LENGTH = 200;
/**
* Class to put on our equations IMG elements.
* @type {string}
*/
goog.ui.equation.ImageRenderer.EE_IMG_CLASS = 'ee_img';
/**
* Non-standard to put on our equations IMG elements. Useful when classes need
* to be scrubbed from the user-generated HTML, but non-standard attributes
* can be white-listed.
*
* @type {string}
*/
goog.ui.equation.ImageRenderer.EE_IMG_ATTR = 'eeimg';
/**
* Vertical alignment for the equations IMG elements.
* @type {string}
*/
goog.ui.equation.ImageRenderer.EE_IMG_VERTICAL_ALIGN = 'middle';
/**
* The default background color as used in the img url, which is fully
* transparent white.
* @type {string}
*/
goog.ui.equation.ImageRenderer.BACKGROUND_COLOR = 'FFFFFF00';
/**
* The default foreground color as used in the img url, which is black.
* @type {string}
*/
goog.ui.equation.ImageRenderer.FOREGROUND_COLOR = '000000';
/**
* Class to put on IMG elements to keep the resize property bubble from
* appearing. This is different from PLACEHOLDER_IMG_CLASS because it's
* reasonable in some cases to be able to resize a placeholder (which should
* be reflected when the placeholder is replaced with the other content).
* @type {string}
*/
goog.ui.equation.ImageRenderer.NO_RESIZE_IMG_CLASS =
goog.getCssName('tr_noresize');
/**
* Returns the equation image src url given the equation.
* @param {string} equation The equation.
* @return {string} The equation image src url (empty string in case the
* equation was empty).
*/
goog.ui.equation.ImageRenderer.getImageUrl = function(equation) {
if (!equation) {
return '';
}
var url = goog.ui.equation.ImageRenderer.SERVER_NAME_ +
'/chart?cht=tx' +
'&chf=bg,s,' +
goog.ui.equation.ImageRenderer.BACKGROUND_COLOR +
'&chco=' +
goog.ui.equation.ImageRenderer.FOREGROUND_COLOR +
'&chl=' +
encodeURIComponent(equation);
return url;
};
/**
* Returns the equation string src for given image url.
* @param {string} imageUrl The image url.
* @return {string?} The equation string, null if imageUrl cannot be parsed.
*/
goog.ui.equation.ImageRenderer.getEquationFromImageUrl = function(imageUrl) {
return goog.uri.utils.getParamValue(imageUrl, 'chl');
};
/**
* Gets the equation string from the given equation IMG node. Returns empty
* string if the src attribute of the is not a valid equation url.
* @param {Element} equationNode The equation IMG element.
* @return {string} The equation string.
*/
goog.ui.equation.ImageRenderer.getEquationFromImage = function(equationNode) {
var url = equationNode.getAttribute('src');
if (!url) {
// Should never happen.
return '';
}
return goog.ui.equation.ImageRenderer.getEquationFromImageUrl(
url) || '';
};
/**
* Checks whether given node is an equation element.
* @param {Node} node The node to check.
* @return {boolean} Whether given node is an equation element.
*/
goog.ui.equation.ImageRenderer.isEquationElement = function(node) {
return node.nodeName == goog.dom.TagName.IMG &&
(node.getAttribute(
goog.ui.equation.ImageRenderer.EE_IMG_ATTR) ||
goog.dom.classes.has(node,
goog.ui.equation.ImageRenderer.EE_IMG_CLASS));
};
/**
* Returns the html for the html image tag for the given equation.
* @param {string} equation The equation.
* @return {string} The html code to embed in the document.
*/
goog.ui.equation.ImageRenderer.getHtml = function(equation) {
var imageSrc =
goog.ui.equation.ImageRenderer.getImageUrl(equation);
if (!imageSrc) {
return '';
}
return '<img src="' + imageSrc + '" ' +
'alt="' + goog.string.htmlEscape(equation) + '" ' +
'class="' + goog.ui.equation.ImageRenderer.EE_IMG_CLASS +
' ' + goog.ui.equation.ImageRenderer.NO_RESIZE_IMG_CLASS +
'" ' + goog.ui.equation.ImageRenderer.EE_IMG_ATTR + '="1" ' +
'style="vertical-align: ' +
goog.ui.equation.ImageRenderer.EE_IMG_VERTICAL_ALIGN + '">';
};
/**
* Checks whether equation is too long to be displayed.
* @param {string} equation The equation to test.
* @return {boolean} Whether the equation is too long.
*/
goog.ui.equation.ImageRenderer.isEquationTooLong = function(equation) {
return equation.length >
goog.ui.equation.ImageRenderer.MAX_EQUATION_LENGTH;
};

View File

@@ -0,0 +1,55 @@
// Copyright 2009 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.equation.MathPalette');
goog.require('goog.math.Size');
goog.require('goog.ui.equation.Palette');
/**
* Constructs a new math palette.
* @param {goog.ui.equation.PaletteManager} paletteManager The
* manager of the palette.
* @extends {goog.ui.equation.Palette}
* @constructor
*/
goog.ui.equation.MathPalette = function(paletteManager) {
goog.ui.equation.Palette.call(this, paletteManager,
goog.ui.equation.Palette.Type.MATH,
0, 90, 30, 56,
['x_{a}',
'x^{b}',
'x_{a}^{b}',
'\\bar{x}',
'\\tilde{x}',
'\\frac{a}{b}',
'\\sqrt{x}',
'\\sqrt[n]{x}',
'\\bigcap_{a}^{b}',
'\\bigcup_{a}^{b}',
'\\prod_{a}^{b}',
'\\coprod_{a}^{b}',
'\\left( x \\right)',
'\\left[ x \\right]',
'\\left\\{ x \\right\\}',
'\\left| x \\right|',
'\\int_{a}^{b}',
'\\oint_{a}^{b}',
'\\sum_{a}^{b}{x}',
'\\lim_{a \\rightarrow b}{x}']);
this.setSize(new goog.math.Size(10, 2));
};
goog.inherits(goog.ui.equation.MathPalette, goog.ui.equation.Palette);

View File

@@ -0,0 +1,86 @@
// Copyright 2009 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.equation.MenuPalette');
goog.provide('goog.ui.equation.MenuPaletteRenderer');
goog.require('goog.math.Size');
goog.require('goog.ui.PaletteRenderer');
goog.require('goog.ui.equation.Palette');
goog.require('goog.ui.equation.PaletteRenderer');
/**
* Constructs a new menu palette.
* @param {goog.ui.equation.PaletteManager} paletteManager The
* manager of the palette.
* @extends {goog.ui.equation.Palette}
* @constructor
*/
goog.ui.equation.MenuPalette = function(paletteManager) {
goog.ui.equation.Palette.call(this, paletteManager,
goog.ui.equation.Palette.Type.MENU,
0, 0, 46, 18,
[goog.ui.equation.Palette.Type.GREEK,
goog.ui.equation.Palette.Type.SYMBOL,
goog.ui.equation.Palette.Type.COMPARISON,
goog.ui.equation.Palette.Type.MATH,
goog.ui.equation.Palette.Type.ARROW],
goog.ui.equation.MenuPaletteRenderer.getInstance());
this.setSize(new goog.math.Size(5, 1));
};
goog.inherits(goog.ui.equation.MenuPalette, goog.ui.equation.Palette);
/**
* The CSS class name for the palette.
* @type {string}
*/
goog.ui.equation.MenuPalette.CSS_CLASS = 'ee-menu-palette';
/**
* Overrides the setVisible method to make menu palette always visible.
* @param {boolean} visible Whether to show or hide the component.
* @param {boolean=} opt_force If true, doesn't check whether the component
* already has the requested visibility, and doesn't dispatch any events.
* @return {boolean} Whether the visibility was changed.
* @override
*/
goog.ui.equation.MenuPalette.prototype.setVisible = function(
visible, opt_force) {
return goog.base(this, 'setVisible', true, opt_force);
};
/**
* The renderer for menu palette.
* @extends {goog.ui.equation.PaletteRenderer}
* @constructor
*/
goog.ui.equation.MenuPaletteRenderer = function() {
goog.ui.PaletteRenderer.call(this);
};
goog.inherits(goog.ui.equation.MenuPaletteRenderer,
goog.ui.equation.PaletteRenderer);
goog.addSingletonGetter(goog.ui.equation.MenuPaletteRenderer);
/** @override */
goog.ui.equation.MenuPaletteRenderer.prototype.getCssClass =
function() {
return goog.ui.equation.MenuPalette.CSS_CLASS;
};

View File

@@ -0,0 +1,288 @@
// Copyright 2009 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 palette of icons.
* The icons are generated from a single sprite image that
* is used for multiple palettes.
* All icons of a single palette must be on the same sprite row
* (same y coordinate) and all have the same width.
* Each item has an associated action command that should be taken
* when certain event is dispatched.
*
*/
goog.provide('goog.ui.equation.Palette');
goog.provide('goog.ui.equation.PaletteEvent');
goog.provide('goog.ui.equation.PaletteRenderer');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.events.Event');
goog.require('goog.ui.Palette');
goog.require('goog.ui.PaletteRenderer');
/**
* Constructs a new palette.
* @param {goog.ui.equation.PaletteManager} paletteManager The
* manager of the palette.
* @param {goog.ui.equation.Palette.Type} type The type of the
* palette.
* @param {number} spriteX Coordinate of first icon in sprite.
* @param {number} spriteY Coordinate of top of all icons in sprite.
* @param {number} itemWidth Pixel width of each palette icon.
* @param {number} itemHeight Pixel height of each palette icon.
* @param {Array.<string>=} opt_actions An optional action list for palette
* elements. The number of actions determine the number of palette
* elements.
* @param {goog.ui.PaletteRenderer=} opt_renderer Optional customized renderer,
* defaults to {@link goog.ui.PaletteRenderer}.
* @extends {goog.ui.Palette}
* @constructor
*/
goog.ui.equation.Palette = function(paletteManager, type, spriteX,
spriteY, itemWidth, itemHeight, opt_actions, opt_renderer) {
/**
* The type of the palette.
* @type {goog.ui.equation.Palette.Type}
* @private
*/
this.type_ = type;
/**
* The palette actions.
* @type {Array.<string>}
* @private
*/
this.actions_ = opt_actions || [];
var renderer =
opt_renderer ||
goog.ui.equation.PaletteRenderer.getInstance();
// Create a div element for each icon.
var elements = [];
var x = - spriteX;
var y = - spriteY;
for (var i = 0; i < opt_actions.length; i++) {
elements.push(paletteManager.getDomHelper().createDom(
goog.dom.TagName.DIV,
{'class': renderer.getItemCssClass(),
'style': 'width:' + itemWidth +
'px;height:' + itemHeight +
'px;' +
'background-position:' +
x + 'px ' + y + 'px;'}));
x -= itemWidth;
}
/**
* The palette manager that manages all the palettes.
* @type {goog.ui.equation.PaletteManager}
* @private
*/
this.paletteManager_ = paletteManager;
goog.ui.Palette.call(this, elements, renderer, paletteManager.getDomHelper());
};
goog.inherits(goog.ui.equation.Palette, goog.ui.Palette);
/**
* The type of possible palettes. They are made short to minimize JS size.
* @enum {string}
*/
goog.ui.equation.Palette.Type = {
MENU: 'mn',
GREEK: 'g',
SYMBOL: 's',
COMPARISON: 'c',
MATH: 'm',
ARROW: 'a'
};
/**
* The CSS class name for the palette.
* @type {string}
*/
goog.ui.equation.Palette.CSS_CLASS = 'ee-palette';
/**
* Returns the type of the palette.
* @return {goog.ui.equation.Palette.Type} The type of the palette.
*/
goog.ui.equation.Palette.prototype.getType = function() {
return this.type_;
};
/**
* Returns the palette manager.
* @return {goog.ui.equation.PaletteManager} The palette manager
* that manages all the palette.
*/
goog.ui.equation.Palette.prototype.getPaletteManager = function() {
return this.paletteManager_;
};
/**
* Returns actions for this palette.
* @return {Array.<string>} The palette actions.
*/
goog.ui.equation.Palette.prototype.getActions = function() {
return this.actions_;
};
/**
* Returns the action for a given index.
* @param {number} index The index of the action to be retrieved.
* @return {string?} The action for given index, or {@code null} if action is
* not found.
*/
goog.ui.equation.Palette.prototype.getAction = function(index) {
return (index >= 0 && index < this.actions_.length) ?
this.actions_[index] : null;
};
/**
* Handles mouseup events. Overrides {@link goog.ui.Palette#handleMouseUp}
* by dispatching a {@link goog.ui.equation.PaletteEvent}.
* @param {goog.events.Event} e Mouse event to handle.
* @override
*/
goog.ui.equation.Palette.prototype.handleMouseUp = function(e) {
goog.base(this, 'handleMouseUp', e);
this.paletteManager_.dispatchEvent(
new goog.ui.equation.PaletteEvent(
goog.ui.equation.PaletteEvent.Type.ACTION, this));
};
/**
* Handles mouse out events. Overrides {@link goog.ui.Palette#handleMouseOut}
* by deactivate the palette.
* @param {goog.events.BrowserEvent} e Mouse event to handle.
* @override
*/
goog.ui.equation.Palette.prototype.handleMouseOut = function(e) {
goog.base(this, 'handleMouseOut', e);
// Ignore mouse moves between descendants.
if (e.relatedTarget &&
!goog.dom.contains(this.getElement(), e.relatedTarget)) {
this.paletteManager_.deactivate();
}
};
/**
* Handles mouse over events. Overrides {@link goog.ui.Palette#handleMouseOver}
* by stop deactivating the palette. When mouse leaves the palettes, the
* palettes will be deactivated after a centain period of time. Reentering the
* palettes inside this time will stop the timer and cancel the deactivation.
* @param {goog.events.BrowserEvent} e Mouse event to handle.
* @override
*/
goog.ui.equation.Palette.prototype.handleMouseOver = function(e) {
goog.base(this, 'handleMouseOver', e);
// Ignore mouse moves between descendants.
if (e.relatedTarget &&
!goog.dom.contains(this.getElement(), e.relatedTarget)) {
// Stop the timer to deactivate the palettes.
this.paletteManager_.stopDeactivation();
}
};
/**
* The event that palettes dispatches.
* @param {string} type Type of the event.
* @param {goog.ui.equation.Palette} palette The palette that the
* event is fired on.
* @param {Element=} opt_target The optional target of the event.
* @constructor
* @extends {goog.events.Event}
*/
goog.ui.equation.PaletteEvent = function(type, palette, opt_target) {
goog.events.Event.call(this, type, opt_target);
/**
* The palette the event is fired from.
* @type {goog.ui.equation.Palette}
* @private
*/
this.palette_ = palette;
};
/**
* The type of events that can be fired on palettes.
* @enum {string}
*/
goog.ui.equation.PaletteEvent.Type = {
// Take the action that is associated with the palette item.
ACTION: 'a'
};
/**
* Returns the palette that this event is fired from.
* @return {goog.ui.equation.Palette} The palette this event is
* fired from.
*/
goog.ui.equation.PaletteEvent.prototype.getPalette = function() {
return this.palette_;
};
/**
* The renderer for palette.
* @extends {goog.ui.PaletteRenderer}
* @constructor
*/
goog.ui.equation.PaletteRenderer = function() {
goog.ui.PaletteRenderer.call(this);
};
goog.inherits(goog.ui.equation.PaletteRenderer, goog.ui.PaletteRenderer);
goog.addSingletonGetter(goog.ui.equation.PaletteRenderer);
/** @override */
goog.ui.equation.PaletteRenderer.prototype.getCssClass =
function() {
return goog.ui.equation.Palette.CSS_CLASS;
};
/**
* Returns the CSS class name for the palette item.
* @return {string} The CSS class name of the palette item.
*/
goog.ui.equation.PaletteRenderer.prototype.getItemCssClass = function() {
return this.getCssClass() + '-item';
};

View File

@@ -0,0 +1,203 @@
// Copyright 2009 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.equation.PaletteManager');
goog.require('goog.Timer');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.ui.equation.ArrowPalette');
goog.require('goog.ui.equation.ComparisonPalette');
goog.require('goog.ui.equation.GreekPalette');
goog.require('goog.ui.equation.MathPalette');
goog.require('goog.ui.equation.MenuPalette');
goog.require('goog.ui.equation.Palette');
goog.require('goog.ui.equation.SymbolPalette');
/**
* Constructs the palette manager that manages all the palettes in Equation
* Editor.
* @param {!goog.dom.DomHelper} domHelper The DOM helper to be used for
* document interaction.
* @constructor
* @extends {goog.events.EventTarget}
*/
goog.ui.equation.PaletteManager = function(domHelper) {
goog.events.EventTarget.call(this);
/** @private {!goog.dom.DomHelper} */
this.domHelper_ = domHelper;
/**
* The map of palette type and instance pair.
* @type {Object.<string, goog.ui.equation.Palette>}
* @private
*/
this.paletteMap_ = {};
/**
* The current active palette.
* @type {goog.ui.equation.Palette}
* @private
*/
this.activePalette_ = null;
/**
* The event handler for managing events.
* @type {goog.events.EventHandler}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
/**
* The timer used to add grace period when deactivate palettes.
* @type {goog.Timer}
* @private
*/
this.deactivationTimer_ = new goog.Timer(300);
this.eventHandler_.listen(this.deactivationTimer_, goog.Timer.TICK,
this.handleDeactivation_);
};
goog.inherits(goog.ui.equation.PaletteManager,
goog.events.EventTarget);
/**
* Clears the deactivation timer. This is used to prevent palette manager
* deactivation when mouse pointer is moved outside palettes and moved back
* quickly inside a grace period.
*/
goog.ui.equation.PaletteManager.prototype.stopDeactivation = function() {
this.deactivationTimer_.stop();
};
/**
* Returns the palette instance of given type.
* @param {goog.ui.equation.Palette.Type} type The type of palette
* to get.
* @return {goog.ui.equation.Palette} The palette instance of given
* type. A new instance will be created. If the instance doesn't exist.
*/
goog.ui.equation.PaletteManager.prototype.getPalette =
function(type) {
var paletteMap = this.paletteMap_;
var palette = paletteMap[type];
if (!palette) {
switch (type) {
case goog.ui.equation.Palette.Type.MENU:
palette = new goog.ui.equation.MenuPalette(this);
break;
case goog.ui.equation.Palette.Type.GREEK:
palette = new goog.ui.equation.GreekPalette(this);
break;
case goog.ui.equation.Palette.Type.SYMBOL:
palette = new goog.ui.equation.SymbolPalette(this);
break;
case goog.ui.equation.Palette.Type.COMPARISON:
palette = new goog.ui.equation.ComparisonPalette(this);
break;
case goog.ui.equation.Palette.Type.MATH:
palette = new goog.ui.equation.MathPalette(this);
break;
case goog.ui.equation.Palette.Type.ARROW:
palette = new goog.ui.equation.ArrowPalette(this);
break;
default:
throw new Error('Invalid palette type!');
}
paletteMap[type] = palette;
}
return palette;
};
/**
* Sets the palette instance of given type to be the active one.
* @param {goog.ui.equation.Palette.Type} type The type of the
* palette to set active.
* @return {goog.ui.equation.Palette} The palette instance of given
* type. A new instance will be created, if the instance doesn't exist.
*/
goog.ui.equation.PaletteManager.prototype.setActive =
function(type) {
var palette = this.activePalette_;
if (palette) {
palette.setVisible(false);
}
palette = this.getPalette(type);
this.activePalette_ = palette;
palette.setVisible(true);
return palette;
};
/**
* Returns the active palette.
* @return {goog.ui.equation.Palette} The active palette.
*/
goog.ui.equation.PaletteManager.prototype.getActive = function() {
return this.activePalette_;
};
/**
* Starts the deactivation of open palette.
* This method has a slight delay before doing the real deactivation. This
* helps prevent sudden disappearing of palettes when user moves mouse outside
* them just briefly (and maybe accidentally). If you really want to deactivate
* the active palette, use {@link #deactivateNow()} instead.
*/
goog.ui.equation.PaletteManager.prototype.deactivate = function() {
this.deactivationTimer_.start();
};
/**
* Deactivate the open palette immediately.
*/
goog.ui.equation.PaletteManager.prototype.deactivateNow = function() {
this.handleDeactivation_();
};
/**
* Internal process of deactivation of the manager.
* @private
*/
goog.ui.equation.PaletteManager.prototype.handleDeactivation_ = function() {
this.setActive(goog.ui.equation.Palette.Type.MENU);
};
/**
* @return {!goog.dom.DomHelper} This object's DOM helper.
*/
goog.ui.equation.PaletteManager.prototype.getDomHelper = function() {
return this.domHelper_;
};
/** @override */
goog.ui.equation.PaletteManager.prototype.disposeInternal = function() {
goog.base(this, 'disposeInternal');
this.activePalette_ = null;
this.paletteMap_ = null;
};

View File

@@ -0,0 +1,74 @@
// Copyright 2009 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 palette of symbols.
*
*/
goog.provide('goog.ui.equation.SymbolPalette');
goog.require('goog.math.Size');
goog.require('goog.ui.equation.Palette');
/**
* Constructs a new symbols palette.
* @param {goog.ui.equation.PaletteManager} paletteManager The
* manager of the palette.
* @extends {goog.ui.equation.Palette}
* @constructor
*/
goog.ui.equation.SymbolPalette = function(paletteManager) {
goog.ui.equation.Palette.call(this, paletteManager,
goog.ui.equation.Palette.Type.SYMBOL,
0, 50, 18, 18,
['\\times',
'\\div',
'\\cdot',
'\\pm',
'\\mp',
'\\ast',
'\\star',
'\\circ',
'\\bullet',
'\\oplus',
'\\ominus',
'\\oslash',
'\\otimes',
'\\odot',
'\\dagger',
'\\ddagger',
'\\vee',
'\\wedge',
'\\cap',
'\\cup',
'\\aleph',
'\\Re',
'\\Im',
'\\top',
'\\bot',
'\\infty',
'\\partial',
'\\forall',
'\\exists',
'\\neg',
'\\angle',
'\\triangle',
'\\diamond']);
this.setSize(new goog.math.Size(7, 5));
};
goog.inherits(goog.ui.equation.SymbolPalette, goog.ui.equation.Palette);

View File

@@ -0,0 +1,141 @@
// Copyright 2009 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.equation.TexEditor');
goog.require('goog.ui.Component');
goog.require('goog.ui.equation.ImageRenderer');
goog.require('goog.ui.equation.TexPane');
/**
* User interface for equation editor plugin.
* @constructor
* @param {Object} context The context that this Tex editor runs in.
* @param {string} helpUrl URL pointing to help documentation.
* @param {goog.dom.DomHelper=} opt_domHelper DomHelper to use.
* @extends {goog.ui.Component}
*/
goog.ui.equation.TexEditor = function(context, helpUrl, opt_domHelper) {
goog.ui.Component.call(this, opt_domHelper);
/**
* The context that this Tex editor runs in.
* @type {Object}
* @private
*/
this.context_ = context;
/**
* A URL pointing to help documentation.
* @type {string}
* @private
*/
this.helpUrl_ = helpUrl;
};
goog.inherits(goog.ui.equation.TexEditor, goog.ui.Component);
/**
* The TeX editor pane.
* @type {goog.ui.equation.TexPane}
* @private
*/
goog.ui.equation.TexEditor.prototype.texPane_ = null;
/** @override */
goog.ui.equation.TexEditor.prototype.createDom = function() {
goog.base(this, 'createDom');
this.createDom_();
};
/**
* Creates main editor contents.
* @private
*/
goog.ui.equation.TexEditor.prototype.createDom_ = function() {
var contentElement = this.getElement();
this.texPane_ = new goog.ui.equation.TexPane(this.context_,
this.helpUrl_, this.dom_);
this.addChild(this.texPane_);
this.texPane_.render(contentElement);
this.texPane_.setVisible(true);
};
/** @override */
goog.ui.equation.TexEditor.prototype.decorateInternal = function(element) {
this.setElementInternal(element);
this.createDom_();
};
/**
* Returns the encoded equation.
* @return {string} The encoded equation.
*/
goog.ui.equation.TexEditor.prototype.getEquation = function() {
return this.texPane_.getEquation();
};
/**
* Parse an equation and draw it.
* Clears any previous displayed equation.
* @param {string} equation The equation text to parse.
*/
goog.ui.equation.TexEditor.prototype.setEquation = function(equation) {
this.texPane_.setEquation(equation);
};
/**
* @return {string} The html code to embed in the document.
*/
goog.ui.equation.TexEditor.prototype.getHtml = function() {
return goog.ui.equation.ImageRenderer.getHtml(this.getEquation());
};
/**
* Checks whether the current equation is valid and can be used in a document.
* @return {boolean} Whether the equation valid.
*/
goog.ui.equation.TexEditor.prototype.isValid = function() {
return goog.ui.equation.ImageRenderer.isEquationTooLong(
this.getEquation());
};
/**
* Sets the visibility of the editor.
* @param {boolean} visible Whether the editor should be visible.
*/
goog.ui.equation.TexEditor.prototype.setVisible = function(visible) {
this.texPane_.setVisible(visible);
};
/** @override */
goog.ui.equation.TexEditor.prototype.disposeInternal = function() {
if (this.texPane_) {
this.texPane_.dispose();
}
this.context_ = null;
goog.base(this, 'disposeInternal');
};

View File

@@ -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.
goog.provide('goog.ui.equation.TexPane');
goog.require('goog.Timer');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.dom.selection');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.events.InputHandler');
goog.require('goog.style');
goog.require('goog.ui.equation.ChangeEvent');
goog.require('goog.ui.equation.EditorPane');
goog.require('goog.ui.equation.ImageRenderer');
goog.require('goog.ui.equation.Palette');
goog.require('goog.ui.equation.PaletteEvent');
/**
* User interface for TeX equation editor tab pane.
* @param {Object} context The context this Tex editor pane runs in.
* @param {string} helpUrl The help link URL.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @constructor
* @extends {goog.ui.equation.EditorPane}
*/
goog.ui.equation.TexPane = function(
context, helpUrl, opt_domHelper) {
goog.ui.equation.EditorPane.call(this, opt_domHelper);
this.setHelpUrl(helpUrl);
/**
* The palette manager instance.
* @type {goog.ui.equation.PaletteManager}
* @private
*/
this.paletteManager_ =
/** @type {goog.ui.equation.PaletteManager} */(
context.paletteManager);
};
goog.inherits(goog.ui.equation.TexPane,
goog.ui.equation.EditorPane);
/**
* The CSS class name for the preview container.
* @type {string}
*/
goog.ui.equation.TexPane.PREVIEW_CONTAINER_CSS_CLASS =
'ee-preview-container';
/**
* The CSS class name for section titles.
* @type {string}
*/
goog.ui.equation.TexPane.SECTION_TITLE_CSS_CLASS =
'ee-section-title';
/**
* The CSS class name for section titles that float left.
* @type {string}
*/
goog.ui.equation.TexPane.SECTION_TITLE_FLOAT_CSS_CLASS =
'ee-section-title-floating';
/**
* The CSS id name for the link to "Learn more".
* @type {string}
*/
goog.ui.equation.TexPane.SECTION_LEARN_MORE_CSS_ID =
'ee-section-learn-more';
/**
* The CSS class name for the Tex editor.
* @type {string}
*/
goog.ui.equation.TexPane.TEX_EDIT_CSS_CLASS = 'ee-tex';
/**
* The CSS class name for the preview container.
* @type {string}
*/
goog.ui.equation.TexPane.WARNING_CLASS =
'ee-warning';
/**
* The content div of the TeX editor.
* @type {Element}
* @private
*/
goog.ui.equation.TexPane.prototype.texEditorElement_ = null;
/**
* The container div for the server-generated image of the equation.
* @type {Element}
* @private
*/
goog.ui.equation.TexPane.prototype.previewContainer_;
/**
* An inner container used to layout all the elements in Tex Editor.
* @type {Element}
* @private
*/
goog.ui.equation.TexPane.prototype.innerContainer_;
/**
* The textarea for free form TeX.
* @type {Element}
* @private
*/
goog.ui.equation.TexPane.prototype.texEdit_;
/**
* The input handler for Tex editor.
* @type {goog.events.InputHandler}
* @private
*/
goog.ui.equation.TexPane.prototype.texInputHandler_;
/**
* The last text that was renderred as an image.
* @type {string}
* @private
*/
goog.ui.equation.TexPane.prototype.lastRenderredText_ = '';
/**
* A sequence number for text change events. Used to delay drawing
* until the user paused typing.
* @type {number}
* @private
*/
goog.ui.equation.TexPane.prototype.changeSequence_ = 0;
/** @override */
goog.ui.equation.TexPane.prototype.createDom = function() {
/** @desc Title for TeX editor tab in the equation editor dialog. */
var MSG_EE_TEX_EQUATION = goog.getMsg('TeX Equation');
/** @desc Title for equation preview image in the equation editor dialog. */
var MSG_EE_TEX_PREVIEW = goog.getMsg('Preview');
/** @desc Link text that leads to an info page about the equation dialog. */
var MSG_EE_LEARN_MORE = goog.getMsg('Learn more');
var domHelper = this.dom_;
var innerContainer;
var texEditorEl = domHelper.createDom(goog.dom.TagName.DIV,
{'style': 'display: none;'},
domHelper.createDom(goog.dom.TagName.SPAN,
{'class':
goog.ui.equation.TexPane.SECTION_TITLE_CSS_CLASS +
' ' +
goog.ui.equation.TexPane.SECTION_TITLE_FLOAT_CSS_CLASS},
MSG_EE_TEX_EQUATION),
this.getHelpUrl() ?
domHelper.createDom(goog.dom.TagName.A,
{'id':
goog.ui.equation.TexPane.SECTION_LEARN_MORE_CSS_ID,
'target': '_blank', 'href': this.getHelpUrl()},
MSG_EE_LEARN_MORE) : null,
domHelper.createDom(goog.dom.TagName.DIV,
{'style': 'clear: both;'}),
innerContainer = this.innerContainer_ =
domHelper.createDom(goog.dom.TagName.DIV,
{'style': 'position: relative'}));
// Create menu palette.
var menuPalette =
this.paletteManager_.setActive(
goog.ui.equation.Palette.Type.MENU);
// Render the menu palette.
menuPalette.render(innerContainer);
innerContainer.appendChild(domHelper.createDom(goog.dom.TagName.DIV,
{'style': 'clear:both'}));
var texEdit = this.texEdit_ = domHelper.createDom('textarea',
{'class': goog.ui.equation.TexPane.TEX_EDIT_CSS_CLASS,
'dir': 'ltr'});
innerContainer.appendChild(texEdit);
innerContainer.appendChild(
domHelper.createDom(goog.dom.TagName.DIV,
{'class':
goog.ui.equation.TexPane.SECTION_TITLE_CSS_CLASS},
MSG_EE_TEX_PREVIEW));
var previewContainer = this.previewContainer_ = domHelper.createDom(
goog.dom.TagName.DIV,
{'class':
goog.ui.equation.TexPane.PREVIEW_CONTAINER_CSS_CLASS});
innerContainer.appendChild(previewContainer);
this.setElementInternal(texEditorEl);
};
/** @override */
goog.ui.equation.TexPane.prototype.enterDocument = function() {
this.texInputHandler_ = new goog.events.InputHandler(this.texEdit_);
// Listen to changes in the edit box to redraw equation.
goog.events.listen(this.texInputHandler_,
goog.events.InputHandler.EventType.INPUT,
this.handleTexChange_, false, this);
// Add a keyup listener for Safari that does not support the INPUT event,
// and for users pasting with ctrl+v, which does not generate an INPUT event
// in some browsers.
this.getHandler().listen(
this.texEdit_, goog.events.EventType.KEYDOWN, this.handleTexChange_);
// Listen to the action event on the active palette.
this.getHandler().listen(this.paletteManager_,
goog.ui.equation.PaletteEvent.Type.ACTION,
this.handlePaletteAction_, false, this);
};
/** @override */
goog.ui.equation.TexPane.prototype.setVisible = function(visible) {
goog.base(this, 'setVisible', visible);
if (visible) {
goog.Timer.callOnce(this.focusTexEdit_, 0, this);
}
};
/**
* Sets the focus to the TeX edit box.
* @private
*/
goog.ui.equation.TexPane.prototype.focusTexEdit_ = function() {
this.texEdit_.focus();
goog.dom.selection.setCursorPosition(this.texEdit_,
this.texEdit_.value.length);
};
/**
* Handles input change within the TeX textarea.
* @private
*/
goog.ui.equation.TexPane.prototype.handleEquationChange_ = function() {
var text = this.getEquation();
if (text == this.lastRenderredText_) {
return; // No change, no need to re-draw
}
this.lastRenderredText_ = text;
var isEquationValid =
!goog.ui.equation.ImageRenderer.isEquationTooLong(text);
// Dispatch change so that dialog might update the state of its buttons.
this.dispatchEvent(
new goog.ui.equation.ChangeEvent(
isEquationValid));
var container = this.previewContainer_;
var dom = goog.dom.getDomHelper(container);
dom.removeChildren(container);
if (text) {
var childNode;
if (isEquationValid) {
// Show equation image.
var imgSrc = goog.ui.equation.ImageRenderer.getImageUrl(text);
childNode = dom.createDom(goog.dom.TagName.IMG, {'src': imgSrc});
} else {
// Show a warning message.
/**
* @desc A warning message shown when equation the user entered is too
* long to display.
*/
var MSG_EE_TEX_EQUATION_TOO_LONG =
goog.getMsg('Equation is too long');
childNode = dom.createDom(goog.dom.TagName.DIV,
{'class': goog.ui.equation.TexPane.WARNING_CLASS},
MSG_EE_TEX_EQUATION_TOO_LONG);
}
dom.appendChild(container, childNode);
}
};
/**
* Handles a change to the equation text.
* Queues a request to handle input change within the TeX textarea.
* Refreshing the image is done only after a short timeout, to combine
* fast typing events into one draw.
* @param {goog.events.Event} e The keyboard event.
* @private
*/
goog.ui.equation.TexPane.prototype.handleTexChange_ = function(e) {
this.changeSequence_++;
goog.Timer.callOnce(
goog.bind(this.handleTexChangeTimer_, this, this.changeSequence_),
500);
};
/**
* Handles a timer timeout on delayed text change redraw.
* @param {number} seq The change sequence number when the timer started.
* @private
*/
goog.ui.equation.TexPane.prototype.handleTexChangeTimer_ =
function(seq) {
// Draw only if this was the last change. If not, just wait for the last.
if (seq == this.changeSequence_) {
this.handleEquationChange_();
}
};
/**
* Handles an action generated by a palette click.
* @param {goog.ui.equation.PaletteEvent} e The event object.
* @private
*/
goog.ui.equation.TexPane.prototype.handlePaletteAction_ = function(e) {
var palette = e.getPalette();
var paletteManager = this.paletteManager_;
var activePalette = paletteManager.getActive();
var texEdit = this.texEdit_;
// This is a click on the menu palette.
if (palette.getType() == goog.ui.equation.Palette.Type.MENU) {
var idx = palette.getHighlightedIndex();
var action = (idx != -1) ? palette.getAction(idx) : null;
// Current palette is not menu. This means there's a palette popping up.
if (activePalette != palette && activePalette.getType() == action) {
// Deactivate the palette.
paletteManager.deactivateNow();
return;
}
// We are clicking on the menu palette and there's no sub palette opening.
// Then we just open the one corresponding to the item under the mouse.
if (action) {
var subPalette = this.paletteManager_.setActive(
/** @type {goog.ui.equation.Palette.Type} */ (action));
if (!subPalette.getElement()) {
subPalette.render(this.innerContainer_);
}
var el = subPalette.getElement();
goog.style.setPosition(el, 0, - el.clientHeight);
}
} else {
activePalette = this.paletteManager_.getActive();
var action = activePalette.getAction(activePalette.getHighlightedIndex());
// If the click is on white space in the palette, do nothing.
if (!action) {
return;
}
// Do actual insert async because IE8 does not move the selection
// position and inserts in the wrong place if called in flow.
// See bug 2066876
goog.Timer.callOnce(goog.bind(this.insert_, this, action + ' '), 0);
}
// Let the tex editor always catch the focus.
texEdit.focus();
};
/**
* Inserts text into the equation at the current cursor position.
* Moves the cursor to after the inserted text.
* @param {string} text Text to insert.
* @private
*/
goog.ui.equation.TexPane.prototype.insert_ = function(text) {
var texEdit = this.texEdit_;
var pos = goog.dom.selection.getStart(texEdit);
var equation = texEdit['value'];
equation = equation.substring(0, pos) + text + equation.substring(pos);
texEdit['value'] = equation;
goog.dom.selection.setCursorPosition(texEdit, pos + text.length);
this.handleEquationChange_();
};
/** @override */
goog.ui.equation.TexPane.prototype.getEquation = function() {
return this.texEdit_['value'];
};
/** @override */
goog.ui.equation.TexPane.prototype.setEquation =
function(equation) {
this.texEdit_['value'] = equation;
this.handleEquationChange_();
};
/** @override */
goog.ui.equation.TexPane.prototype.disposeInternal = function() {
this.texInputHandler_.dispose();
this.paletteManager_ = null;
goog.base(this, 'disposeInternal');
};

View File

@@ -0,0 +1,575 @@
// Copyright 2007 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 Menu where items can be filtered based on user keyboard input.
* If a filter is specified only the items matching it will be displayed.
*
* @author eae@google.com (Emil A Eklund)
* @see ../demos/filteredmenu.html
*/
goog.provide('goog.ui.FilteredMenu');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.events.InputHandler');
goog.require('goog.events.KeyCodes');
goog.require('goog.string');
goog.require('goog.style');
goog.require('goog.ui.Component');
goog.require('goog.ui.FilterObservingMenuItem');
goog.require('goog.ui.Menu');
goog.require('goog.userAgent');
/**
* Filtered menu class.
* @param {goog.ui.MenuRenderer=} opt_renderer Renderer used to render filtered
* menu; defaults to {@link goog.ui.MenuRenderer}.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @constructor
* @extends {goog.ui.Menu}
*/
goog.ui.FilteredMenu = function(opt_renderer, opt_domHelper) {
goog.ui.Menu.call(this, opt_domHelper, opt_renderer);
};
goog.inherits(goog.ui.FilteredMenu, goog.ui.Menu);
/**
* Events fired by component.
* @enum {string}
*/
goog.ui.FilteredMenu.EventType = {
/** Dispatched after the component filter criteria has been changed. */
FILTER_CHANGED: 'filterchange'
};
/**
* Filter input element.
* @type {Element|undefined}
* @private
*/
goog.ui.FilteredMenu.prototype.filterInput_;
/**
* The input handler that provides the input event.
* @type {goog.events.InputHandler|undefined}
* @private
*/
goog.ui.FilteredMenu.prototype.inputHandler_;
/**
* Maximum number of characters for filter input.
* @type {number}
* @private
*/
goog.ui.FilteredMenu.prototype.maxLength_ = 0;
/**
* Label displayed in the filter input when no text has been entered.
* @type {string}
* @private
*/
goog.ui.FilteredMenu.prototype.label_ = '';
/**
* Label element.
* @type {Element|undefined}
* @private
*/
goog.ui.FilteredMenu.prototype.labelEl_;
/**
* Whether multiple items can be entered comma separated.
* @type {boolean}
* @private
*/
goog.ui.FilteredMenu.prototype.allowMultiple_ = false;
/**
* List of items entered in the search box if multiple entries are allowed.
* @type {Array.<string>|undefined}
* @private
*/
goog.ui.FilteredMenu.prototype.enteredItems_;
/**
* Index of first item that should be affected by the filter. Menu items with
* a lower index will not be affected by the filter.
* @type {number}
* @private
*/
goog.ui.FilteredMenu.prototype.filterFromIndex_ = 0;
/**
* Filter applied to the menu.
* @type {string|undefined|null}
* @private
*/
goog.ui.FilteredMenu.prototype.filterStr_;
/**
* Map of child nodes that shouldn't be affected by filtering.
* @type {Object|undefined}
* @private
*/
goog.ui.FilteredMenu.prototype.persistentChildren_;
/** @override */
goog.ui.FilteredMenu.prototype.createDom = function() {
goog.ui.FilteredMenu.superClass_.createDom.call(this);
var dom = this.getDomHelper();
var el = dom.createDom('div',
goog.getCssName(this.getRenderer().getCssClass(), 'filter'),
this.labelEl_ = dom.createDom('div', null, this.label_),
this.filterInput_ = dom.createDom('input', {'type': 'text'}));
var element = this.getElement();
dom.appendChild(element, el);
this.contentElement_ = dom.createDom('div',
goog.getCssName(this.getRenderer().getCssClass(), 'content'));
dom.appendChild(element, this.contentElement_);
this.initFilterInput_();
};
/**
* Helper method that initializes the filter input element.
* @private
*/
goog.ui.FilteredMenu.prototype.initFilterInput_ = function() {
this.setFocusable(true);
this.setKeyEventTarget(this.filterInput_);
// Workaround for mozilla bug #236791.
if (goog.userAgent.GECKO) {
this.filterInput_.setAttribute('autocomplete', 'off');
}
if (this.maxLength_) {
this.filterInput_.maxLength = this.maxLength_;
}
};
/**
* Sets up listeners and prepares the filter functionality.
* @private
*/
goog.ui.FilteredMenu.prototype.setUpFilterListeners_ = function() {
if (!this.inputHandler_ && this.filterInput_) {
this.inputHandler_ = new goog.events.InputHandler(
/** @type {Element} */ (this.filterInput_));
goog.style.setUnselectable(this.filterInput_, false);
goog.events.listen(this.inputHandler_,
goog.events.InputHandler.EventType.INPUT,
this.handleFilterEvent, false, this);
goog.events.listen(this.filterInput_.parentNode,
goog.events.EventType.CLICK,
this.onFilterLabelClick_, false, this);
if (this.allowMultiple_) {
this.enteredItems_ = [];
}
}
};
/**
* Tears down listeners and resets the filter functionality.
* @private
*/
goog.ui.FilteredMenu.prototype.tearDownFilterListeners_ = function() {
if (this.inputHandler_) {
goog.events.unlisten(this.inputHandler_,
goog.events.InputHandler.EventType.INPUT,
this.handleFilterEvent, false, this);
goog.events.unlisten(this.filterInput_.parentNode,
goog.events.EventType.CLICK,
this.onFilterLabelClick_, false, this);
this.inputHandler_.dispose();
this.inputHandler_ = undefined;
this.enteredItems_ = undefined;
}
};
/** @override */
goog.ui.FilteredMenu.prototype.setVisible = function(show, opt_force, opt_e) {
var visibilityChanged = goog.ui.FilteredMenu.superClass_.setVisible.call(this,
show, opt_force, opt_e);
if (visibilityChanged && show && this.isInDocument()) {
this.setFilter('');
this.setUpFilterListeners_();
} else if (visibilityChanged && !show) {
this.tearDownFilterListeners_();
}
return visibilityChanged;
};
/** @override */
goog.ui.FilteredMenu.prototype.disposeInternal = function() {
this.tearDownFilterListeners_();
this.filterInput_ = undefined;
this.labelEl_ = undefined;
goog.ui.FilteredMenu.superClass_.disposeInternal.call(this);
};
/**
* Sets the filter label (the label displayed in the filter input element if no
* text has been entered).
* @param {?string} label Label text.
*/
goog.ui.FilteredMenu.prototype.setFilterLabel = function(label) {
this.label_ = label || '';
if (this.labelEl_) {
goog.dom.setTextContent(this.labelEl_, this.label_);
}
};
/**
* @return {string} The filter label.
*/
goog.ui.FilteredMenu.prototype.getFilterLabel = function() {
return this.label_;
};
/**
* Sets the filter string.
* @param {?string} str Filter string.
*/
goog.ui.FilteredMenu.prototype.setFilter = function(str) {
if (this.filterInput_) {
this.filterInput_.value = str;
this.filterItems_(str);
}
};
/**
* Returns the filter string.
* @return {string} Current filter or an an empty string.
*/
goog.ui.FilteredMenu.prototype.getFilter = function() {
return this.filterInput_ && goog.isString(this.filterInput_.value) ?
this.filterInput_.value : '';
};
/**
* Sets the index of first item that should be affected by the filter. Menu
* items with a lower index will not be affected by the filter.
* @param {number} index Index of first item that should be affected by filter.
*/
goog.ui.FilteredMenu.prototype.setFilterFromIndex = function(index) {
this.filterFromIndex_ = index;
};
/**
* Returns the index of first item that is affected by the filter.
* @return {number} Index of first item that is affected by filter.
*/
goog.ui.FilteredMenu.prototype.getFilterFromIndex = function() {
return this.filterFromIndex_;
};
/**
* Gets a list of items entered in the search box.
* @return {Array.<string>} The entered items.
*/
goog.ui.FilteredMenu.prototype.getEnteredItems = function() {
return this.enteredItems_ || [];
};
/**
* Sets whether multiple items can be entered comma separated.
* @param {boolean} b Whether multiple items can be entered.
*/
goog.ui.FilteredMenu.prototype.setAllowMultiple = function(b) {
this.allowMultiple_ = b;
};
/**
* @return {boolean} Whether multiple items can be entered comma separated.
*/
goog.ui.FilteredMenu.prototype.getAllowMultiple = function() {
return this.allowMultiple_;
};
/**
* Sets whether the specified child should be affected (shown/hidden) by the
* filter criteria.
* @param {goog.ui.Component} child Child to change.
* @param {boolean} persistent Whether the child should be persistent.
*/
goog.ui.FilteredMenu.prototype.setPersistentVisibility = function(child,
persistent) {
if (!this.persistentChildren_) {
this.persistentChildren_ = {};
}
this.persistentChildren_[child.getId()] = persistent;
};
/**
* Returns whether the specified child should be affected (shown/hidden) by the
* filter criteria.
* @param {goog.ui.Component} child Menu item to check.
* @return {boolean} Whether the menu item is persistent.
*/
goog.ui.FilteredMenu.prototype.hasPersistentVisibility = function(child) {
return !!(this.persistentChildren_ &&
this.persistentChildren_[child.getId()]);
};
/**
* Handles filter input events.
* @param {goog.events.BrowserEvent} e The event object.
*/
goog.ui.FilteredMenu.prototype.handleFilterEvent = function(e) {
this.filterItems_(this.filterInput_.value);
// Highlight the first visible item unless there's already a highlighted item.
var highlighted = this.getHighlighted();
if (!highlighted || !highlighted.isVisible()) {
this.highlightFirst();
}
this.dispatchEvent(goog.ui.FilteredMenu.EventType.FILTER_CHANGED);
};
/**
* Shows/hides elements based on the supplied filter.
* @param {?string} str Filter string.
* @private
*/
goog.ui.FilteredMenu.prototype.filterItems_ = function(str) {
// Do nothing unless the filter string has changed.
if (this.filterStr_ == str) {
return;
}
if (this.labelEl_) {
this.labelEl_.style.visibility = str == '' ? 'visible' : 'hidden';
}
if (this.allowMultiple_ && this.enteredItems_) {
// Matches all non space characters after the last comma.
var lastWordRegExp = /^(.+),[ ]*([^,]*)$/;
var matches = str.match(lastWordRegExp);
// matches[1] is the string up to, but not including, the last comma and
// matches[2] the part after the last comma. If there are no non-space
// characters after the last comma matches[2] is undefined.
var items = matches && matches[1] ? matches[1].split(',') : [];
// If the number of comma separated items has changes recreate the
// entered items array and fire a change event.
if (str.substr(str.length - 1, 1) == ',' ||
items.length != this.enteredItems_.length) {
var lastItem = items[items.length - 1] || '';
// Auto complete text in input box based on the highlighted item.
if (this.getHighlighted() && lastItem != '') {
var caption = this.getHighlighted().getCaption();
if (caption.toLowerCase().indexOf(lastItem.toLowerCase()) == 0) {
items[items.length - 1] = caption;
this.filterInput_.value = items.join(',') + ',';
}
}
this.enteredItems_ = items;
this.dispatchEvent(goog.ui.Component.EventType.CHANGE);
this.setHighlightedIndex(-1);
}
if (matches) {
str = matches.length > 2 ? goog.string.trim(matches[2]) : '';
}
}
var matcher = new RegExp('(^|[- ,_/.:])' +
goog.string.regExpEscape(str), 'i');
for (var child, i = this.filterFromIndex_; child = this.getChildAt(i); i++) {
if (child instanceof goog.ui.FilterObservingMenuItem) {
child.callObserver(str);
} else if (!this.hasPersistentVisibility(child)) {
// Only show items matching the filter and highlight the part of the
// caption that matches.
var caption = child.getCaption();
if (caption) {
var matchArray = caption.match(matcher);
if (str == '' || matchArray) {
child.setVisible(true);
var pos = caption.indexOf(matchArray[0]);
// If position is non zero increase by one to skip the separator.
if (pos) {
pos++;
}
if (str == '') {
child.setContent(caption);
} else {
child.setContent(this.getDomHelper().createDom('span', null,
caption.substr(0, pos),
this.getDomHelper().createDom(
'b', null, caption.substr(pos, str.length)),
caption.substr(pos + str.length,
caption.length - str.length - pos)));
}
} else {
child.setVisible(false);
}
} else {
// Hide separators and other items without a caption if a filter string
// has been entered.
child.setVisible(str == '');
}
}
}
this.filterStr_ = str;
};
/**
* Handles the menu's behavior for a key event. The highlighted menu item will
* be given the opportunity to handle the key behavior.
* @param {goog.events.KeyEvent} e A browser event.
* @return {boolean} Whether the event was handled.
* @override
*/
goog.ui.FilteredMenu.prototype.handleKeyEventInternal = function(e) {
// Home, end and the arrow keys are normally used to change the selected menu
// item. Return false here to prevent the menu from preventing the default
// behavior for HOME, END and any key press with a modifier.
if (e.shiftKey || e.ctrlKey || e.altKey ||
e.keyCode == goog.events.KeyCodes.HOME ||
e.keyCode == goog.events.KeyCodes.END) {
return false;
}
if (e.keyCode == goog.events.KeyCodes.ESC) {
this.dispatchEvent(goog.ui.Component.EventType.BLUR);
return true;
}
return goog.ui.FilteredMenu.superClass_.handleKeyEventInternal.call(this, e);
};
/**
* Sets the highlighted index, unless the HIGHLIGHT event is intercepted and
* cancelled. -1 = no highlight. Also scrolls the menu item into view.
* @param {number} index Index of menu item to highlight.
* @override
*/
goog.ui.FilteredMenu.prototype.setHighlightedIndex = function(index) {
goog.ui.FilteredMenu.superClass_.setHighlightedIndex.call(this, index);
var contentEl = this.getContentElement();
var el = this.getHighlighted() ? this.getHighlighted().getElement() : null;
if (el && goog.dom.contains(contentEl, el)) {
var contentTop = goog.userAgent.IE && !goog.userAgent.isVersionOrHigher(8) ?
0 : contentEl.offsetTop;
// IE (tested on IE8) sometime does not scroll enough by about
// 1px. So we add 1px to the scroll amount. This still looks ok in
// other browser except for the most degenerate case (menu height <=
// item height).
// Scroll down if the highlighted item is below the bottom edge.
var diff = (el.offsetTop + el.offsetHeight - contentTop) -
(contentEl.clientHeight + contentEl.scrollTop) + 1;
contentEl.scrollTop += Math.max(diff, 0);
// Scroll up if the highlighted item is above the top edge.
diff = contentEl.scrollTop - (el.offsetTop - contentTop) + 1;
contentEl.scrollTop -= Math.max(diff, 0);
}
};
/**
* Handles clicks on the filter label. Focuses the input element.
* @param {goog.events.BrowserEvent} e A browser event.
* @private
*/
goog.ui.FilteredMenu.prototype.onFilterLabelClick_ = function(e) {
this.filterInput_.focus();
};
/** @override */
goog.ui.FilteredMenu.prototype.getContentElement = function() {
return this.contentElement_ || this.getElement();
};
/**
* Returns the filter input element.
* @return {Element} Input element.
*/
goog.ui.FilteredMenu.prototype.getFilterInputElement = function() {
return this.filterInput_ || null;
};
/** @override */
goog.ui.FilteredMenu.prototype.decorateInternal = function(element) {
this.setElementInternal(element);
// Decorate the menu content.
this.decorateContent(element);
// Locate internally managed elements.
var el = this.getDomHelper().getElementsByTagNameAndClass('div',
goog.getCssName(this.getRenderer().getCssClass(), 'filter'), element)[0];
this.labelEl_ = goog.dom.getFirstElementChild(el);
this.filterInput_ = goog.dom.getNextElementSibling(this.labelEl_);
this.contentElement_ = goog.dom.getNextElementSibling(el);
// Decorate additional menu items (like 'apply').
this.getRenderer().decorateChildren(this, el.parentNode,
this.contentElement_);
this.initFilterInput_();
};

View File

@@ -0,0 +1,97 @@
// Copyright 2007 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 Menu item observing the filter text in a
* {@link goog.ui.FilteredMenu}. The observer method is called when the filter
* text changes and allows the menu item to update its content and state based
* on the filter.
*
* @author eae@google.com (Emil A Eklund)
*/
goog.provide('goog.ui.FilterObservingMenuItem');
goog.require('goog.ui.FilterObservingMenuItemRenderer');
goog.require('goog.ui.MenuItem');
goog.require('goog.ui.registry');
/**
* Class representing a filter observing menu item.
*
* @param {goog.ui.ControlContent} content Text caption or DOM structure to
* display as the content of the item (use to add icons or styling to
* menus).
* @param {*=} opt_model Data/model associated with the menu item.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper used for
* document interactions.
* @param {goog.ui.MenuItemRenderer=} opt_renderer Optional renderer.
* @constructor
* @extends {goog.ui.MenuItem}
*/
goog.ui.FilterObservingMenuItem = function(content, opt_model, opt_domHelper,
opt_renderer) {
goog.ui.MenuItem.call(this, content, opt_model, opt_domHelper,
opt_renderer || new goog.ui.FilterObservingMenuItemRenderer());
};
goog.inherits(goog.ui.FilterObservingMenuItem, goog.ui.MenuItem);
/**
* Function called when the filter text changes.
* @type {Function} function(goog.ui.FilterObservingMenuItem, string)
* @private
*/
goog.ui.FilterObservingMenuItem.prototype.observer_ = null;
/** @override */
goog.ui.FilterObservingMenuItem.prototype.enterDocument = function() {
goog.ui.FilterObservingMenuItem.superClass_.enterDocument.call(this);
this.callObserver();
};
/**
* Sets the observer functions.
* @param {Function} f function(goog.ui.FilterObservingMenuItem, string).
*/
goog.ui.FilterObservingMenuItem.prototype.setObserver = function(f) {
this.observer_ = f;
this.callObserver();
};
/**
* Calls the observer function if one has been specified.
* @param {?string=} opt_str Filter string.
*/
goog.ui.FilterObservingMenuItem.prototype.callObserver = function(opt_str) {
if (this.observer_) {
this.observer_(this, opt_str || '');
}
};
// Register a decorator factory function for
// goog.ui.FilterObservingMenuItemRenderer.
goog.ui.registry.setDecoratorByClassName(
goog.ui.FilterObservingMenuItemRenderer.CSS_CLASS,
function() {
// FilterObservingMenuItem defaults to using
// FilterObservingMenuItemRenderer.
return new goog.ui.FilterObservingMenuItem(null);
});

View File

@@ -0,0 +1,62 @@
// 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 Menu item observing the filter text in a
* {@link goog.ui.FilteredMenu}. The observer method is called when the filter
* text changes and allows the menu item to update its content and state based
* on the filter.
*
* @author eae@google.com (Emil A Eklund)
*/
goog.provide('goog.ui.FilterObservingMenuItemRenderer');
goog.require('goog.ui.MenuItemRenderer');
/**
* Default renderer for {@link goog.ui.FilterObservingMenuItem}s. Each item has
* the following structure:
* <div class="goog-filterobsmenuitem"><div>...(content)...</div></div>
*
* @constructor
* @extends {goog.ui.MenuItemRenderer}
*/
goog.ui.FilterObservingMenuItemRenderer = function() {
goog.ui.MenuItemRenderer.call(this);
};
goog.inherits(goog.ui.FilterObservingMenuItemRenderer,
goog.ui.MenuItemRenderer);
goog.addSingletonGetter(goog.ui.FilterObservingMenuItemRenderer);
/**
* CSS class name the renderer applies to menu item elements.
* @type {string}
*/
goog.ui.FilterObservingMenuItemRenderer.CSS_CLASS =
goog.getCssName('goog-filterobsmenuitem');
/**
* Returns the CSS class to be applied to menu items rendered using this
* renderer.
* @return {string} Renderer-specific CSS class.
* @override
*/
goog.ui.FilterObservingMenuItemRenderer.prototype.getCssClass = function() {
return goog.ui.FilterObservingMenuItemRenderer.CSS_CLASS;
};

View File

@@ -0,0 +1,146 @@
// 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 Similiar functionality of {@link goog.ui.ButtonRenderer},
* but uses a <div> element instead of a <button> or <input> element.
*
*/
goog.provide('goog.ui.FlatButtonRenderer');
goog.require('goog.a11y.aria.Role');
goog.require('goog.dom.classes');
goog.require('goog.ui.Button');
goog.require('goog.ui.ButtonRenderer');
goog.require('goog.ui.INLINE_BLOCK_CLASSNAME');
goog.require('goog.ui.registry');
/**
* Flat renderer for {@link goog.ui.Button}s. Flat buttons can contain
* almost arbitrary HTML content, will flow like inline elements, but can be
* styled like block-level elements.
* @constructor
* @extends {goog.ui.ButtonRenderer}
*/
goog.ui.FlatButtonRenderer = function() {
goog.ui.ButtonRenderer.call(this);
};
goog.inherits(goog.ui.FlatButtonRenderer, goog.ui.ButtonRenderer);
goog.addSingletonGetter(goog.ui.FlatButtonRenderer);
/**
* Default CSS class to be applied to the root element of components rendered
* by this renderer.
* @type {string}
*/
goog.ui.FlatButtonRenderer.CSS_CLASS = goog.getCssName('goog-flat-button');
/**
* Returns the control's contents wrapped in a div element, with
* the renderer's own CSS class and additional state-specific classes applied
* to it, and the button's disabled attribute set or cleared as needed.
* Overrides {@link goog.ui.ButtonRenderer#createDom}.
* @param {goog.ui.Control} button Button to render.
* @return {Element} Root element for the button.
* @override
*/
goog.ui.FlatButtonRenderer.prototype.createDom = function(button) {
var classNames = this.getClassNames(button);
var attributes = {
'class': goog.ui.INLINE_BLOCK_CLASSNAME + ' ' + classNames.join(' ')
};
var element = button.getDomHelper().createDom(
'div', attributes, button.getContent());
this.setTooltip(element, button.getTooltip());
this.setAriaStates(button, element);
return element;
};
/**
* Returns the ARIA role to be applied to flat buttons.
* @return {goog.a11y.aria.Role|undefined} ARIA role.
* @override
*/
goog.ui.FlatButtonRenderer.prototype.getAriaRole = function() {
return goog.a11y.aria.Role.BUTTON;
};
/**
* Returns true if this renderer can decorate the element. Overrides
* {@link goog.ui.ButtonRenderer#canDecorate} by returning true if the
* element is a DIV, false otherwise.
* @param {Element} element Element to decorate.
* @return {boolean} Whether the renderer can decorate the element.
* @override
*/
goog.ui.FlatButtonRenderer.prototype.canDecorate = function(element) {
return element.tagName == 'DIV';
};
/**
* Takes an existing element and decorates it with the flat button control.
* Initializes the control's ID, content, tooltip, value, and state based
* on the ID of the element, its child nodes, and its CSS classes, respectively.
* Returns the element. Overrides {@link goog.ui.ButtonRenderer#decorate}.
* @param {goog.ui.Control} button Button instance to decorate the element.
* @param {Element} element Element to decorate.
* @return {Element} Decorated element.
* @override
*/
goog.ui.FlatButtonRenderer.prototype.decorate = function(button, element) {
goog.dom.classes.add(element, goog.ui.INLINE_BLOCK_CLASSNAME);
return goog.ui.FlatButtonRenderer.superClass_.decorate.call(this, button,
element);
};
/**
* Flat buttons can't use the value attribute since they are div elements.
* Overrides {@link goog.ui.ButtonRenderer#getValue} to prevent trying to
* access the element's value.
* @param {Element} element The button control's root element.
* @return {string} Value not valid for flat buttons.
* @override
*/
goog.ui.FlatButtonRenderer.prototype.getValue = function(element) {
// Flat buttons don't store their value in the DOM.
return '';
};
/**
* Returns the CSS class to be applied to the root element of components
* rendered using this renderer.
* @return {string} Renderer-specific CSS class.
* @override
*/
goog.ui.FlatButtonRenderer.prototype.getCssClass = function() {
return goog.ui.FlatButtonRenderer.CSS_CLASS;
};
// Register a decorator factory function for Flat Buttons.
goog.ui.registry.setDecoratorByClassName(goog.ui.FlatButtonRenderer.CSS_CLASS,
function() {
// Uses goog.ui.Button, but with FlatButtonRenderer.
return new goog.ui.Button(null, goog.ui.FlatButtonRenderer.getInstance());
});

View File

@@ -0,0 +1,238 @@
// 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 Similiar functionality of {@link goog.ui.MenuButtonRenderer},
* but inherits from {@link goog.ui.FlatButtonRenderer} instead of
* {@link goog.ui.CustomButtonRenderer}. This creates a simpler menu button
* that will look more like a traditional <select> menu.
*
*/
goog.provide('goog.ui.FlatMenuButtonRenderer');
goog.require('goog.a11y.aria');
goog.require('goog.a11y.aria.State');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.string');
goog.require('goog.style');
goog.require('goog.ui.Component');
goog.require('goog.ui.FlatButtonRenderer');
goog.require('goog.ui.INLINE_BLOCK_CLASSNAME');
goog.require('goog.ui.Menu');
goog.require('goog.ui.MenuButton');
goog.require('goog.ui.MenuRenderer');
goog.require('goog.ui.registry');
/**
* Flat Menu Button renderer. Creates a simpler version of
* {@link goog.ui.MenuButton} that doesn't look like a button and
* doesn't have rounded corners. Uses just a <div> and looks more like
* a traditional <select> element.
* @constructor
* @extends {goog.ui.FlatButtonRenderer}
*/
goog.ui.FlatMenuButtonRenderer = function() {
goog.ui.FlatButtonRenderer.call(this);
};
goog.inherits(goog.ui.FlatMenuButtonRenderer, goog.ui.FlatButtonRenderer);
goog.addSingletonGetter(goog.ui.FlatMenuButtonRenderer);
/**
* Default CSS class to be applied to the root element of components rendered
* by this renderer.
* @type {string}
*/
goog.ui.FlatMenuButtonRenderer.CSS_CLASS =
goog.getCssName('goog-flat-menu-button');
/**
* Returns the button's contents wrapped in the following DOM structure:
* <div class="goog-inline-block goog-flat-menu-button">
* <div class="goog-inline-block goog-flat-menu-button-caption">
* Contents...
* </div>
* <div class="goog-inline-block goog-flat-menu-button-dropdown">
* &nbsp;
* </div>
* </div>
* Overrides {@link goog.ui.FlatButtonRenderer#createDom}.
* @param {goog.ui.Control} control Button to render.
* @return {Element} Root element for the button.
* @override
*/
goog.ui.FlatMenuButtonRenderer.prototype.createDom = function(control) {
var button = /** @type {goog.ui.Button} */ (control);
var classNames = this.getClassNames(button);
var attributes = {
'class': goog.ui.INLINE_BLOCK_CLASSNAME + ' ' + classNames.join(' ')
};
var element = button.getDomHelper().createDom('div', attributes,
[this.createCaption(button.getContent(), button.getDomHelper()),
this.createDropdown(button.getDomHelper())]);
this.setTooltip(
element, /** @type {!string}*/ (button.getTooltip()));
return element;
};
/**
* Takes the button's root element and returns the parent element of the
* button's contents.
* @param {Element} element Root element of the button whose content
* element is to be returned.
* @return {Element} The button's content element (if any).
* @override
*/
goog.ui.FlatMenuButtonRenderer.prototype.getContentElement = function(element) {
return element && /** @type {Element} */ (element.firstChild);
};
/**
* Updates the flat menu button's ARIA (accessibility) state so that
* aria-expanded does not appear when the button is "opened."
* @param {Element} element Element whose ARIA state is to be updated.
* @param {goog.ui.Component.State} state Component state being enabled or
* disabled.
* @param {boolean} enable Whether the state is being enabled or disabled.
* @protected
* @override
*/
goog.ui.FlatMenuButtonRenderer.prototype.updateAriaState = function(
element, state, enable) {
// If button has OPENED state, do not assign an ARIA state. Usually
// aria-expanded would be assigned, but aria-expanded is not a valid state
// for a menu button.
goog.asserts.assertObject(
element, 'The flat button menu DOM element cannot be null.');
goog.asserts.assert(goog.string.isEmpty(
goog.a11y.aria.getState(element, goog.a11y.aria.State.EXPANDED)),
'Menu buttons do not support the ARIA expanded attribute. ' +
'Please use ARIA disabled instead.');
if (state != goog.ui.Component.State.OPENED) {
goog.base(this, 'updateAriaState', element, state, enable);
}
};
/**
* Takes an element, decorates it with the menu button control, and returns
* the element. Overrides {@link goog.ui.CustomButtonRenderer#decorate} by
* looking for a child element that can be decorated by a menu, and if it
* finds one, decorates it and attaches it to the menu button.
* @param {goog.ui.Control} button Menu button to decorate the element.
* @param {Element} element Element to decorate.
* @return {Element} Decorated element.
* @override
*/
goog.ui.FlatMenuButtonRenderer.prototype.decorate = function(button, element) {
// TODO(user): MenuButtonRenderer uses the exact same code.
// Refactor this block to its own module where both can use it.
var menuElem = goog.dom.getElementsByTagNameAndClass(
'*', goog.ui.MenuRenderer.CSS_CLASS, element)[0];
if (menuElem) {
// Move the menu element directly under the body, but hide it first; see
// bug 1089244.
goog.style.setElementShown(menuElem, false);
button.getDomHelper().getDocument().body.appendChild(menuElem);
// Decorate the menu and attach it to the button.
var menu = new goog.ui.Menu();
menu.decorate(menuElem);
button.setMenu(menu);
}
// Add the caption if it's not already there.
var captionElem = goog.dom.getElementsByTagNameAndClass(
'*', goog.getCssName(this.getCssClass(), 'caption'), element)[0];
if (!captionElem) {
element.appendChild(
this.createCaption(element.childNodes, button.getDomHelper()));
}
// Add the dropdown icon if it's not already there.
var dropdownElem = goog.dom.getElementsByTagNameAndClass(
'*', goog.getCssName(this.getCssClass(), 'dropdown'), element)[0];
if (!dropdownElem) {
element.appendChild(this.createDropdown(button.getDomHelper()));
}
// Let the superclass do the rest.
return goog.ui.FlatMenuButtonRenderer.superClass_.decorate.call(this, button,
element);
};
/**
* Takes a text caption or existing DOM structure, and returns it wrapped in
* an appropriately-styled DIV. Creates the following DOM structure:
* <div class="goog-inline-block goog-flat-menu-button-caption">
* Contents...
* </div>
* @param {goog.ui.ControlContent} content Text caption or DOM structure to wrap
* in a box.
* @param {goog.dom.DomHelper} dom DOM helper, used for document interaction.
* @return {Element} Caption element.
*/
goog.ui.FlatMenuButtonRenderer.prototype.createCaption = function(content,
dom) {
return dom.createDom('div',
goog.ui.INLINE_BLOCK_CLASSNAME + ' ' +
goog.getCssName(this.getCssClass(), 'caption'), content);
};
/**
* Returns an appropriately-styled DIV containing a dropdown arrow element.
* Creates the following DOM structure:
* <div class="goog-inline-block goog-flat-menu-button-dropdown">
* &nbsp;
* </div>
* @param {goog.dom.DomHelper} dom DOM helper, used for document interaction.
* @return {Element} Dropdown element.
*/
goog.ui.FlatMenuButtonRenderer.prototype.createDropdown = function(dom) {
// 00A0 is &nbsp;
return dom.createDom('div',
goog.ui.INLINE_BLOCK_CLASSNAME + ' ' +
goog.getCssName(this.getCssClass(), 'dropdown'), '\u00A0');
};
/**
* Returns the CSS class to be applied to the root element of components
* rendered using this renderer.
* @return {string} Renderer-specific CSS class.
* @override
*/
goog.ui.FlatMenuButtonRenderer.prototype.getCssClass = function() {
return goog.ui.FlatMenuButtonRenderer.CSS_CLASS;
};
// Register a decorator factory function for Flat Menu Buttons.
goog.ui.registry.setDecoratorByClassName(
goog.ui.FlatMenuButtonRenderer.CSS_CLASS,
function() {
// Uses goog.ui.MenuButton, but with FlatMenuButtonRenderer.
return new goog.ui.MenuButton(null, null,
goog.ui.FlatMenuButtonRenderer.getInstance());
});

View File

@@ -0,0 +1,109 @@
// 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 Utility for making the browser submit a hidden form, which can
* be used to effect a POST from JavaScript.
*
*/
goog.provide('goog.ui.FormPost');
goog.require('goog.array');
goog.require('goog.dom.TagName');
goog.require('goog.string');
goog.require('goog.string.StringBuffer');
goog.require('goog.ui.Component');
/**
* Creates a formpost object.
* @constructor
* @extends {goog.ui.Component}
* @param {goog.dom.DomHelper=} opt_dom The DOM helper.
*/
goog.ui.FormPost = function(opt_dom) {
goog.ui.Component.call(this, opt_dom);
};
goog.inherits(goog.ui.FormPost, goog.ui.Component);
/** @override */
goog.ui.FormPost.prototype.createDom = function() {
this.setElementInternal(this.getDomHelper().createDom(goog.dom.TagName.FORM,
{'method': 'POST', 'style': 'display:none'}));
};
/**
* Constructs a POST request and directs the browser as if a form were
* submitted.
* @param {Object} parameters Object with parameter values. Values can be
* strings, numbers, or arrays of strings or numbers.
* @param {string=} opt_url The destination URL. If not specified, uses the
* current URL for window for the DOM specified in the constructor.
* @param {string=} opt_target An optional name of a window in which to open the
* URL. If not specified, uses the window for the DOM specified in the
* constructor.
*/
goog.ui.FormPost.prototype.post = function(parameters, opt_url, opt_target) {
var form = this.getElement();
if (!form) {
this.render();
form = this.getElement();
}
form.action = opt_url || '';
form.target = opt_target || '';
this.setParameters_(form, parameters);
form.submit();
};
/**
* Creates hidden inputs in a form to match parameters.
* @param {Element} form The form element.
* @param {Object} parameters Object with parameter values. Values can be
* strings, numbers, or arrays of strings or numbers.
* @private
*/
goog.ui.FormPost.prototype.setParameters_ = function(form, parameters) {
var name, value, sb = new goog.string.StringBuffer();
for (name in parameters) {
value = parameters[name];
if (goog.isArrayLike(value)) {
goog.array.forEach(value, goog.bind(this.appendInput_, this, sb, name));
} else {
this.appendInput_(sb, name, value);
}
}
form.innerHTML = sb.toString();
};
/**
* Appends a hidden <INPUT> tag to a string buffer.
* @param {goog.string.StringBuffer} out A string buffer.
* @param {string} name The name of the input.
* @param {string} value The value of the input.
* @private
*/
goog.ui.FormPost.prototype.appendInput_ = function(out, name, value) {
out.append(
'<input type="hidden" name="',
goog.string.htmlEscape(name),
'" value="',
goog.string.htmlEscape(value),
'">');
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,169 @@
// Copyright 2007 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 The color theme used by a gauge (goog.ui.Guage).
*/
goog.provide('goog.ui.GaugeTheme');
goog.require('goog.graphics.LinearGradient');
goog.require('goog.graphics.SolidFill');
goog.require('goog.graphics.Stroke');
/**
* A class for the default color theme for a Gauge.
* Users can extend this class to provide a custom color theme, and apply the
* custom color theme by calling {@link goog.ui.Gauge#setTheme}.
* @constructor
*/
goog.ui.GaugeTheme = function() {
};
/**
* Returns the stroke for the external border of the gauge.
* @return {goog.graphics.Stroke} The stroke to use.
*/
goog.ui.GaugeTheme.prototype.getExternalBorderStroke = function() {
return new goog.graphics.Stroke(1, '#333333');
};
/**
* Returns the fill for the external border of the gauge.
* @param {number} cx X coordinate of the center of the gauge.
* @param {number} cy Y coordinate of the center of the gauge.
* @param {number} r Radius of the gauge.
* @return {goog.graphics.Fill} The fill to use.
*/
goog.ui.GaugeTheme.prototype.getExternalBorderFill = function(cx, cy, r) {
return new goog.graphics.LinearGradient(cx + r, cy - r, cx - r, cy + r,
'#f7f7f7', '#cccccc');
};
/**
* Returns the stroke for the internal border of the gauge.
* @return {goog.graphics.Stroke} The stroke to use.
*/
goog.ui.GaugeTheme.prototype.getInternalBorderStroke = function() {
return new goog.graphics.Stroke(2, '#e0e0e0');
};
/**
* Returns the fill for the internal border of the gauge.
* @param {number} cx X coordinate of the center of the gauge.
* @param {number} cy Y coordinate of the center of the gauge.
* @param {number} r Radius of the gauge.
* @return {goog.graphics.Fill} The fill to use.
*/
goog.ui.GaugeTheme.prototype.getInternalBorderFill = function(cx, cy, r) {
return new goog.graphics.SolidFill('#f7f7f7');
};
/**
* Returns the stroke for the major ticks of the gauge.
* @return {goog.graphics.Stroke} The stroke to use.
*/
goog.ui.GaugeTheme.prototype.getMajorTickStroke = function() {
return new goog.graphics.Stroke(2, '#333333');
};
/**
* Returns the stroke for the minor ticks of the gauge.
* @return {goog.graphics.Stroke} The stroke to use.
*/
goog.ui.GaugeTheme.prototype.getMinorTickStroke = function() {
return new goog.graphics.Stroke(1, '#666666');
};
/**
* Returns the stroke for the hinge at the center of the gauge.
* @return {goog.graphics.Stroke} The stroke to use.
*/
goog.ui.GaugeTheme.prototype.getHingeStroke = function() {
return new goog.graphics.Stroke(1, '#666666');
};
/**
* Returns the fill for the hinge at the center of the gauge.
* @param {number} cx X coordinate of the center of the gauge.
* @param {number} cy Y coordinate of the center of the gauge.
* @param {number} r Radius of the hinge.
* @return {goog.graphics.Fill} The fill to use.
*/
goog.ui.GaugeTheme.prototype.getHingeFill = function(cx, cy, r) {
return new goog.graphics.LinearGradient(cx + r, cy - r, cx - r, cy + r,
'#4684ee', '#3776d6');
};
/**
* Returns the stroke for the gauge needle.
* @return {goog.graphics.Stroke} The stroke to use.
*/
goog.ui.GaugeTheme.prototype.getNeedleStroke = function() {
return new goog.graphics.Stroke(1, '#c63310');
};
/**
* Returns the fill for the hinge at the center of the gauge.
* @param {number} cx X coordinate of the center of the gauge.
* @param {number} cy Y coordinate of the center of the gauge.
* @param {number} r Radius of the gauge.
* @return {goog.graphics.Fill} The fill to use.
*/
goog.ui.GaugeTheme.prototype.getNeedleFill = function(cx, cy, r) {
// Make needle a bit transparent so that text underneeth is still visible.
return new goog.graphics.SolidFill('#dc3912', 0.7);
};
/**
* Returns the color for the gauge title.
* @return {string} The color to use.
*/
goog.ui.GaugeTheme.prototype.getTitleColor = function() {
return '#333333';
};
/**
* Returns the color for the gauge value.
* @return {string} The color to use.
*/
goog.ui.GaugeTheme.prototype.getValueColor = function() {
return 'black';
};
/**
* Returns the color for the labels (formatted values) of tick marks.
* @return {string} The color to use.
*/
goog.ui.GaugeTheme.prototype.getTickLabelColor = function() {
return '#333333';
};

View File

@@ -0,0 +1,456 @@
// 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 Show hovercards with a delay after the mouse moves over an
* element of a specified type and with a specific attribute.
*
* @see ../demos/hovercard.html
*/
goog.provide('goog.ui.HoverCard');
goog.provide('goog.ui.HoverCard.EventType');
goog.provide('goog.ui.HoverCard.TriggerEvent');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.ui.AdvancedTooltip');
goog.require('goog.ui.PopupBase');
goog.require('goog.ui.Tooltip');
/**
* Create a hover card object. Hover cards extend tooltips in that they don't
* have to be manually attached to each element that can cause them to display.
* Instead, you can create a function that gets called when the mouse goes over
* any element on your page, and returns whether or not the hovercard should be
* shown for that element.
*
* Alternatively, you can define a map of tag names to the attribute name each
* tag should have for that tag to trigger the hover card. See example below.
*
* Hovercards can also be triggered manually by calling
* {@code triggerForElement}, shown without a delay by calling
* {@code showForElement}, or triggered over other elements by calling
* {@code attach}. For the latter two cases, the application is responsible
* for calling {@code detach} when finished.
*
* HoverCard objects fire a TRIGGER event when the mouse moves over an element
* that can trigger a hovercard, and BEFORE_SHOW when the hovercard is
* about to be shown. Clients can respond to these events and can prevent the
* hovercard from being triggered or shown.
*
* @param {Function|Object} isAnchor Function that returns true if a given
* element should trigger the hovercard. Alternatively, it can be a map of
* tag names to the attribute that the tag should have in order to trigger
* the hovercard, e.g., {A: 'href'} for all links. Tag names must be all
* upper case; attribute names are case insensitive.
* @param {boolean=} opt_checkDescendants Use false for a performance gain if
* you are sure that none of your triggering elements have child elements.
* Default is true.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper to use for
* creating and rendering the hovercard element.
* @param {Document=} opt_triggeringDocument Optional document to use in place
* of the one included in the DomHelper for finding triggering elements.
* Defaults to the document included in the DomHelper.
* @constructor
* @extends {goog.ui.AdvancedTooltip}
*/
goog.ui.HoverCard = function(isAnchor, opt_checkDescendants, opt_domHelper,
opt_triggeringDocument) {
goog.ui.AdvancedTooltip.call(this, null, null, opt_domHelper);
if (goog.isFunction(isAnchor)) {
// Override default implementation of {@code isAnchor_}.
this.isAnchor_ = isAnchor;
} else {
/**
* Map of tag names to attribute names that will trigger a hovercard.
* @type {Object}
* @private
*/
this.anchors_ = isAnchor;
}
/**
* Whether anchors may have child elements. If true, then we need to check
* the parent chain of any mouse over event to see if any of those elements
* could be anchors. Default is true.
* @type {boolean}
* @private
*/
this.checkDescendants_ = opt_checkDescendants != false;
/**
* Array of anchor elements that should be detached when we are no longer
* associated with them.
* @type {!Array.<Element>}
* @private
*/
this.tempAttachedAnchors_ = [];
/**
* Document containing the triggering elements, to which we listen for
* mouseover events.
* @type {Document}
* @private
*/
this.document_ = opt_triggeringDocument || (opt_domHelper ?
opt_domHelper.getDocument() : goog.dom.getDocument());
goog.events.listen(this.document_, goog.events.EventType.MOUSEOVER,
this.handleTriggerMouseOver_, false, this);
};
goog.inherits(goog.ui.HoverCard, goog.ui.AdvancedTooltip);
/**
* Enum for event type fired by HoverCard.
* @enum {string}
*/
goog.ui.HoverCard.EventType = {
TRIGGER: 'trigger',
CANCEL_TRIGGER: 'canceltrigger',
BEFORE_SHOW: goog.ui.PopupBase.EventType.BEFORE_SHOW,
SHOW: goog.ui.PopupBase.EventType.SHOW,
BEFORE_HIDE: goog.ui.PopupBase.EventType.BEFORE_HIDE,
HIDE: goog.ui.PopupBase.EventType.HIDE
};
/** @override */
goog.ui.HoverCard.prototype.disposeInternal = function() {
goog.ui.HoverCard.superClass_.disposeInternal.call(this);
goog.events.unlisten(this.document_, goog.events.EventType.MOUSEOVER,
this.handleTriggerMouseOver_, false, this);
};
/**
* Anchor of hovercard currently being shown. This may be different from
* {@code anchor} property if a second hovercard is triggered, when
* {@code anchor} becomes the second hovercard while {@code currentAnchor_}
* is still the old (but currently displayed) anchor.
* @type {Element}
* @private
*/
goog.ui.HoverCard.prototype.currentAnchor_;
/**
* Maximum number of levels to search up the dom when checking descendants.
* @type {number}
* @private
*/
goog.ui.HoverCard.prototype.maxSearchSteps_;
/**
* This function can be overridden by passing a function as the first parameter
* to the constructor.
* @param {Node} node Node to test.
* @return {boolean} Whether or not hovercard should be shown.
* @private
*/
goog.ui.HoverCard.prototype.isAnchor_ = function(node) {
return node.tagName in this.anchors_ &&
!!node.getAttribute(this.anchors_[node.tagName]);
};
/**
* If the user mouses over an element with the correct tag and attribute, then
* trigger the hovercard for that element. If anchors could have children, then
* we also need to check the parent chain of the given element.
* @param {goog.events.Event} e Mouse over event.
* @private
*/
goog.ui.HoverCard.prototype.handleTriggerMouseOver_ = function(e) {
var target = /** @type {Element} */ (e.target);
// Target might be null when hovering over disabled input textboxes in IE.
if (!target) {
return;
}
if (this.isAnchor_(target)) {
this.setPosition(null);
this.triggerForElement(target);
} else if (this.checkDescendants_) {
var trigger = goog.dom.getAncestor(target,
goog.bind(this.isAnchor_, this),
false,
this.maxSearchSteps_);
if (trigger) {
this.setPosition(null);
this.triggerForElement(/** @type {Element} */ (trigger));
}
}
};
/**
* Triggers the hovercard to show after a delay.
* @param {Element} anchorElement Element that is triggering the hovercard.
* @param {goog.positioning.AbstractPosition=} opt_pos Position to display
* hovercard.
* @param {Object=} opt_data Data to pass to the onTrigger event.
*/
goog.ui.HoverCard.prototype.triggerForElement = function(anchorElement,
opt_pos, opt_data) {
if (anchorElement == this.currentAnchor_) {
// Element is already showing, just make sure it doesn't hide.
this.clearHideTimer();
return;
}
if (anchorElement == this.anchor) {
// Hovercard is pending, no need to retrigger.
return;
}
// If a previous hovercard was being triggered, cancel it.
this.maybeCancelTrigger_();
// Create a new event for this trigger
var triggerEvent = new goog.ui.HoverCard.TriggerEvent(
goog.ui.HoverCard.EventType.TRIGGER, this, anchorElement, opt_data);
if (!this.getElements().contains(anchorElement)) {
this.attach(anchorElement);
this.tempAttachedAnchors_.push(anchorElement);
}
this.anchor = anchorElement;
if (!this.onTrigger(triggerEvent)) {
this.onCancelTrigger();
return;
}
var pos = opt_pos || this.position_;
this.startShowTimer(anchorElement,
/** @type {goog.positioning.AbstractPosition} */ (pos));
};
/**
* Sets the current anchor element at the time that the hovercard is shown.
* @param {Element} anchor New current anchor element, or null if there is
* no current anchor.
* @private
*/
goog.ui.HoverCard.prototype.setCurrentAnchor_ = function(anchor) {
if (anchor != this.currentAnchor_) {
this.detachTempAnchor_(this.currentAnchor_);
}
this.currentAnchor_ = anchor;
};
/**
* If given anchor is in the list of temporarily attached anchors, then
* detach and remove from the list.
* @param {Element|undefined} anchor Anchor element that we may want to detach
* from.
* @private
*/
goog.ui.HoverCard.prototype.detachTempAnchor_ = function(anchor) {
var pos = goog.array.indexOf(this.tempAttachedAnchors_, anchor);
if (pos != -1) {
this.detach(anchor);
this.tempAttachedAnchors_.splice(pos, 1);
}
};
/**
* Called when an element triggers the hovercard. This will return false
* if an event handler sets preventDefault to true, which will prevent
* the hovercard from being shown.
* @param {!goog.ui.HoverCard.TriggerEvent} triggerEvent Event object to use
* for trigger event.
* @return {boolean} Whether hovercard should be shown or cancelled.
* @protected
*/
goog.ui.HoverCard.prototype.onTrigger = function(triggerEvent) {
return this.dispatchEvent(triggerEvent);
};
/**
* Abort pending hovercard showing, if any.
*/
goog.ui.HoverCard.prototype.cancelTrigger = function() {
this.clearShowTimer();
this.onCancelTrigger();
};
/**
* If hovercard is in the process of being triggered, then cancel it.
* @private
*/
goog.ui.HoverCard.prototype.maybeCancelTrigger_ = function() {
if (this.getState() == goog.ui.Tooltip.State.WAITING_TO_SHOW ||
this.getState() == goog.ui.Tooltip.State.UPDATING) {
this.cancelTrigger();
}
};
/**
* This method gets called when we detect that a trigger event will not lead
* to the hovercard being shown.
* @protected
*/
goog.ui.HoverCard.prototype.onCancelTrigger = function() {
var event = new goog.ui.HoverCard.TriggerEvent(
goog.ui.HoverCard.EventType.CANCEL_TRIGGER, this, this.anchor || null);
this.dispatchEvent(event);
this.detachTempAnchor_(this.anchor);
delete this.anchor;
};
/**
* Gets the DOM element that triggered the current hovercard. Note that in
* the TRIGGER or CANCEL_TRIGGER events, the current hovercard's anchor may not
* be the one that caused the event, so use the event's anchor property instead.
* @return {Element} Object that caused the currently displayed hovercard (or
* pending hovercard if none is displayed) to be triggered.
*/
goog.ui.HoverCard.prototype.getAnchorElement = function() {
// this.currentAnchor_ is only set if the hovercard is showing. If it isn't
// showing yet, then use this.anchor as the pending anchor.
return /** @type {Element} */ (this.currentAnchor_ || this.anchor);
};
/**
* Make sure we detach from temp anchor when we are done displaying hovercard.
* @protected
* @suppress {underscore}
* @override
*/
goog.ui.HoverCard.prototype.onHide_ = function() {
goog.ui.HoverCard.superClass_.onHide_.call(this);
this.setCurrentAnchor_(null);
};
/**
* This mouse over event is only received if the anchor is already attached.
* If it was attached manually, then it may need to be triggered.
* @param {goog.events.BrowserEvent} event Mouse over event.
* @override
*/
goog.ui.HoverCard.prototype.handleMouseOver = function(event) {
// If this is a child of a triggering element, find the triggering element.
var trigger = this.getAnchorFromElement(
/** @type {Element} */ (event.target));
// If we moused over an element different from the one currently being
// triggered (if any), then trigger this new element.
if (trigger && trigger != this.anchor) {
this.triggerForElement(trigger);
return;
}
goog.ui.HoverCard.superClass_.handleMouseOver.call(this, event);
};
/**
* If the mouse moves out of the trigger while we're being triggered, then
* cancel it.
* @param {goog.events.BrowserEvent} event Mouse out or blur event.
* @override
*/
goog.ui.HoverCard.prototype.handleMouseOutAndBlur = function(event) {
// Get ready to see if a trigger should be cancelled.
var anchor = this.anchor;
var state = this.getState();
goog.ui.HoverCard.superClass_.handleMouseOutAndBlur.call(this, event);
if (state != this.getState() &&
(state == goog.ui.Tooltip.State.WAITING_TO_SHOW ||
state == goog.ui.Tooltip.State.UPDATING)) {
// Tooltip's handleMouseOutAndBlur method sets anchor to null. Reset
// so that the cancel trigger event will have the right data, and so that
// it will be properly detached.
this.anchor = anchor;
this.onCancelTrigger(); // This will remove and detach the anchor.
}
};
/**
* Called by timer from mouse over handler. If this is called and the hovercard
* is not shown for whatever reason, then send a cancel trigger event.
* @param {Element} el Element to show tooltip for.
* @param {goog.positioning.AbstractPosition=} opt_pos Position to display popup
* at.
* @override
*/
goog.ui.HoverCard.prototype.maybeShow = function(el, opt_pos) {
goog.ui.HoverCard.superClass_.maybeShow.call(this, el, opt_pos);
if (!this.isVisible()) {
this.cancelTrigger();
} else {
this.setCurrentAnchor_(el);
}
};
/**
* Sets the max number of levels to search up the dom if checking descendants.
* @param {number} maxSearchSteps Maximum number of levels to search up the
* dom if checking descendants.
*/
goog.ui.HoverCard.prototype.setMaxSearchSteps = function(maxSearchSteps) {
if (!maxSearchSteps) {
this.checkDescendants_ = false;
} else if (this.checkDescendants_) {
this.maxSearchSteps_ = maxSearchSteps;
}
};
/**
* Create a trigger event for specified anchor and optional data.
* @param {goog.ui.HoverCard.EventType} type Event type.
* @param {goog.ui.HoverCard} target Hovercard that is triggering the event.
* @param {Element} anchor Element that triggered event.
* @param {Object=} opt_data Optional data to be available in the TRIGGER event.
* @constructor
* @extends {goog.events.Event}
*/
goog.ui.HoverCard.TriggerEvent = function(type, target, anchor, opt_data) {
goog.events.Event.call(this, type, target);
/**
* Element that triggered the hovercard event.
* @type {Element}
*/
this.anchor = anchor;
/**
* Optional data to be passed to the listener.
* @type {Object|undefined}
*/
this.data = opt_data;
};
goog.inherits(goog.ui.HoverCard.TriggerEvent, goog.events.Event);

View File

@@ -0,0 +1,298 @@
// 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 An HSVA (hue/saturation/value/alpha) color palette/picker
* implementation.
* Without the styles from the demo css file, only a hex color label and input
* field show up.
*
* @see ../demos/hsvapalette.html
*/
goog.provide('goog.ui.HsvaPalette');
goog.require('goog.array');
goog.require('goog.color.alpha');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.style');
goog.require('goog.ui.Component');
goog.require('goog.ui.HsvPalette');
/**
* Creates an HSVA palette. Allows a user to select the hue, saturation,
* value/brightness and alpha/opacity.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @param {string=} opt_color Optional initial color, without alpha (default is
* red).
* @param {number=} opt_alpha Optional initial alpha (default is 1).
* @param {string=} opt_class Optional base for creating classnames (default is
* 'goog-hsva-palette').
* @extends {goog.ui.HsvPalette}
* @constructor
*/
goog.ui.HsvaPalette = function(opt_domHelper, opt_color, opt_alpha, opt_class) {
goog.base(this, opt_domHelper, opt_color, opt_class);
/**
* Alpha transparency of the currently selected color, in [0, 1]. When
* undefined, the palette will behave as a non-transparent HSV palette,
* assuming full opacity.
* @type {number}
* @private
*/
this.alpha_ = goog.isDef(opt_alpha) ? opt_alpha : 1;
/**
* @override
*/
this.className = opt_class || goog.getCssName('goog-hsva-palette');
/**
* The document which is being listened to.
* type {HTMLDocument}
* @private
*/
this.document_ = opt_domHelper ? opt_domHelper.getDocument() :
goog.dom.getDomHelper().getDocument();
};
goog.inherits(goog.ui.HsvaPalette, goog.ui.HsvPalette);
/**
* DOM element representing the alpha background image.
* @type {Element}
* @private
*/
goog.ui.HsvaPalette.prototype.aImageEl_;
/**
* DOM element representing the alpha handle.
* @type {Element}
* @private
*/
goog.ui.HsvaPalette.prototype.aHandleEl_;
/**
* DOM element representing the swatch backdrop image.
* @type {Element}
* @private
*/
goog.ui.HsvaPalette.prototype.swatchBackdropEl_;
/** @override */
goog.ui.HsvaPalette.prototype.getAlpha = function() {
return this.alpha_;
};
/**
* Sets which color is selected and update the UI. The passed color should be
* in #rrggbb format. The alpha value will be set to 1.
* @param {number} alpha The selected alpha value, in [0, 1].
*/
goog.ui.HsvaPalette.prototype.setAlpha = function(alpha) {
this.setColorAlphaHelper_(this.color_, alpha);
};
/**
* Sets which color is selected and update the UI. The passed color should be
* in #rrggbb format. The alpha value will be set to 1.
* @param {string} color The selected color.
* @override
*/
goog.ui.HsvaPalette.prototype.setColor = function(color) {
this.setColorAlphaHelper_(color, 1);
};
/**
* Gets the color that is currently selected in this color picker, in #rrggbbaa
* format.
* @return {string} The string of the selected color with alpha.
*/
goog.ui.HsvaPalette.prototype.getColorRgbaHex = function() {
var alphaHex = Math.floor(this.alpha_ * 255).toString(16);
return this.color_ + (alphaHex.length == 1 ? '0' + alphaHex : alphaHex);
};
/**
* Sets which color is selected and update the UI. The passed color should be
* in #rrggbbaa format. The alpha value will be set to 1.
* @param {string} color The selected color with alpha.
*/
goog.ui.HsvaPalette.prototype.setColorRgbaHex = function(color) {
var parsed = goog.ui.HsvaPalette.parseColorRgbaHex_(color);
this.setColorAlphaHelper_(parsed[0], parsed[1]);
};
/**
* Sets which color and alpha value are selected and update the UI. The passed
* color should be in #rrggbb format.
* @param {string} color The selected color in #rrggbb format.
* @param {number} alpha The selected alpha value, in [0, 1].
* @private
*/
goog.ui.HsvaPalette.prototype.setColorAlphaHelper_ = function(color, alpha) {
var colorChange = this.color_ != color;
var alphaChange = this.alpha_ != alpha;
this.alpha_ = alpha;
this.color_ = color;
if (colorChange) {
// This is to prevent multiple event dispatches.
goog.ui.HsvaPalette.superClass_.setColor_.call(this, color);
}
if (colorChange || alphaChange) {
this.updateUi();
this.dispatchEvent(goog.ui.Component.EventType.ACTION);
}
};
/** @override */
goog.ui.HsvaPalette.prototype.createDom = function() {
goog.ui.HsvaPalette.superClass_.createDom.call(this);
var dom = this.getDomHelper();
this.aImageEl_ = dom.createDom(
goog.dom.TagName.DIV, goog.getCssName(this.className, 'a-image'));
this.aHandleEl_ = dom.createDom(
goog.dom.TagName.DIV, goog.getCssName(this.className, 'a-handle'));
this.swatchBackdropEl_ = dom.createDom(
goog.dom.TagName.DIV, goog.getCssName(this.className, 'swatch-backdrop'));
dom.appendChild(this.element_, this.aImageEl_);
dom.appendChild(this.element_, this.aHandleEl_);
dom.appendChild(this.element_, this.swatchBackdropEl_);
};
/** @override */
goog.ui.HsvaPalette.prototype.disposeInternal = function() {
goog.ui.HsvaPalette.superClass_.disposeInternal.call(this);
delete this.aImageEl_;
delete this.aHandleEl_;
delete this.swatchBackdropEl_;
};
/** @override */
goog.ui.HsvaPalette.prototype.updateUi = function() {
goog.base(this, 'updateUi');
if (this.isInDocument()) {
var a = this.alpha_ * 255;
var top = this.aImageEl_.offsetTop -
Math.floor(this.aHandleEl_.offsetHeight / 2) +
this.aImageEl_.offsetHeight * ((255 - a) / 255);
this.aHandleEl_.style.top = top + 'px';
this.aImageEl_.style.backgroundColor = this.color_;
goog.style.setOpacity(this.swatchElement, a / 255);
}
};
/** @override */
goog.ui.HsvaPalette.prototype.updateInput = function() {
if (!goog.array.equals([this.color_, this.alpha_],
goog.ui.HsvaPalette.parseUserInput_(this.inputElement.value))) {
this.inputElement.value = this.getColorRgbaHex();
}
};
/** @override */
goog.ui.HsvaPalette.prototype.handleMouseDown = function(e) {
goog.base(this, 'handleMouseDown', e);
if (e.target == this.aImageEl_ || e.target == this.aHandleEl_) {
// Setup value change listeners
var b = goog.style.getBounds(this.valueBackgroundImageElement);
this.handleMouseMoveA_(b, e);
this.mouseMoveListener_ = goog.events.listen(this.document_,
goog.events.EventType.MOUSEMOVE,
goog.bind(this.handleMouseMoveA_, this, b));
this.mouseUpListener_ = goog.events.listen(this.document_,
goog.events.EventType.MOUSEUP, this.handleMouseUp, false, this);
}
};
/**
* Handles mousemove events on the document once a drag operation on the alpha
* slider has started.
* @param {goog.math.Rect} b Boundaries of the value slider object at the start
* of the drag operation.
* @param {goog.events.Event} e Event object.
* @private
*/
goog.ui.HsvaPalette.prototype.handleMouseMoveA_ = function(b, e) {
e.preventDefault();
var vportPos = this.getDomHelper().getDocumentScroll();
var newA = (b.top + b.height - Math.min(
Math.max(vportPos.y + e.clientY, b.top),
b.top + b.height)) / b.height;
this.setAlpha(newA);
};
/** @override */
goog.ui.HsvaPalette.prototype.handleInput = function(e) {
var parsed = goog.ui.HsvaPalette.parseUserInput_(this.inputElement.value);
if (parsed) {
this.setColorAlphaHelper_(parsed[0], parsed[1]);
}
};
/**
* Parses an #rrggbb or #rrggbbaa color string.
* @param {string} value User-entered color value.
* @return {Array} A two element array [color, alpha], where color is #rrggbb
* and alpha is in [0, 1]. Null if the argument was invalid.
* @private
*/
goog.ui.HsvaPalette.parseUserInput_ = function(value) {
if (/^#[0-9a-f]{8}$/i.test(value)) {
return goog.ui.HsvaPalette.parseColorRgbaHex_(value);
} else if (/^#[0-9a-f]{6}$/i.test(value)) {
return [value, 1];
}
return null;
};
/**
* Parses a #rrggbbaa color string.
* @param {string} color The color and alpha in #rrggbbaa format.
* @return {Array} A two element array [color, alpha], where color is #rrggbb
* and alpha is in [0, 1].
* @private
*/
goog.ui.HsvaPalette.parseColorRgbaHex_ = function(color) {
var hex = goog.color.alpha.parse(color).hex;
return [
goog.color.alpha.extractHexColor(hex),
parseInt(goog.color.alpha.extractAlpha(hex), 16) / 255
];
};

View File

@@ -0,0 +1,505 @@
// 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 An HSV (hue/saturation/value) color palette/picker
* implementation. Inspired by examples like
* http://johndyer.name/lab/colorpicker/ and the author's initial work. This
* control allows for more control in picking colors than a simple swatch-based
* palette. Without the styles from the demo css file, only a hex color label
* and input field show up.
*
* @author arv@google.com (Erik Arvidsson)
* @author smcbride@google.com (Sean McBride)
* @author manucornet@google.com (Manu Cornet)
* @see ../demos/hsvpalette.html
*/
goog.provide('goog.ui.HsvPalette');
goog.require('goog.color');
goog.require('goog.dom.TagName');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.events.InputHandler');
goog.require('goog.style');
goog.require('goog.style.bidi');
goog.require('goog.ui.Component');
goog.require('goog.userAgent');
/**
* Creates an HSV palette. Allows a user to select the hue, saturation and
* value/brightness.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @param {string=} opt_color Optional initial color (default is red).
* @param {string=} opt_class Optional base for creating classnames (default is
* goog.getCssName('goog-hsv-palette')).
* @extends {goog.ui.Component}
* @constructor
*/
goog.ui.HsvPalette = function(opt_domHelper, opt_color, opt_class) {
goog.ui.Component.call(this, opt_domHelper);
this.setColor_(opt_color || '#f00');
/**
* The base class name for the component.
* @type {string}
* @protected
*/
this.className = opt_class || goog.getCssName('goog-hsv-palette');
/**
* The document which is being listened to.
* type {HTMLDocument}
* @private
*/
this.document_ = this.getDomHelper().getDocument();
};
goog.inherits(goog.ui.HsvPalette, goog.ui.Component);
// TODO(user): Make this inherit from goog.ui.Control and split this into
// a control and a renderer.
/**
* DOM element representing the hue/saturation background image.
* @type {Element}
* @private
*/
goog.ui.HsvPalette.prototype.hsImageEl_;
/**
* DOM element representing the hue/saturation handle.
* @type {Element}
* @private
*/
goog.ui.HsvPalette.prototype.hsHandleEl_;
/**
* DOM element representing the value background image.
* @type {Element}
* @protected
*/
goog.ui.HsvPalette.prototype.valueBackgroundImageElement;
/**
* DOM element representing the value handle.
* @type {Element}
* @private
*/
goog.ui.HsvPalette.prototype.vHandleEl_;
/**
* DOM element representing the current color swatch.
* @type {Element}
* @protected
*/
goog.ui.HsvPalette.prototype.swatchElement;
/**
* DOM element representing the hex color input text field.
* @type {Element}
* @protected
*/
goog.ui.HsvPalette.prototype.inputElement;
/**
* Input handler object for the hex value input field.
* @type {goog.events.InputHandler}
* @private
*/
goog.ui.HsvPalette.prototype.inputHandler_;
/**
* Listener key for the mousemove event (during a drag operation).
* @type {goog.events.Key}
* @private
*/
goog.ui.HsvPalette.prototype.mouseMoveListener_;
/**
* Listener key for the mouseup event (during a drag operation).
* @type {goog.events.Key}
* @private
*/
goog.ui.HsvPalette.prototype.mouseUpListener_;
/**
* Gets the color that is currently selected in this color picker.
* @return {string} The string of the selected color.
*/
goog.ui.HsvPalette.prototype.getColor = function() {
return this.color_;
};
/**
* Alpha transparency of the currently selected color, in [0, 1].
* For the HSV palette this always returns 1. The HSVA palette overrides
* this method.
* @return {number} The current alpha value.
*/
goog.ui.HsvPalette.prototype.getAlpha = function() {
return 1;
};
/**
* Updates the text entry field.
* @protected
*/
goog.ui.HsvPalette.prototype.updateInput = function() {
var parsed;
try {
parsed = goog.color.parse(this.inputElement.value).hex;
} catch (e) {
// ignore
}
if (this.color_ != parsed) {
this.inputElement.value = this.color_;
}
};
/**
* Sets which color is selected and update the UI.
* @param {string} color The selected color.
*/
goog.ui.HsvPalette.prototype.setColor = function(color) {
if (color != this.color_) {
this.setColor_(color);
this.updateUi();
this.dispatchEvent(goog.ui.Component.EventType.ACTION);
}
};
/**
* Sets which color is selected.
* @param {string} color The selected color.
* @private
*/
goog.ui.HsvPalette.prototype.setColor_ = function(color) {
var rgbHex = goog.color.parse(color).hex;
var rgbArray = goog.color.hexToRgb(rgbHex);
this.hsv_ = goog.color.rgbArrayToHsv(rgbArray);
// Hue is divided by 360 because the documentation for goog.color is currently
// incorrect.
// TODO(user): Fix this, see http://1324469 .
this.hsv_[0] = this.hsv_[0] / 360;
this.color_ = rgbHex;
};
/**
* Alters the hue, saturation, and/or value of the currently selected color and
* updates the UI.
* @param {?number=} opt_hue (optional) hue in [0, 1].
* @param {?number=} opt_saturation (optional) saturation in [0, 1].
* @param {?number=} opt_value (optional) value in [0, 255].
*/
goog.ui.HsvPalette.prototype.setHsv = function(opt_hue,
opt_saturation,
opt_value) {
if (opt_hue != null || opt_saturation != null || opt_value != null) {
this.setHsv_(opt_hue, opt_saturation, opt_value);
this.updateUi();
this.dispatchEvent(goog.ui.Component.EventType.ACTION);
}
};
/**
* Alters the hue, saturation, and/or value of the currently selected color.
* @param {?number=} opt_hue (optional) hue in [0, 1].
* @param {?number=} opt_saturation (optional) saturation in [0, 1].
* @param {?number=} opt_value (optional) value in [0, 255].
* @private
*/
goog.ui.HsvPalette.prototype.setHsv_ = function(opt_hue,
opt_saturation,
opt_value) {
this.hsv_[0] = (opt_hue != null) ? opt_hue : this.hsv_[0];
this.hsv_[1] = (opt_saturation != null) ? opt_saturation : this.hsv_[1];
this.hsv_[2] = (opt_value != null) ? opt_value : this.hsv_[2];
// Hue is multiplied by 360 because the documentation for goog.color is
// currently incorrect.
// TODO(user): Fix this, see http://1324469 .
this.color_ = goog.color.hsvArrayToHex([
this.hsv_[0] * 360,
this.hsv_[1],
this.hsv_[2]
]);
};
/**
* HsvPalettes cannot be used to decorate pre-existing html, since the
* structure they build is fairly complicated.
* @param {Element} element Element to decorate.
* @return {boolean} Returns always false.
* @override
*/
goog.ui.HsvPalette.prototype.canDecorate = function(element) {
return false;
};
/** @override */
goog.ui.HsvPalette.prototype.createDom = function() {
var dom = this.getDomHelper();
var noalpha = (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('7')) ?
' ' + goog.getCssName(this.className, 'noalpha') : '';
var backdrop = dom.createDom(goog.dom.TagName.DIV,
goog.getCssName(this.className, 'hs-backdrop'));
this.hsHandleEl_ = dom.createDom(goog.dom.TagName.DIV,
goog.getCssName(this.className, 'hs-handle'));
this.hsImageEl_ = dom.createDom(goog.dom.TagName.DIV,
goog.getCssName(this.className, 'hs-image'),
this.hsHandleEl_);
this.valueBackgroundImageElement = dom.createDom(
goog.dom.TagName.DIV,
goog.getCssName(this.className, 'v-image'));
this.vHandleEl_ = dom.createDom(
goog.dom.TagName.DIV,
goog.getCssName(this.className, 'v-handle'));
this.swatchElement = dom.createDom(goog.dom.TagName.DIV,
goog.getCssName(this.className, 'swatch'));
this.inputElement = dom.createDom('input', {
'class': goog.getCssName(this.className, 'input'),
'type': 'text', 'dir': 'ltr'
});
var labelElement = dom.createDom('label', null, this.inputElement);
var element = dom.createDom(goog.dom.TagName.DIV,
this.className + noalpha,
backdrop,
this.hsImageEl_,
this.valueBackgroundImageElement,
this.vHandleEl_,
this.swatchElement,
labelElement);
this.setElementInternal(element);
// TODO(arv): Set tabIndex
};
/**
* Renders the color picker inside the provided element. This will override the
* current content of the element.
* @override
*/
goog.ui.HsvPalette.prototype.enterDocument = function() {
goog.ui.HsvPalette.superClass_.enterDocument.call(this);
// TODO(user): Accessibility.
this.updateUi();
var handler = this.getHandler();
handler.listen(this.getElement(), goog.events.EventType.MOUSEDOWN,
this.handleMouseDown, false, this);
// Cannot create InputHandler in createDom because IE throws an exception
// on document.activeElement
if (!this.inputHandler_) {
this.inputHandler_ = new goog.events.InputHandler(this.inputElement);
}
handler.listen(this.inputHandler_,
goog.events.InputHandler.EventType.INPUT, this.handleInput, false, this);
};
/** @override */
goog.ui.HsvPalette.prototype.disposeInternal = function() {
goog.ui.HsvPalette.superClass_.disposeInternal.call(this);
delete this.hsImageEl_;
delete this.hsHandleEl_;
delete this.valueBackgroundImageElement;
delete this.vHandleEl_;
delete this.swatchElement;
delete this.inputElement;
if (this.inputHandler_) {
this.inputHandler_.dispose();
delete this.inputHandler_;
}
goog.events.unlistenByKey(this.mouseMoveListener_);
goog.events.unlistenByKey(this.mouseUpListener_);
};
/**
* Updates the position, opacity, and styles for the UI representation of the
* palette.
* @protected
*/
goog.ui.HsvPalette.prototype.updateUi = function() {
if (this.isInDocument()) {
var h = this.hsv_[0];
var s = this.hsv_[1];
var v = this.hsv_[2];
var left = this.hsImageEl_.offsetWidth * h;
// We don't use a flipped gradient image in RTL, so we need to flip the
// offset in RTL so that it still hovers over the correct color on the
// gradiant.
if (this.isRightToLeft()) {
left = this.hsImageEl_.offsetWidth - left;
}
// We also need to account for the handle size.
var handleOffset = Math.ceil(this.hsHandleEl_.offsetWidth / 2);
left -= handleOffset;
var top = this.hsImageEl_.offsetHeight * (1 - s);
// Account for the handle size.
top -= Math.ceil(this.hsHandleEl_.offsetHeight / 2);
goog.style.bidi.setPosition(this.hsHandleEl_, left, top,
this.isRightToLeft());
top = this.valueBackgroundImageElement.offsetTop -
Math.floor(this.vHandleEl_.offsetHeight / 2) +
this.valueBackgroundImageElement.offsetHeight * ((255 - v) / 255);
this.vHandleEl_.style.top = top + 'px';
goog.style.setOpacity(this.hsImageEl_, (v / 255));
goog.style.setStyle(this.valueBackgroundImageElement, 'background-color',
goog.color.hsvToHex(this.hsv_[0] * 360, this.hsv_[1], 255));
goog.style.setStyle(this.swatchElement, 'background-color', this.color_);
goog.style.setStyle(this.swatchElement, 'color',
(this.hsv_[2] > 255 / 2) ? '#000' : '#fff');
this.updateInput();
}
};
/**
* Handles mousedown events on palette UI elements.
* @param {goog.events.BrowserEvent} e Event object.
* @protected
*/
goog.ui.HsvPalette.prototype.handleMouseDown = function(e) {
if (e.target == this.valueBackgroundImageElement ||
e.target == this.vHandleEl_) {
// Setup value change listeners
var b = goog.style.getBounds(this.valueBackgroundImageElement);
this.handleMouseMoveV_(b, e);
this.mouseMoveListener_ = goog.events.listen(this.document_,
goog.events.EventType.MOUSEMOVE,
goog.bind(this.handleMouseMoveV_, this, b));
this.mouseUpListener_ = goog.events.listen(this.document_,
goog.events.EventType.MOUSEUP, this.handleMouseUp, false, this);
} else if (e.target == this.hsImageEl_ || e.target == this.hsHandleEl_) {
// Setup hue/saturation change listeners
var b = goog.style.getBounds(this.hsImageEl_);
this.handleMouseMoveHs_(b, e);
this.mouseMoveListener_ = goog.events.listen(this.document_,
goog.events.EventType.MOUSEMOVE,
goog.bind(this.handleMouseMoveHs_, this, b));
this.mouseUpListener_ = goog.events.listen(this.document_,
goog.events.EventType.MOUSEUP, this.handleMouseUp, false, this);
}
};
/**
* Handles mousemove events on the document once a drag operation on the value
* slider has started.
* @param {goog.math.Rect} b Boundaries of the value slider object at the start
* of the drag operation.
* @param {goog.events.BrowserEvent} e Event object.
* @private
*/
goog.ui.HsvPalette.prototype.handleMouseMoveV_ = function(b, e) {
e.preventDefault();
var vportPos = this.getDomHelper().getDocumentScroll();
var height = Math.min(
Math.max(vportPos.y + e.clientY, b.top),
b.top + b.height);
var newV = Math.round(
255 * (b.top + b.height - height) / b.height);
this.setHsv(null, null, newV);
};
/**
* Handles mousemove events on the document once a drag operation on the
* hue/saturation slider has started.
* @param {goog.math.Rect} b Boundaries of the value slider object at the start
* of the drag operation.
* @param {goog.events.BrowserEvent} e Event object.
* @private
*/
goog.ui.HsvPalette.prototype.handleMouseMoveHs_ = function(b, e) {
e.preventDefault();
var vportPos = this.getDomHelper().getDocumentScroll();
var newH = (Math.min(Math.max(vportPos.x + e.clientX, b.left),
b.left + b.width) - b.left) / b.width;
var newS = (-Math.min(Math.max(vportPos.y + e.clientY, b.top),
b.top + b.height) + b.top + b.height) / b.height;
this.setHsv(newH, newS, null);
};
/**
* Handles mouseup events on the document, which ends a drag operation.
* @param {goog.events.Event} e Event object.
* @protected
*/
goog.ui.HsvPalette.prototype.handleMouseUp = function(e) {
goog.events.unlistenByKey(this.mouseMoveListener_);
goog.events.unlistenByKey(this.mouseUpListener_);
};
/**
* Handles input events on the hex value input field.
* @param {goog.events.Event} e Event object.
* @protected
*/
goog.ui.HsvPalette.prototype.handleInput = function(e) {
if (/^#[0-9a-f]{6}$/i.test(this.inputElement.value)) {
this.setColor(this.inputElement.value);
}
};

Some files were not shown because too many files have changed in this diff Show More