/* global SettingsListener, Service */ 'use strict'; (function(exports) { var DEBUG = false; var _id = 0; var _globalStates = null; // Manipulate global states for app text selection dialog across all apps. All // states which are shared by apps should be put in this class. var AppTextSelectionDialogGlobalStates = function() { this._hasCutOrCopiedTimeoutId = null; this._isPrefOn = true; this.appTSDs = new Set(); this.bindOnObservePrefChanged = this.onObservePrefChanged.bind(this); SettingsListener.observe('copypaste.enabled', true, this.bindOnObservePrefChanged); }; // If a user cuts or copies something, a paste shortcut will pop up every time // when single tapping on an editable field within this timeout. AppTextSelectionDialogGlobalStates.prototype.CUT_OR_COPIED_TIMEOUT_MS = 15000; AppTextSelectionDialogGlobalStates.prototype.hasCutOrCopied = function() { return !!this._hasCutOrCopiedTimeoutId; }; AppTextSelectionDialogGlobalStates.prototype.launchHasCutOrCopiedTimer = function() { this._hasCutOrCopiedTimeoutId = window.setTimeout(() => { this._hasCutOrCopiedTimeoutId = null; }, this.CUT_OR_COPIED_TIMEOUT_MS); }; AppTextSelectionDialogGlobalStates.prototype.cancelHasCutOrCopiedTimer = function() { window.clearTimeout(this._hasCutOrCopiedTimeoutId); this._hasCutOrCopiedTimeoutId = null; }; AppTextSelectionDialogGlobalStates.prototype.resetAllStates = function() { this._hasCutOrCopiedTimeoutId = null; this._isPrefOn = true; this.appTSDs.clear(); }; AppTextSelectionDialogGlobalStates.prototype.onObservePrefChanged = function (value) { this._isPrefOn = value; this.appTSDs.forEach(appTSD => { this._isPrefOn ? appTSD.start() : appTSD.stop(); }); }; // Text selection dialog per AppWindow. var AppTextSelectionDialog = function (app) { this.app = app; this.containerElement = (app && app.element) ? app.element : document.getElementById('text-selection-dialog-root'); this.instanceID = _id++; this.event = null; this._enabled = false; this._injected = false; this._isCommandSendable = false; this._transitionState = 'closed'; this._transitionStateOnPressCaret = null; this.textualmenuDetail = null; this.globalStates = _globalStates = _globalStates || new AppTextSelectionDialogGlobalStates(); this.globalStates.appTSDs.add(this); }; AppTextSelectionDialog.prototype = Object.create(window.BaseUI.prototype); AppTextSelectionDialog.prototype.TEXTDIALOG_HEIGHT = 46; AppTextSelectionDialog.prototype.TEXTDIALOG_WIDTH = 48; // Distance between selected area and the bottom of menu when menu show on // the top of selected area. // By UI spec, 12px from the top of selected area to utility menu. AppTextSelectionDialog.prototype.DISTANCE_FROM_MENUBOTTOM_TO_SELECTEDAREA = 12; // Distance between selected area and the top of menu when menu show on // the bottom of selected area. // Caret height is 36px which is controlled by gecko (libpref/init/all.js). // Note that 36px includes 28px for caret and 8px for space underneath caret. // So 28px plus 12px which defined in UI spec, we get 40px from the bottom // of selected area to utility menu. AppTextSelectionDialog.prototype.DISTANCE_FROM_SELECTEDAREA_TO_MENUTOP = 40; // Minimum distance between bubble and boundary. AppTextSelectionDialog.prototype.DISTANCE_FROM_BOUNDARY = 5; AppTextSelectionDialog.prototype.ID_NAME = 'AppTextSelectionDialog'; AppTextSelectionDialog.prototype.ELEMENT_PREFIX = 'textselection-dialog-'; // Overwrite _unregisterEvents to remove this AppTextSelectionDialog // from global states when destroying this app. AppTextSelectionDialog.prototype._unregisterEvents = function tsd__unregisterEvents() { this.globalStates.appTSDs.delete(this); }; AppTextSelectionDialog.prototype.start = function tsd_start() { if (this._enabled) { return; } this._enabled = true; if (this.app && this.app.element) { this.app.element.addEventListener('mozbrowsercaretstatechanged', this); } else { window.addEventListener('mozChromeEvent', this); } }; AppTextSelectionDialog.prototype.stop = function tsd_stop() { if (!this._enabled) { return; } this._enabled = false; if (this.app && this.app.element) { this.app.element.removeEventListener('mozbrowsercaretstatechanged', this); } else { window.removeEventListener('mozChromeEvent', this); } }; AppTextSelectionDialog.prototype.debug = function tsd_debug(msg) { if (DEBUG || this._DEBUG) { console.log(this.ID_NAME + '(' + this.CLASS_NAME + this.instanceID + '): ' + msg); } }; AppTextSelectionDialog.prototype.handleEvent = function tsd_handleEvent(evt) { switch (evt.type) { case 'mozbrowsercaretstatechanged': evt.preventDefault(); evt.stopPropagation(); this._handleCaretStateChanged(evt.detail); break; case 'mozChromeEvent': switch (evt.detail.type) { case 'caretstatechanged': evt.preventDefault(); evt.stopPropagation(); this._handleCaretStateChanged(evt.detail.detail); break; } } }; AppTextSelectionDialog.prototype._handleCaretStateChanged = function tsd__handleCaretStateChanged(detail) { if (!detail) { return; } this.debug('on receive caret state change event'); this.debug(JSON.stringify(detail)); if (detail.reason === 'visibilitychange' && !detail.caretVisible) { this.hide(); return; } if (detail.reason === 'presscaret') { this._transitionStateOnPressCaret = this._transitionState; this.hide(); return; } if (!detail.selectionVisible) { this.hide(); return; } if (detail.collapsed) { this._onCollapsedMode(detail); } else { this._onSelectionMode(detail); } }; AppTextSelectionDialog.prototype._onCollapsedMode = function tsd__onCollapsedMode(detail) { var showDialog = false; switch (detail.reason) { case 'taponcaret': case 'longpressonemptycontent': // Always allow, do nothing here. showDialog = true; break; case 'updateposition': // Only allow when something had been cut or copied. if (this.globalStates.hasCutOrCopied()) { showDialog = true; } break; case 'releasecaret': // Show the dialog if it was shown when pressing the caret. if (this._transitionStateOnPressCaret === 'opened') { showDialog = true; } this._transitionStateOnPressCaret = null; break; default: // Not allow break; } if (showDialog) { detail.commands.canCut = false; detail.commands.canCopy = false; detail.commands.canSelectAll = false; this.show(detail); } else { this.hide(); } }; AppTextSelectionDialog.prototype._onSelectionMode = function tsd__onSelectionMode(detail) { // make sure cut command option is only shown on editable element detail.commands.canCut = detail.commands.canCut && detail.selectionEditable; this.show(detail); }; AppTextSelectionDialog.prototype._fetchElements = function tsd__fetchElements() { this.element = document.getElementById(this.CLASS_NAME + this.instanceID); this.elements = {}; var toCamelCase = function toCamelCase(str) { return str.replace(/\-(.)/g, function replacer(str, p1) { return p1.toUpperCase(); }); }; this.elementClasses = ['copy', 'cut', 'paste', 'selectall']; // Loop and add element with camel style name to Modal Dialog attribute. this.elementClasses.forEach(function createElementRef(name) { this.elements[toCamelCase(name)] = this.element.querySelector('.' + this.ELEMENT_PREFIX + name); }, this); }; AppTextSelectionDialog.prototype._registerEvents = function tsd__registerEvents() { var elements = this.elements; for (var ele in elements) { elements[ele].addEventListener('mousedown', this._elementEventHandler.bind(this)); // We should not send command to gecko if user move their finger out of // the original button. elements[ele].addEventListener('mouseout', this._elementEventHandler.bind(this)); elements[ele].addEventListener('click', this._elementEventHandler.bind(this)); } this.element.addEventListener('transitionend', this._elementEventHandler.bind(this)); }; AppTextSelectionDialog.prototype._elementEventHandler = function tsd__elementEventHandler(evt) { switch (evt.type) { case 'mousedown': this._isCommandSendable = true; evt.preventDefault(); break; case 'transitionend': if (this._transitionState === 'closing') { this._changeTransitionState('closed'); } break; case 'click': this[evt.target.dataset.action + 'Handler'] && this[evt.target.dataset.action + 'Handler'](evt); break; case 'mouseout': this._isCommandSendable = false; break; } }; AppTextSelectionDialog.prototype._changeTransitionState = function tsd__changeTransitionState(state) { if (!this.element) { return; } switch (state) { case 'opened': this.element.classList.add('active'); this.element.classList.add('visible'); break; case 'closing': this.element.classList.remove('visible'); break; case 'closed': this.element.classList.remove('active'); break; } this._transitionState = state; }; AppTextSelectionDialog.prototype._doCommand = function tsd_doCommand(evt, cmd, closeDialog) { if (!this._isCommandSendable) { return; } if (this.app && this.app.element) { if (this.textualmenuDetail) { this.textualmenuDetail.sendDoCommandMsg(cmd); } } else { var props = { detail: { type: 'copypaste-do-command', cmd: cmd } }; this.debug(JSON.stringify(props)); window.dispatchEvent( new CustomEvent('mozContentEvent', props)); } if (closeDialog) { this.close(); } evt.preventDefault(); evt.stopPropagation(); }; AppTextSelectionDialog.prototype.copyHandler = function tsd_copyHandler(evt) { this._doCommand(evt, 'copy', true); this.globalStates.launchHasCutOrCopiedTimer(); }; AppTextSelectionDialog.prototype.cutHandler = function tsd_cutHandler(evt) { this._doCommand(evt, 'cut', true); this.globalStates.launchHasCutOrCopiedTimer(); }; AppTextSelectionDialog.prototype.pasteHandler = function tsd_pasteHandler(evt) { this._doCommand(evt, 'paste', true); this.globalStates.cancelHasCutOrCopiedTimer(); }; AppTextSelectionDialog.prototype.selectallHandler = function tsd_selectallHandler(evt) { this._doCommand(evt, 'selectall', false); }; AppTextSelectionDialog.prototype.view = function tsd_view() { var id = this.CLASS_NAME + this.instanceID; var temp = `