'use strict'; /** * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ /** * Copyright 2009 Google Inc. * * 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. */ const browser = require('./vendors/browser'); const hooks = require('./pluginfw/hooks'); const padutils = require('./pad_utils').padutils; const padeditor = require('./pad_editor').padeditor; const padsavedrevs = require('./pad_savedrevs'); const _ = require('underscore'); require('./vendors/nice-select'); class ToolbarItem { constructor(element) { this.$el = element; } getCommand() { return this.$el.attr('data-key'); } getValue() { if (this.isSelect()) { return this.$el.find('select').val(); } } setValue(val) { if (this.isSelect()) { return this.$el.find('select').val(val); } } getType() { return this.$el.attr('data-type'); } isSelect() { return this.getType() === 'select'; } isButton() { return this.getType() === 'button'; } bind(callback) { if (this.isButton()) { this.$el.click((event) => { $(':focus').blur(); callback(this.getCommand(), this); event.preventDefault(); }); } else if (this.isSelect()) { this.$el.find('select').change(() => { callback(this.getCommand(), this); }); } } } const syncAnimation = (() => { const SYNCING = -100; const DONE = 100; let state = DONE; const fps = 25; const step = 1 / fps; const T_START = -0.5; const T_FADE = 1.0; const T_GONE = 1.5; const animator = padutils.makeAnimationScheduler(() => { if (state === SYNCING || state === DONE) { return false; } else if (state >= T_GONE) { state = DONE; $('#syncstatussyncing').css('display', 'none'); $('#syncstatusdone').css('display', 'none'); return false; } else if (state < 0) { state += step; if (state >= 0) { $('#syncstatussyncing').css('display', 'none'); $('#syncstatusdone').css('display', 'block').css('opacity', 1); } return true; } else { state += step; if (state >= T_FADE) { $('#syncstatusdone').css('opacity', (T_GONE - state) / (T_GONE - T_FADE)); } return true; } }, step * 1000); return { syncing: () => { state = SYNCING; $('#syncstatussyncing').css('display', 'block'); $('#syncstatusdone').css('display', 'none'); }, done: () => { state = T_START; animator.scheduleAnimation(); }, }; })(); exports.padeditbar = new class { constructor() { this._editbarPosition = 0; this.commands = {}; this.dropdowns = []; } init() { $('#editbar .editbarbutton').attr('unselectable', 'on'); // for IE this.enable(); $('#editbar [data-key]').each((i, elt) => { $(elt).unbind('click'); new ToolbarItem($(elt)).bind((command, item) => { this.triggerCommand(command, item); }); }); $('body:not(#editorcontainerbox)').on('keydown', (evt) => { this._bodyKeyEvent(evt); }); $('.show-more-icon-btn').click(() => { $('.toolbar').toggleClass('full-icons'); }); this.checkAllIconsAreDisplayedInToolbar(); $(window).resize(_.debounce(() => this.checkAllIconsAreDisplayedInToolbar(), 100)); this._registerDefaultCommands(); hooks.callAll('postToolbarInit', { toolbar: this, ace: padeditor.ace, }); /* * On safari, the dropdown in the toolbar gets hidden because of toolbar * overflow:hidden property. This is a bug from Safari: any children with * position:fixed (like the dropdown) should be displayed no matter * overflow:hidden on parent */ if (!browser.safari) { $('select').niceSelect(); } // When editor is scrolled, we add a class to style the editbar differently $('iframe[name="ace_outer"]').contents().scroll((ev) => { $('#editbar').toggleClass('editor-scrolled', $(ev.currentTarget).scrollTop() > 2); }); } isEnabled() { return true; } disable() { $('#editbar').addClass('disabledtoolbar').removeClass('enabledtoolbar'); } enable() { $('#editbar').addClass('enabledtoolbar').removeClass('disabledtoolbar'); } registerCommand(cmd, callback) { this.commands[cmd] = callback; return this; } registerDropdownCommand(cmd, dropdown) { dropdown = dropdown || cmd; this.dropdowns.push(dropdown); this.registerCommand(cmd, () => { this.toggleDropDown(dropdown); }); } registerAceCommand(cmd, callback) { this.registerCommand(cmd, (cmd, ace, item) => { ace.callWithAce((ace) => { callback(cmd, ace, item); }, cmd, true); }); } triggerCommand(cmd, item) { if (this.isEnabled() && this.commands[cmd]) { this.commands[cmd](cmd, padeditor.ace, item); } if (padeditor.ace) padeditor.ace.focus(); } // cb is deprecated (this function is synchronous so a callback is unnecessary). toggleDropDown(moduleName, cb = null) { let cbErr = null; try { // do nothing if users are sticked if (moduleName === 'users' && $('#users').hasClass('stickyUsers')) { return; } $('.nice-select').removeClass('open'); $('.toolbar-popup').removeClass('popup-show'); // hide all modules and remove highlighting of all buttons if (moduleName === 'none') { for (const thisModuleName of this.dropdowns) { // skip the userlist if (thisModuleName === 'users') continue; const module = $(`#${thisModuleName}`); // skip any "force reconnect" message const isAForceReconnectMessage = module.find('button#forcereconnect:visible').length > 0; if (isAForceReconnectMessage) continue; if (module.hasClass('popup-show')) { $(`li[data-key=${thisModuleName}] > a`).removeClass('selected'); module.removeClass('popup-show'); } } } else { // hide all modules that are not selected and remove highlighting // respectively add highlighting to the corresponding button for (const thisModuleName of this.dropdowns) { const module = $(`#${thisModuleName}`); if (module.hasClass('popup-show')) { $(`li[data-key=${thisModuleName}] > a`).removeClass('selected'); module.removeClass('popup-show'); } else if (thisModuleName === moduleName) { $(`li[data-key=${thisModuleName}] > a`).addClass('selected'); module.addClass('popup-show'); } } } } catch (err) { cbErr = err || new Error(err); } finally { if (cb) Promise.resolve().then(() => cb(cbErr)); } } setSyncStatus(status) { if (status === 'syncing') { syncAnimation.syncing(); } else if (status === 'done') { syncAnimation.done(); } } setEmbedLinks() { const padUrl = window.location.href.split('?')[0]; const params = '?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false'; const props = 'width="100%" height="600" frameborder="0"'; if ($('#readonlyinput').is(':checked')) { const urlParts = padUrl.split('/'); urlParts.pop(); const readonlyLink = `${urlParts.join('/')}/${clientVars.readOnlyId}`; $('#embedinput') .val(``); $('#linkinput').val(readonlyLink); } else { $('#embedinput') .val(``); $('#linkinput').val(padUrl); } } checkAllIconsAreDisplayedInToolbar() { // reset style $('.toolbar').removeClass('cropped'); $('body').removeClass('mobile-layout'); const menuLeft = $('.toolbar .menu_left')[0]; // this is approximate, we cannot measure it because on mobile // Layout it takes the full width on the bottom of the page const menuRightWidth = 280; if (menuLeft && menuLeft.scrollWidth > $('.toolbar').width() - menuRightWidth || $('.toolbar').width() < 1000) { $('body').addClass('mobile-layout'); } if (menuLeft && menuLeft.scrollWidth > $('.toolbar').width()) { $('.toolbar').addClass('cropped'); } } _bodyKeyEvent(evt) { // If the event is Alt F9 or Escape & we're already in the editbar menu // Send the users focus back to the pad if ((evt.keyCode === 120 && evt.altKey) || evt.keyCode === 27) { if ($(':focus').parents('.toolbar').length === 1) { // If we're in the editbar already.. // Close any dropdowns we have open.. this.toggleDropDown('none'); // Shift focus away from any drop downs $(':focus').blur(); // required to do not try to remove! // Check we're on a pad and not on the timeslider // Or some other window I haven't thought about! if (typeof pad === 'undefined') { // Timeslider probably.. $('#editorcontainerbox').focus(); // Focus back onto the pad } else { padeditor.ace.focus(); // Sends focus back to pad // The above focus doesn't always work in FF, you have to hit enter afterwards evt.preventDefault(); } } else { // Focus on the editbar :) const firstEditbarElement = parent.parent.$('#editbar button').first(); $(evt.currentTarget).blur(); firstEditbarElement.focus(); evt.preventDefault(); } } // Are we in the toolbar?? if ($(':focus').parents('.toolbar').length === 1) { // On arrow keys go to next/previous button item in editbar if (evt.keyCode !== 39 && evt.keyCode !== 37) return; // Get all the focusable items in the editbar const focusItems = $('#editbar').find('button, select'); // On left arrow move to next button in editbar if (evt.keyCode === 37) { // If a dropdown is visible or we're in an input don't move to the next button if ($('.popup').is(':visible') || evt.target.localName === 'input') return; this._editbarPosition--; // Allow focus to shift back to end of row and start of row if (this._editbarPosition === -1) this._editbarPosition = focusItems.length - 1; $(focusItems[this._editbarPosition]).focus(); } // On right arrow move to next button in editbar if (evt.keyCode === 39) { // If a dropdown is visible or we're in an input don't move to the next button if ($('.popup').is(':visible') || evt.target.localName === 'input') return; this._editbarPosition++; // Allow focus to shift back to end of row and start of row if (this._editbarPosition >= focusItems.length) this._editbarPosition = 0; $(focusItems[this._editbarPosition]).focus(); } } } _registerDefaultCommands() { this.registerDropdownCommand('showusers', 'users'); this.registerDropdownCommand('settings'); this.registerDropdownCommand('connectivity'); this.registerDropdownCommand('import_export'); this.registerDropdownCommand('embed'); this.registerCommand('settings', () => { this.toggleDropDown('settings'); $('#options-stickychat').focus(); }); this.registerCommand('import_export', () => { this.toggleDropDown('import_export'); // If Import file input exists then focus on it.. if ($('#importfileinput').length !== 0) { setTimeout(() => { $('#importfileinput').focus(); }, 100); } else { $('.exportlink').first().focus(); } }); this.registerCommand('showusers', () => { this.toggleDropDown('users'); $('#myusernameedit').focus(); }); this.registerCommand('embed', () => { this.setEmbedLinks(); this.toggleDropDown('embed'); $('#linkinput').focus().select(); }); this.registerCommand('savedRevision', () => { padsavedrevs.saveNow(); }); this.registerCommand('showTimeSlider', () => { document.location = `${document.location.pathname}/timeslider`; }); const aceAttributeCommand = (cmd, ace) => { ace.ace_toggleAttributeOnSelection(cmd); }; this.registerAceCommand('bold', aceAttributeCommand); this.registerAceCommand('italic', aceAttributeCommand); this.registerAceCommand('underline', aceAttributeCommand); this.registerAceCommand('strikethrough', aceAttributeCommand); this.registerAceCommand('undo', (cmd, ace) => { ace.ace_doUndoRedo(cmd); }); this.registerAceCommand('redo', (cmd, ace) => { ace.ace_doUndoRedo(cmd); }); this.registerAceCommand('insertunorderedlist', (cmd, ace) => { ace.ace_doInsertUnorderedList(); }); this.registerAceCommand('insertorderedlist', (cmd, ace) => { ace.ace_doInsertOrderedList(); }); this.registerAceCommand('indent', (cmd, ace) => { if (!ace.ace_doIndentOutdent(false)) { ace.ace_doInsertUnorderedList(); } }); this.registerAceCommand('outdent', (cmd, ace) => { ace.ace_doIndentOutdent(true); }); this.registerAceCommand('clearauthorship', (cmd, ace) => { // If we have the whole document selected IE control A has been hit const rep = ace.ace_getRep(); let doPrompt = false; const lastChar = rep.lines.atIndex(rep.lines.length() - 1).width - 1; const lastLineIndex = rep.lines.length() - 1; if (rep.selStart[0] === 0 && rep.selStart[1] === 0) { // nesting intentionally here to make things readable if (rep.selEnd[0] === lastLineIndex && rep.selEnd[1] === lastChar) { doPrompt = true; } } /* * NOTICE: This command isn't fired on Control Shift C. * I intentionally didn't create duplicate code because if you are hitting * Control Shift C we make the assumption you are a "power user" * and as such we assume you don't need the prompt to bug you each time! * This does make wonder if it's worth having a checkbox to avoid being * prompted again but that's probably overkill for this contribution. */ // if we don't have any text selected, we have a caret or we have already said to prompt if ((!(rep.selStart && rep.selEnd)) || ace.ace_isCaret() || doPrompt) { if (window.confirm(html10n.get('pad.editbar.clearcolors'))) { ace.ace_performDocumentApplyAttributesToCharRange(0, ace.ace_getRep().alltext.length, [ ['author', ''], ]); } } else { ace.ace_setAttributeOnSelection('author', ''); } }); this.registerCommand('timeslider_returnToPad', (cmd) => { if (document.referrer.length > 0 && document.referrer.substring(document.referrer.lastIndexOf('/') - 1, document.referrer.lastIndexOf('/')) === 'p') { document.location = document.referrer; } else { document.location = document.location.href .substring(0, document.location.href.lastIndexOf('/')); } }); } }();