Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keymaps feature #1594

Merged
merged 18 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"build:server:debug": "SILEX_DEBUG=true tsc -p tsconfig-server-debug.json",
"build:plugins:server": "tsc -p tsconfig-plugins-server.json",
"build:plugins:client": "tsc -p tsconfig-plugins-client.json",
"lint": "eslint 'src/ts/**/*.ts'",
"lint": "eslint \"src/ts/**/*.ts\"",
"lint:fix": "$npm_execpath run lint -- --fix",
"test": "node --experimental-vm-modules `node_modules jest`/jest/bin/jest.js",
"test:watch": "$npm_execpath test -- --watch"
Expand Down
6 changes: 5 additions & 1 deletion src/ts/client/grapesjs/PublicationUi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'
import { cmdPublicationLogin, cmdPublicationLogout, cmdPublicationStart, PublicationStatus, PublishableEditor } from './PublicationManager'
import { ConnectorData, ConnectorType, PublicationJobData, PublicationSettings } from '../../types'
import { connectorList } from '../api'
import { defaultKms } from './keymaps'
import { titleCase } from '../utils'

/**
* @fileoverview define the publication dialog
Expand Down Expand Up @@ -132,7 +134,7 @@ export class PublicationUi {
id: 'publish-button',
className: 'silex-button--size publish-button',
command: cmdPublish,
attributes: { title: 'Publish' },
attributes: { title: `Publish [${titleCase(defaultKms.kmOpenPublish.keys, '+')}]` },
label: '<span class="fa-solid fa-upload"></span><span class="silex-button--small">Publish</span>',
})
})
Expand All @@ -152,6 +154,8 @@ export class PublicationUi {
`, this.el)
if (this.isOpen) {
this.el.classList.remove('silex-dialog-hide')
const primaryBtn = this.el.querySelector('.silex-button--primary') as HTMLElement
if (primaryBtn) primaryBtn.focus()
} else {
this.el.classList.add('silex-dialog-hide')
}
Expand Down
49 changes: 36 additions & 13 deletions src/ts/client/grapesjs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ import { blocksPlugin } from './blocks'
import { semanticPlugin } from './semantic'
import { orderedList, richTextPlugin, unorderedList } from './rich-text'
import { internalLinksPlugin } from './internal-links'
import { defaultKms, keymapsPlugin } from './keymaps'
import publicationManagerPlugin, { PublicationManagerOptions } from './PublicationManager'
import ViewButtons from './view-buttons'
import { storagePlugin } from './storage'
import { API_PATH, API_WEBSITE_ASSETS_WRITE, API_WEBSITE_PATH } from '../../constants'
import { ClientConfig } from '../config'
import { titleCase } from '../utils'

const plugins = [
{name: './project-bar', value: projectBarPlugin}, // has to be before panels and dialogs
Expand All @@ -79,6 +81,7 @@ const plugins = [
{name: 'grapesjs-plugin-forms', value: formPlugin},
{name: 'grapesjs-custom-code', value: codePlugin},
{name: './internal-links', value: internalLinksPlugin},
{name: './keymaps', value: keymapsPlugin},
{name: '@silexlabs/grapesjs-ui-suggest-classes', value: uiSuggestClasses},
{name: '@silexlabs/grapesjs-filter-styles', value: filterStyles},
{name: './symbolDialogs', value: symbolDialogsPlugin},
Expand Down Expand Up @@ -111,6 +114,12 @@ const QUATERNARY_COLOR = '#A291FF'
const DARKER_PRIMARY_COLOR = '#363636'
const LIGHTER_PRIMARY_COLOR = '#575757'

// Commands
export const cmdToggleLayers = 'open-layers'
export const cmdToggleBlocks = 'open-blocks'
export const cmdToggleSymbols = 'open-symbols'
export const cmdToggleNotifications = 'open-notifications'
lexoyo marked this conversation as resolved.
Show resolved Hide resolved

// ////////////////////
// Config
// ////////////////////
Expand Down Expand Up @@ -195,13 +204,15 @@ export function getEditorConfig(config: ClientConfig): EditorConfig {
}, {
id: 'block-manager-btn',
className: 'block-manager-btn fa fa-fw fa-plus',
attributes: { title: 'Blocks', containerClassName: 'block-manager-container', },
command: 'open-blocks',
name: 'Blocks',
attributes: { title: `Blocks [${titleCase(defaultKms.kmBlocks.keys, '+')}]`, containerClassName: 'block-manager-container', },
command: cmdToggleBlocks,
}, {
id: 'symbols-btn',
className: 'symbols-btn fa-regular fa-gem',
attributes: { title: 'Symbols', containerClassName: 'symbols-list-container', },
command: 'open-symbols',
name: 'Symbols',
attributes: { title: `Symbols [${titleCase(defaultKms.kmSymbols.keys, '+')}]`, containerClassName: 'symbols-list-container', },
command: cmdToggleSymbols,
buttons: [
{
id: 'symbol-create-button',
Expand All @@ -213,7 +224,8 @@ export function getEditorConfig(config: ClientConfig): EditorConfig {
}, {
id: 'page-panel-btn',
className: 'page-panel-btn fa fa-fw fa-file',
attributes: { title: 'Pages', containerClassName: 'page-panel-container', },
name: 'Pages',
attributes: { title: `Pages [${titleCase(defaultKms.kmPages.keys, '+')}]`, containerClassName: 'page-panel-container', },
command: cmdTogglePages,
buttons: [{
className: 'gjs-pn-btn',
Expand All @@ -223,19 +235,22 @@ export function getEditorConfig(config: ClientConfig): EditorConfig {
}, {
id: 'layer-manager-btn',
className: 'layer-manager-btn fa-solid fa-layer-group',
attributes: { title: 'Layers', containerClassName: 'layer-manager-container', },
command: 'open-layers',
name: 'Layers',
attributes: { title: `Layers [${titleCase(defaultKms.kmLayers.keys, '+')}]`, containerClassName: 'layer-manager-container', },
command: cmdToggleLayers,
}, {
id: 'font-dialog-btn',
className: 'font-manager-btn fa-solid fa-font',
attributes: { title: 'Fonts' },
name: 'Fonts',
attributes: { title: `Fonts [${titleCase(defaultKms.kmOpenFonts.keys, '+')}]` },
command: () => {
editor.runCommand(cmdOpenFonts)
},
}, {
id: 'settings-dialog-btn',
className: 'page-panel-btn fa-solid fa-gears',
attributes: { title: 'Settings' },
name: 'Settings',
attributes: { title: `Settings [${titleCase(defaultKms.kmOpenSettings.keys, '+')}]` },
command: cmdOpenSettings,
}, {
id: 'spacer',
Expand All @@ -244,8 +259,9 @@ export function getEditorConfig(config: ClientConfig): EditorConfig {
}, {
id: 'notifications-btn',
className: 'notifications-btn fa-regular fa-bell',
attributes: { title: 'Notifications', containerClassName: 'notifications-container', },
command: 'open-notifications',
name: 'Notifications',
attributes: { title: `Notifications [${titleCase(defaultKms.kmNotifications.keys, '+')}]`, containerClassName: 'notifications-container', },
command: cmdToggleNotifications,
buttons: [{
className: 'gjs-pn-btn',
command: 'notifications:clear',
Expand Down Expand Up @@ -289,6 +305,13 @@ export function getEditorConfig(config: ClientConfig): EditorConfig {
[filterStyles]: {
appendBefore: '.gjs-sm-sectors',
},
[internalLinksPlugin.toString()]: {
SuperDelphi marked this conversation as resolved.
Show resolved Hide resolved
// FIXME: warn the user about links in error
onError: (errors) => console.warn('Links errors:', errors),
},
[keymapsPlugin.toString()]: {
disableKeymaps: false,
},
[codePlugin.toString()]: {
blockLabel: 'HTML',
blockCustomCode: {
Expand Down Expand Up @@ -384,8 +407,8 @@ export async function initEditor(config: EditorConfig) {

// Remove blocks and layers buttons from the properties
// This is because in Silex they are on the left
views.buttons.remove('open-blocks')
views.buttons.remove('open-layers')
views.buttons.remove(cmdToggleBlocks)
views.buttons.remove(cmdToggleLayers)
lexoyo marked this conversation as resolved.
Show resolved Hide resolved

// Remove useless buttons
editor.Panels.getPanel('options').buttons.remove('export-template')
Expand Down
155 changes: 155 additions & 0 deletions src/ts/client/grapesjs/keymaps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import {Editor, PluginOptions} from 'grapesjs'
import {cmdPublish} from './PublicationUi'
import {cmdOpenFonts} from '@silexlabs/grapesjs-fonts'
import {cmdToggleBlocks, cmdToggleLayers, cmdToggleNotifications, cmdToggleSymbols} from './index'
import {cmdTogglePages} from './page-panel'
import {cmdOpenSettings} from './settings'
import {isTextOrInputField, selectBody} from '../utils'
import {PublishableEditor} from './PublicationManager'

// Utility functions

function getPanelCommandIds (): string[] {
lexoyo marked this conversation as resolved.
Show resolved Hide resolved
return [
cmdToggleBlocks,
cmdToggleLayers,
cmdToggleNotifications,
cmdToggleSymbols,
cmdTogglePages,
cmdOpenSettings,
cmdOpenFonts
]
}

function toggleCommand (editor: Editor, name: string): void {
const cmd = editor.Commands

if (!cmd.isActive(name)) {
resetPanel(editor)
cmd.run(name)
} else {
cmd.stop(name)
}
}

function resetPanel(editor: Editor): void {
getPanelCommandIds().forEach(p => editor.Commands.stop(p))
}

/**
* Escapes the current context in this order : modal, Publish dialog, left panel.
* If none of these are open, it selects the body.
* @param editor The editor.
*/
function escapeContext(editor: Editor): void {
const publishDialog = (editor as PublishableEditor).PublicationManager.dialog

if (editor.Modal.isOpen()) {
editor.Modal.close()
} else if (publishDialog && publishDialog.isOpen) {
publishDialog.closeDialog()
} else if (getPanelCommandIds().some(cmd => editor.Commands.isActive(cmd))) {
resetPanel(editor)
} else {
selectBody(editor)
}
}

// Constants

export const cmdSelectBody = 'select-body'
export let prefixKey = 'shift'

export const defaultKms = {
kmOpenSettings: {
id: 'general:open-settings',
keys: 'alt+s',
handler: editor => toggleCommand(editor, cmdOpenSettings)
},
kmOpenPublish: {
id: 'general:open-publish',
keys: 'alt+p',
handler: editor => toggleCommand(editor, cmdPublish)
},
kmOpenFonts: {
id: 'general:open-fonts',
keys: 'alt+f',
handler: editor => toggleCommand(editor, cmdOpenFonts)
},
kmPreviewMode: {
id: 'general:preview-mode',
keys: 'tab',
handler: editor => toggleCommand(editor, 'preview'),
options: {prevent: true}
},
kmLayers: {
id: 'panels:layers',
keys: prefixKey + '+l',
handler: editor => toggleCommand(editor, cmdToggleLayers)
},
kmBlocks: {
id: 'panels:blocks',
keys: prefixKey + '+a',
handler: editor => toggleCommand(editor, cmdToggleBlocks)
},
kmNotifications: {
id: 'panels:notifications',
keys: prefixKey + '+n',
handler: editor => toggleCommand(editor, cmdToggleNotifications)
},
kmPages: {
id: 'panels:pages',
keys: prefixKey + '+p',
handler: editor => toggleCommand(editor, cmdTogglePages)
},
kmSymbols: {
id: 'panels:symbols',
keys: prefixKey + '+s',
handler: editor => toggleCommand(editor, cmdToggleSymbols)
},
kmClosePanel: {
id: 'panels:close-panel',
keys: 'escape',
handler: escapeContext
},
kmSelectBody: {
id: 'workflow:select-body',
keys: prefixKey + '+b',
handler: cmdSelectBody
}
}

// Main part

export function keymapsPlugin(editor: Editor, opts: PluginOptions): void {
// Commands
editor.Commands.add(cmdSelectBody, selectBody)

if (opts.disableKeymaps) return
if (opts.prefixKey) prefixKey = opts.prefixKey

const km = editor.Keymaps

// Default keymaps
for (const keymap in defaultKms) {
km.add(defaultKms[keymap].id, defaultKms[keymap].keys, defaultKms[keymap].handler, defaultKms[keymap].options)
}

// Handling the Escape keymap during text edition
document.addEventListener('keydown', event => {
if (event.key.toLowerCase() === defaultKms.kmClosePanel.keys) {
const target = event.target as HTMLElement | null
const rte = editor.getEditing()

if (rte) { // If in Rich Text edition...
// TODO: Close the Rich Text edition and un-focus the text field
SuperDelphi marked this conversation as resolved.
Show resolved Hide resolved
} else if (target) { // If target exists...
if (target.tagName === 'INPUT' && target.getAttribute('type') === 'submit') { // If it's a submit button...
escapeContext(editor)
} else if (isTextOrInputField(target)) { // If it's a text field...
target.blur()
}
}
}
})
}
10 changes: 6 additions & 4 deletions src/ts/client/grapesjs/project-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface PanelObject {
command: string | ((editor: Editor) => void)
text: string
className: string
name?: string
attributes: {
title?: string
containerClassName?: string
Expand All @@ -41,13 +42,13 @@ export const projectBarPlugin = (editor, opts) => {
// create the panels container for all panels in grapesjs
const containerPanel = editor.Panels.addPanel({
id: containerPanelId,
visible : false,
visible: false,
})
// create the project bar panel in grapesjs
editor.Panels.addPanel({
id: PROJECT_BAR_PANEL_ID,
buttons: opts.panels,
visible : true,
visible: true,
})
// add the panels to the container
opts.panels.map(panel => addButton(editor, panel))
Expand Down Expand Up @@ -79,10 +80,11 @@ export function addButton(editor: Editor, panel: PanelObject) {
if(panel.attributes.containerClassName) {
el.classList.add('project-bar__panel', panel.attributes.containerClassName, 'gjs-hidden')
// add header
if(panel.attributes.title) {
const title = panel.name ?? panel.attributes.title
if(title) {
render(html`
<header class="project-bar__panel-header">
<h3 class="project-bar__panel-header-title">${ panel.attributes.title }</h3>
<h3 class="project-bar__panel-header-title">${ title }</h3>
${ panel.buttons?.map(button => {
return html`
<div
Expand Down
25 changes: 23 additions & 2 deletions src/ts/client/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function onAll(editor: Editor, cbk: (c: Component) => void) {
* SHA256 hash a string
*/
export async function hashString(str: string): Promise<string> {
if (crypto.subtle != undefined) {
if (crypto.subtle != undefined) {
// Convert the string to an ArrayBuffer
const encoder = new TextEncoder()
const data = encoder.encode(str)
Expand All @@ -45,5 +45,26 @@ export async function hashString(str: string): Promise<string> {

return hashHex
}
else {return 'local'}
else {return 'local'}
}

/**
* Select the <body> element in the editor.
* @param editor The GrapesJS editor.
*/

export function selectBody(editor: Editor): void {
editor.select(editor.DomComponents.getWrapper())
}

export function isTextOrInputField(element: HTMLElement): boolean {
const isInput: boolean = element.tagName === 'INPUT' && element.getAttribute('type') !== 'submit'
const isOtherFormElement: boolean = ['TEXTAREA', 'OPTION', 'OPTGROUP', 'SELECT'].includes(element.tagName)

return isInput || isOtherFormElement
}

export function titleCase(str: string, sep: string = ' '): string {
const split = str.split(sep)
return split.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(sep)
}
Loading