Path: blob/trunk/third_party/closure/goog/editor/plugin.js
2868 views
// Copyright 2008 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.13// All Rights Reserved.1415/**16* @fileoverview Abstract API for TrogEdit plugins.17*18* @see ../demos/editor/editor.html19*/2021goog.provide('goog.editor.Plugin');2223// TODO(user): Remove the dependency on goog.editor.Command asap. Currently only24// needed for execCommand issues with links.25goog.require('goog.events.EventTarget');26goog.require('goog.functions');27goog.require('goog.log');28goog.require('goog.object');29goog.require('goog.reflect');30goog.require('goog.userAgent');31323334/**35* Abstract API for trogedit plugins.36* @constructor37* @extends {goog.events.EventTarget}38*/39goog.editor.Plugin = function() {40goog.events.EventTarget.call(this);4142/**43* Whether this plugin is enabled for the registered field object.44* @type {boolean}45* @private46*/47this.enabled_ = this.activeOnUneditableFields();4849/**50* The field object this plugin is attached to.51* @type {goog.editor.Field}52* @protected53* @deprecated Use goog.editor.Plugin.getFieldObject and54* goog.editor.Plugin.setFieldObject.55*/56this.fieldObject = null;5758/**59* Indicates if this plugin should be automatically disposed when the60* registered field is disposed. This should be changed to false for61* plugins used as multi-field plugins.62* @type {boolean}63* @private64*/65this.autoDispose_ = true;6667/**68* The logger for this plugin.69* @type {?goog.log.Logger}70* @protected71*/72this.logger = goog.log.getLogger('goog.editor.Plugin');7374};75goog.inherits(goog.editor.Plugin, goog.events.EventTarget);767778/**79* @return {goog.dom.DomHelper?} The dom helper object associated with the80* currently active field.81*/82goog.editor.Plugin.prototype.getFieldDomHelper = function() {83return this.getFieldObject() && this.getFieldObject().getEditableDomHelper();84};858687/**88* Sets the field object for use with this plugin.89* @return {goog.editor.Field} The editable field object.90* @protected91* @suppress {deprecated} Until fieldObject can be made private.92*/93goog.editor.Plugin.prototype.getFieldObject = function() {94return this.fieldObject;95};969798/**99* Sets the field object for use with this plugin.100* @param {goog.editor.Field} fieldObject The editable field object.101* @protected102* @suppress {deprecated} Until fieldObject can be made private.103*/104goog.editor.Plugin.prototype.setFieldObject = function(fieldObject) {105this.fieldObject = fieldObject;106};107108109/**110* Registers the field object for use with this plugin.111* @param {goog.editor.Field} fieldObject The editable field object.112*/113goog.editor.Plugin.prototype.registerFieldObject = function(fieldObject) {114this.setFieldObject(fieldObject);115};116117118/**119* Unregisters and disables this plugin for the current field object.120* @param {goog.editor.Field} fieldObj The field object. For single-field121* plugins, this parameter is ignored.122*/123goog.editor.Plugin.prototype.unregisterFieldObject = function(fieldObj) {124if (this.getFieldObject()) {125this.disable(this.getFieldObject());126this.setFieldObject(null);127}128};129130131/**132* Enables this plugin for the specified, registered field object. A field133* object should only be enabled when it is loaded.134* @param {goog.editor.Field} fieldObject The field object.135*/136goog.editor.Plugin.prototype.enable = function(fieldObject) {137if (this.getFieldObject() == fieldObject) {138this.enabled_ = true;139} else {140goog.log.error(141this.logger, 'Trying to enable an unregistered field with ' +142'this plugin.');143}144};145146147/**148* Disables this plugin for the specified, registered field object.149* @param {goog.editor.Field} fieldObject The field object.150*/151goog.editor.Plugin.prototype.disable = function(fieldObject) {152if (this.getFieldObject() == fieldObject) {153this.enabled_ = false;154} else {155goog.log.error(156this.logger, 'Trying to disable an unregistered field ' +157'with this plugin.');158}159};160161162/**163* Returns whether this plugin is enabled for the field object.164*165* @param {goog.editor.Field} fieldObject The field object.166* @return {boolean} Whether this plugin is enabled for the field object.167*/168goog.editor.Plugin.prototype.isEnabled = function(fieldObject) {169return this.getFieldObject() == fieldObject ? this.enabled_ : false;170};171172173/**174* Set if this plugin should automatically be disposed when the registered175* field is disposed.176* @param {boolean} autoDispose Whether to autoDispose.177*/178goog.editor.Plugin.prototype.setAutoDispose = function(autoDispose) {179this.autoDispose_ = autoDispose;180};181182183/**184* @return {boolean} Whether or not this plugin should automatically be disposed185* when it's registered field is disposed.186*/187goog.editor.Plugin.prototype.isAutoDispose = function() {188return this.autoDispose_;189};190191192/**193* @return {boolean} If true, field will not disable the command194* when the field becomes uneditable.195*/196goog.editor.Plugin.prototype.activeOnUneditableFields = goog.functions.FALSE;197198199/**200* @param {string} command The command to check.201* @return {boolean} If true, field will not dispatch change events202* for commands of this type. This is useful for "seamless" plugins like203* dialogs and lorem ipsum.204*/205goog.editor.Plugin.prototype.isSilentCommand = goog.functions.FALSE;206207208/** @override */209goog.editor.Plugin.prototype.disposeInternal = function() {210if (this.getFieldObject()) {211this.unregisterFieldObject(this.getFieldObject());212}213214goog.editor.Plugin.superClass_.disposeInternal.call(this);215};216217218/**219* @return {string} The ID unique to this plugin class. Note that different220* instances off the plugin share the same classId.221*/222goog.editor.Plugin.prototype.getTrogClassId;223224225/**226* An enum of operations that plugins may support.227* @enum {number}228*/229goog.editor.Plugin.Op = {230KEYDOWN: 1,231KEYPRESS: 2,232KEYUP: 3,233SELECTION: 4,234SHORTCUT: 5,235EXEC_COMMAND: 6,236QUERY_COMMAND: 7,237PREPARE_CONTENTS_HTML: 8,238CLEAN_CONTENTS_HTML: 10,239CLEAN_CONTENTS_DOM: 11240};241242243/**244* A map from plugin operations to the names of the methods that245* invoke those operations.246*/247goog.editor.Plugin.OPCODE =248goog.object.transpose(goog.reflect.object(goog.editor.Plugin, {249handleKeyDown: goog.editor.Plugin.Op.KEYDOWN,250handleKeyPress: goog.editor.Plugin.Op.KEYPRESS,251handleKeyUp: goog.editor.Plugin.Op.KEYUP,252handleSelectionChange: goog.editor.Plugin.Op.SELECTION,253handleKeyboardShortcut: goog.editor.Plugin.Op.SHORTCUT,254execCommand: goog.editor.Plugin.Op.EXEC_COMMAND,255queryCommandValue: goog.editor.Plugin.Op.QUERY_COMMAND,256prepareContentsHtml: goog.editor.Plugin.Op.PREPARE_CONTENTS_HTML,257cleanContentsHtml: goog.editor.Plugin.Op.CLEAN_CONTENTS_HTML,258cleanContentsDom: goog.editor.Plugin.Op.CLEAN_CONTENTS_DOM259}));260261262/**263* A set of op codes that run even on disabled plugins.264*/265goog.editor.Plugin.IRREPRESSIBLE_OPS = goog.object.createSet(266goog.editor.Plugin.Op.PREPARE_CONTENTS_HTML,267goog.editor.Plugin.Op.CLEAN_CONTENTS_HTML,268goog.editor.Plugin.Op.CLEAN_CONTENTS_DOM);269270271/**272* Handles keydown. It is run before handleKeyboardShortcut and if it returns273* true handleKeyboardShortcut will not be called.274* @param {!goog.events.BrowserEvent} e The browser event.275* @return {boolean} Whether the event was handled and thus should *not* be276* propagated to other plugins or handleKeyboardShortcut.277*/278goog.editor.Plugin.prototype.handleKeyDown;279280281/**282* Handles keypress. It is run before handleKeyboardShortcut and if it returns283* true handleKeyboardShortcut will not be called.284* @param {!goog.events.BrowserEvent} e The browser event.285* @return {boolean} Whether the event was handled and thus should *not* be286* propagated to other plugins or handleKeyboardShortcut.287*/288goog.editor.Plugin.prototype.handleKeyPress;289290291/**292* Handles keyup.293* @param {!goog.events.BrowserEvent} e The browser event.294* @return {boolean} Whether the event was handled and thus should *not* be295* propagated to other plugins.296*/297goog.editor.Plugin.prototype.handleKeyUp;298299300/**301* Handles selection change.302* @param {!goog.events.BrowserEvent=} opt_e The browser event.303* @param {!Node=} opt_target The node the selection changed to.304* @return {boolean} Whether the event was handled and thus should *not* be305* propagated to other plugins.306*/307goog.editor.Plugin.prototype.handleSelectionChange;308309310/**311* Handles keyboard shortcuts. Preferred to using handleKey* as it will use312* the proper event based on browser and will be more performant. If313* handleKeyPress/handleKeyDown returns true, this will not be called. If the314* plugin handles the shortcut, it is responsible for dispatching appropriate315* events (change, selection change at the time of this comment). If the plugin316* calls execCommand on the editable field, then execCommand already takes care317* of dispatching events.318* NOTE: For performance reasons this is only called when any key is pressed319* in conjunction with ctrl/meta keys OR when a small subset of keys (defined320* in goog.editor.Field.POTENTIAL_SHORTCUT_KEYCODES_) are pressed without321* ctrl/meta keys. We specifically don't invoke it when altKey is pressed since322* alt key is used in many i8n UIs to enter certain characters.323* @param {!goog.events.BrowserEvent} e The browser event.324* @param {string} key The key pressed.325* @param {boolean} isModifierPressed Whether the ctrl/meta key was pressed or326* not.327* @return {boolean} Whether the event was handled and thus should *not* be328* propagated to other plugins. We also call preventDefault on the event if329* the return value is true.330*/331goog.editor.Plugin.prototype.handleKeyboardShortcut;332333334/**335* Handles execCommand. This default implementation handles dispatching336* BEFORECHANGE, CHANGE, and SELECTIONCHANGE events, and calls337* execCommandInternal to perform the actual command. Plugins that want to338* do their own event dispatching should override execCommand, otherwise339* it is preferred to only override execCommandInternal.340*341* This version of execCommand will only work for single field plugins.342* Multi-field plugins must override execCommand.343*344* @param {string} command The command to execute.345* @param {...*} var_args Any additional parameters needed to346* execute the command.347* @return {*} The result of the execCommand, if any.348*/349goog.editor.Plugin.prototype.execCommand = function(command, var_args) {350// TODO(user): Replace all uses of isSilentCommand with plugins that just351// override this base execCommand method.352var silent = this.isSilentCommand(command);353if (!silent) {354// Stop listening to mutation events in Firefox while text formatting355// is happening. This prevents us from trying to size the field in the356// middle of an execCommand, catching the field in a strange intermediary357// state where both replacement nodes and original nodes are appended to358// the dom. Note that change events get turned back on by359// fieldObj.dispatchChange.360if (goog.userAgent.GECKO) {361this.getFieldObject().stopChangeEvents(true, true);362}363364this.getFieldObject().dispatchBeforeChange();365}366367try {368var result = this.execCommandInternal.apply(this, arguments);369} finally {370// If the above execCommandInternal call throws an exception, we still need371// to turn change events back on (see http://b/issue?id=1471355).372// NOTE: If if you add to or change the methods called in this finally373// block, please add them as expected calls to the unit test function374// testExecCommandException().375if (!silent) {376// dispatchChange includes a call to startChangeEvents, which unwinds the377// call to stopChangeEvents made before the try block.378this.getFieldObject().dispatchChange();379this.getFieldObject().dispatchSelectionChangeEvent();380}381}382383return result;384};385386387/**388* Handles execCommand. This default implementation does nothing, and is389* called by execCommand, which handles event dispatching. This method should390* be overriden by plugins that don't need to do their own event dispatching.391* If custom event dispatching is needed, execCommand shoul be overriden392* instead.393*394* @param {string} command The command to execute.395* @param {...*} var_args Any additional parameters needed to396* execute the command.397* @return {*} The result of the execCommand, if any.398* @protected399*/400goog.editor.Plugin.prototype.execCommandInternal;401402403/**404* Gets the state of this command if this plugin serves that command.405* @param {string} command The command to check.406* @return {*} The value of the command.407*/408goog.editor.Plugin.prototype.queryCommandValue;409410411/**412* Prepares the given HTML for editing. Strips out content that should not413* appear in an editor, and normalizes content as appropriate. The inverse414* of cleanContentsHtml.415*416* This op is invoked even on disabled plugins.417*418* @param {string} originalHtml The original HTML.419* @param {Object} styles A map of strings. If the plugin wants to add420* any styles to the field element, it should add them as key-value421* pairs to this object.422* @return {string} New HTML that's ok for editing.423*/424goog.editor.Plugin.prototype.prepareContentsHtml;425426427/**428* Cleans the contents of the node passed to it. The node contents are modified429* directly, and the modifications will subsequently be used, for operations430* such as saving the innerHTML of the editor etc. Since the plugins act on431* the DOM directly, this method can be very expensive.432*433* This op is invoked even on disabled plugins.434*435* @param {!Element} fieldCopy The copy of the editable field which436* needs to be cleaned up.437*/438goog.editor.Plugin.prototype.cleanContentsDom;439440441/**442* Cleans the html contents of Trogedit. Both cleanContentsDom and443* and cleanContentsHtml will be called on contents extracted from Trogedit.444* The inverse of prepareContentsHtml.445*446* This op is invoked even on disabled plugins.447*448* @param {string} originalHtml The trogedit HTML.449* @return {string} Cleaned-up HTML.450*/451goog.editor.Plugin.prototype.cleanContentsHtml;452453454/**455* Whether the string corresponds to a command this plugin handles.456* @param {string} command Command string to check.457* @return {boolean} Whether the plugin handles this type of command.458*/459goog.editor.Plugin.prototype.isSupportedCommand = function(command) {460return false;461};462463464/**465* Saves the field's scroll position. See b/7279077 for context.466* Currently only does anything in Edge, since all other browsers467* already seem to work correctly.468* @return {function()} A function to restore the current scroll position.469* @protected470*/471goog.editor.Plugin.prototype.saveScrollPosition = function() {472if (this.getFieldObject() && goog.userAgent.EDGE) {473var win = this.getFieldObject().getEditableDomHelper().getWindow();474return win.scrollTo.bind(win, win.scrollX, win.scrollY);475}476return function() {};477};478479480