/* jshint esversion: 6, asi: true, boss: true */
let isMobile = window.matchMedia('only screen and (max-width: 760px)').matches
function buildMenu (groups) {
// Build the menu container
let menu = $mm('<div>', { class: 'menu' })
// Build groups
for (let g of groups) {
if (g.type === 'dropdown') {
// Build the dropdown item
let dropdown = buildMenuItem(g, g.type)
menu.append(dropdown)
menu.append($mm('<hr>'))
// Build the submenu
let submenu = buildMenu(g.groups)
// Keep track of the close timer so we can delay hiding the dropdown
submenu.timeoutId = null
// Show the submenu on hover over the dropdown
dropdown.on('mouseenter', e => {
e.preventDefault()
// Don't close the dropdown
if (submenu.timeoutId)
clearTimeout(submenu.timeoutId)
setTimeout(() => {
// Close other submenus
for (let submenu of menu.findAll('.menu'))
submenu.remove()
// Position the menu and add it to the DOM
let rect = dropdown.getBoundingClientRect()
submenu.style.top = `${rect.top - 5}px`
submenu.style.right = 'auto'
submenu.style.bottom = 'auto'
submenu.style.left = `${rect.right}px`
dropdown.append(submenu)
// Try to keep the menu on screen
if (rect.right + submenu.offsetWidth > window.innerWidth) {
submenu.style.left = `${window.innerWidth - submenu.offsetWidth}px`
}
if (rect.top - 5 + submenu.offsetHeight > window.innerHeight) {
submenu.style.top = `${rect.bottom + 5 - submenu.offsetHeight}px`
}
})
}, isMobile ? 50 : 0)
// Start the close timer on leaving the dropdown
dropdown.on('mouseleave', e => {
submenu.timeoutId = setTimeout(() => submenu.remove(), 500)
})
} else {
let attrs = { class: `group ${g.type === 'icon' ? 'icon-bar' : 'list'}`}
menu.append(buildMenuGroup(g.items, g.type, attrs))
menu.append($mm('<hr>'))
}
}
menu.find('hr:last-child').remove()
return menu
}
function buildMenuGroup (items, type, attrs = {}) {
// Build the list
let list = $mm('<div>', attrs)
for (let i of items) {
// Build the list item based on type
let el = buildMenuItem (i, type)
// Enable the action if there is one
if ('action' in i) {
el.on('mousedown', (e, ...args) => {
e.preventDefault()
i.action(e, ...args)
})
}
list.append(el)
}
return list
}
function buildMenuItem (item, type) {
switch (type) {
case 'icon':
return $mm('<i>', {
class: `fa fa-${item.icon}`,
title: item.label
})
case 'dropdown':
return $mm('<div>', {
class: 'menu-item dropdown',
html: `<i class='drop-icon fa fa-${'icon' in item ? item.icon : 'fw'}'></i><div class='label'>${item.label}</div><i class="fa fa-angle-right"></i>`
})
default:
return $mm('<div>', {
class: 'menu-item',
html: `<i class='fa fa-${'icon' in item ? item.icon : 'fw'}'></i><div class='label'>${item.label}</div>`
})
}
}
function findShortcuts (groups) {
let shortcuts = []
for (let g of groups) {
if ('groups' in g) {
shortcuts = [...shortcuts ,...findShortcuts(g.groups)]
} else {
for (let i of g.items)
if ('shortcut' in i)
shortcuts.push(i)
}
}
return shortcuts
}
function editor (opts) {
// Make the editor div editable
this.attr('contenteditable', 'true')
let textEditor = this
// Add the custom menu and keyboard shortcuts
if (opts && 'menu' in opts && 'groups' in opts.menu) {
// Build the menu
let menu = buildMenu(opts.menu.groups)
if (isMobile) {
menu.addClass('mobile')
let rect = this.getBoundingClientRect()
let openBtn = $mm('<div>', {
class: 'open-menu',
style: {
top: `${rect.top}px`,
right: `${rect.left}px`
}
})
let openIcon = $mm('<i>', { class: 'fa fa-bars fa-2x' })
openBtn.append(openIcon)
$mm('body').append(openBtn)
let openBtnW = openBtn.offsetWidth,
openBtnH = openBtn.offsetHeight
openBtn.on('mousedown', e => {
e.preventDefault()
setTimeout(() => {
// Remove any lingering submenus
for (let submenu of menu.findAll('.menu'))
submenu.remove()
// Add the menu as invisible
menu.style.opacity = 0
menu.style.top = `${rect.top}px`
menu.style.right = `${rect.left}px`
menu.style.bottom = 'auto'
menu.style.left = 'auto'
$mm('body').append(menu)
// Hide the icon and resize the button
openIcon.style.opacity = 0
openBtn.style.width = `${menu.offsetWidth}px`
openBtn.style.height = `${menu.offsetHeight}px`
// Show the menu
menu.style.opacity = 1
// Remove the menu if the user clicks somewhere else or the editor loses focus
function removeMenu (e) {
e.preventDefault()
let target = $mm(e.target)
if (!target.hasClass('menu') && !target.hasClass('dropdown') && !target.hasClass('group') && !target.parent.hasClass('dropdown')) {
document.removeEventListener('mousedown', removeMenu)
menu.remove()
openBtn.style.width = `${openBtnW}px`
openBtn.style.height = `${openBtnH}px`
openIcon.style.opacity = 1
}
}
$mm(document).on('mousedown', removeMenu)
}, 50)
})
} else {
// Open the custom menu on right click
this.on('contextmenu', e => {
e.preventDefault()
// Remove any lingering submenus
for (let submenu of menu.findAll('.menu'))
submenu.remove()
// Position the menu and add it to the DOM
menu.style.top = `${e.clientY}px`
menu.style.right = 'auto'
menu.style.bottom = 'auto'
menu.style.left = `${e.clientX}px`
$mm('body').append(menu)
// Try to keep the menu on screen
if (e.clientX + menu.offsetWidth > window.innerWidth) {
menu.style.left = `${window.innerWidth - menu.offsetWidth}px`
}
if (e.clientY + menu.offsetHeight > window.innerHeight) {
menu.style.top = 'auto'
menu.style.bottom = `${window.innerHeight - e.clientY}px`
}
// Remove the menu if the user clicks somewhere else or the editor loses focus
function removeMenu (e) {
if (e.type === 'focusout' ? e.target !== textEditor : true) {
let target = $mm(e.target)
if (menu && !target.hasClass('menu') && !target.hasClass('dropdown') && !target.hasClass('group') && !target.parent.hasClass('dropdown')) {
$mm(document).removeEventListener('mousedown', removeMenu)
$mm(document).removeEventListener('focusout', removeMenu)
for (let submenu of menu.findAll('.menu'))
submenu.remove()
menu.remove()
}
}
}
$mm(document).on('mousedown focusout', removeMenu)
})
}
// Find all of the shortcuts
let shortcuts = findShortcuts(opts.menu.groups)
// Enable shortcuts
this.on('keydown', (e, ...args) => {
let modifiers = {
Ctrl: e.ctrlKey,
Alt: e.altKey,
Shift: e.shiftKey,
Meta: e.metaKey
}
let modifier = modifiers.Ctrl || modifiers.Alt || modifiers.Shift || modifiers.Meta
for (let s of shortcuts) {
if (e.key === s.shortcut.key && ('modifier' in s.shortcut ? modifiers[s.shortcut.modifier] : !modifier)) {
e.preventDefault()
s.action(e, ...args)
}
}
})
}
return this
}
// Some example groups
let ex = {
fontSize: [
{
icon: 'font-increase',
label: 'Increase Font',
action: () => document.execCommand('increaseFontSize')
}, {
icon: 'font-decrease',
label: 'Decrease Font',
action: () => document.execCommand('decreaseFontSize')
}
],
font: [
{
icon: 'font',
label: 'Arial',
action: () => document.execCommand('fontName', false, 'Arial')
}, {
icon: 'font',
label: 'Consolas',
action: () => document.execCommand('fontName', false, 'Consolas')
}, {
icon: 'font',
label: 'Open Sans',
action: () => document.execCommand('fontName', false, 'Open Sans')
}, {
icon: 'font',
label: 'Verdana',
action: () => document.execCommand('fontName', false, 'Verdana')
}
],
alignment: [
{
icon: 'align-left',
label: 'Left',
action: () => document.execCommand('justifyLeft')
}, {
icon: 'align-center',
label: 'Center',
action: () => document.execCommand('justifyCenter')
}, {
icon: 'align-right',
label: 'Right',
action: () => document.execCommand('justifyRight')
}, {
icon: 'align-justify',
label: 'Justify',
action: () => document.execCommand('justifyFull')
}
],
textLook: [
{
icon: 'bold',
label: 'Bold',
shortcut: { modifier: 'Ctrl', key: 'b' },
action: () => document.execCommand('bold')
}, {
icon: 'italic',
label: 'Italic',
shortcut: { modifier: 'Ctrl', key: 'i' },
action: () => document.execCommand('italic')
}, {
icon: 'underline',
label: 'Underline',
shortcut: { modifier: 'Ctrl', key: 'u' },
action: () => document.execCommand('underline')
}
],
console: [
{
icon: 'arrow-right',
label: 'Log',
action: () => console.log($mm('.text-editor').innerHTML)
}
],
headers: [
{
icon: 'header',
label: 'Header 1',
action: () => document.execCommand('heading', false, 'H1')
}, {
icon: 'header',
label: 'Header 2',
action: () => document.execCommand('heading', false, 'H2')
}, {
icon: 'header',
label: 'Header 3',
action: () => document.execCommand('heading', false, 'H3')
}, {
icon: 'header',
label: 'Header 4',
action: () => document.execCommand('heading', false, 'H4')
}, {
icon: 'header',
label: 'Header 5',
action: () => document.execCommand('heading', false, 'H5')
}, {
icon: 'header',
label: 'Header 6',
action: () => document.execCommand('heading', false, 'H6')
}
],
indention: [
{
icon: 'indent',
label: 'Indent',
shortcut: {key: 'Tab'},
action: () => document.execCommand('indent')
}, {
icon: 'outdent',
label: 'Outdent',
shortcut: {modifier: 'Shift', key: 'Tab'},
action: () => document.execCommand('outdent')
}
],
list: [
{
icon: 'list-ol',
label: 'Ordered List',
action: () => document.execCommand('insertOrderedList')
}, {
icon: 'list-ul',
label: 'Unordered List',
action: () => document.execCommand('insertUnorderedList')
}
]
}
ex.fontDropdown = {
type: 'dropdown',
icon: 'font',
label: 'Font',
groups: [
{ type: 'icon', items: ex.fontSize },
{ type: 'text', items: ex.font }
]
}
ex.headerDropdown = {
type: 'dropdown',
icon: 'header',
label: 'Header',
groups: [
{ type: 'text', items: ex.headers }
]
}
ex.alignmentDropdown = {
type: 'dropdown',
icon: 'align-left',
label: 'Alignment',
groups: [
{ type: 'icon', items: ex.indention },
{ type: 'text', items: ex.alignment }
]
}
ex.indentionDropdown = {
type: 'dropdown',
icon: 'indent',
label: 'Indention',
groups: [
{ type: 'text', items: ex.indention }
]
}
ex.consoleDropdown = {
type: 'dropdown',
icon: 'arrow-right',
label: 'Console',
groups: [
{ type: 'text', items: ex.console }
]
}
ex.moreDropdown = {
type: 'dropdown',
icon: 'ellipsis-h',
label: 'More',
groups: [
ex.indentionDropdown,
ex.consoleDropdown
]
}
// Provide a way to access the prebuilt examples later
$mm.merge(editor, { prebuilt: ex })
// Adds text editor functionality
$mm.fn.extend({
editor: editor
})
// Example Use
if ($mm('.text-editor.demo')) {
$mm('.text-editor.demo').editor({
menu: {
groups: [
{type: 'icon', items: ex.textLook},
ex.fontDropdown,
ex.headerDropdown,
ex.alignmentDropdown,
{type: 'icon', items: ex.list}
]
}
})
}