toloframework
Version:
Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.
456 lines (431 loc) • 14.6 kB
JavaScript
/**********************************************************************
require( 'tp4.wysiwyg-editor' )
-----------------------------------------------------------------------
https://github.com/neilj/Squire
**********************************************************************/
;
require( "polyfill.promise" );
const
$ = require( "dom" ),
Fx = require( "dom.fx" ),
DB = require( "tfw.data-binding" ),
Flex = require( "wdg.flex" ),
Icon = require( "wdg.icon" ),
Modal = require( "wdg.modal" ),
Combo = require( "wdg.combo" ),
Prompt = require( "wdg.modal.prompt" ),
InputColor = require( "tp4.input-color" );
const
DEFAULT_MENU = [ {
id: 'undo',
key: 'ctrl-Z'
}, {
id: 'redo',
key: 'ctrl-Y'
},
'-', {
id: 'format-bold',
key: 'ctrl-B'
}, {
id: 'format-italic',
key: 'ctrl-I'
}, {
id: 'format-underline',
key: 'ctrl-U'
},
{ id: 'font', key: 'ctrl-F' },
{ id: 'format-header', key: 'ctrl-H' },
'-', {
id: 'eraser',
key: 'ctrl-E'
}, {
id: 'link',
key: 'ctrl-K'
}, {
id: 'image',
key: 'Ctrl-I'
},
'-', {
id: 'format-align-left',
key: 'ctrl-L'
}, {
id: 'format-align-center',
key: 'ctrl-M'
}, {
id: 'format-align-right',
key: 'ctrl-R'
}, {
id: 'format-align-justify',
key: 'ctrl-J'
},
'-',
{ id: 'format-float-left' },
{ id: 'format-float-right' },
{ id: 'format-float-none' }
];
/*
['header', setHeader.bind( squire )],
"|",
['list-ul', squire.makeUnorderedList.bind( squire )],
['list-ol', squire.makeOrderedList.bind( squire )],
*/
/**
* @example
* var WysiwygEditor = require("tp4.wysiwyg-editor");
* var instance = new WysiwygEditor( options );
* @class WysiwygEditor
*/
var WysiwygEditor = function ( opts ) {
var that = this;
// Real focus function will be set when iframe will be loaded.
var postponedFocus = false;
var postponedHTML = null;
var iframeHasChanged = false;
var elem = $.elem( this, 'div', 'wdg-wysiwyg', 'thm-ele2' );
var slider = $.div( 'slider' );
var menu = $.div( 'menu', "thm-bgPL" );
var iframe = $.tag( 'iframe', { src: 'squire2/squire.html' } );
var loaded = new Promise( function ( resolve, reject ) {
iframe.addEventListener( 'load', function () {
// Storing a reference to the wysiwyg editor.
var squire = iframe.contentWindow.editor;
that.squire = squire;
if ( postponedFocus ) {
that.focus = true;
}
// Adding editor buttons.
if ( postponedHTML !== null ) {
squire.setHTML( postponedHTML );
postponedHTML = null;
}
// Adding onChange event when focus is lost.
squire.addEventListener( 'input', function () {
// Prevent from update looping.
iframeHasChanged = true;
that.value = squire.getHTML();
} );
squire.addEventListener( "willPaste", function ( event ) {
console.info( "[tp4.wysiwyg-editor] (willPaste) event=", event );
var text = event.fragment.textContent;
var normalized = text.trim().toLowerCase();
if ( normalized.substr( 0, 8 ) !== '<iframe ' ) return;
if ( normalized.substr( normalized.length - 9 ) !== '</iframe>' ) return;
var div = $.div();
div.innerHTML = text;
var iframe = div.firstChild;
$.css( iframe, { maxWidth: "80vw" } );
var src = iframe.getAttribute( "src" );
var a = $.tag( "a", { target: "IFRAME", href: src } );
a.innerHTML = '<svg width=32 height=32><g stroke="#000" fill="none" stroke-width=2><path d="M14,8h-6v6 M14,24h-6v-6 M18,8h6v6 M18,24h6v-6"/></g></svg>';
a.setAttribute(
"style",
"border-radius:100%;position:absolute;right:4px;top:4px;display:inline-block;z-index:2;" +
"background:#fff;width:32px;height:32px;line-height:32px;text-align:center;" +
"font-size:16px;font-weight:bold;font-family:sans-serif;box-shadow:0 8px 16px rgba(0,0,0,.5);" +
"text-decoration:none;color:#000;"
);
$.add( div, a );
event.fragment.firstChild.textContent = "";
event.fragment.firstChild.innerHTML = div.innerHTML;
$.css( event.fragment.firstChild, { position: "relative", padding: 0 } );
} );
Object.defineProperty( that, 'squire', {
value: squire,
writable: false,
configurable: true,
enumerable: true
} );
resolve( squire );
}, false );
} );
var body = $.div( 'body', [ iframe ] );
$.add( elem, menu, body, slider );
DB.propBoolean( this, 'focus' )( function ( v ) {
loaded.then( function () {
if ( v ) {
that.squire.focus();
} else {
that.squire.blur();
}
} );
} );
DB.propUnit( this, 'height' )( function ( v ) {
if ( v.u === 'px' ) {
// Show the slider only for heights in pixels.
$.removeClass( elem, 'absolute' );
} else {
// Otherwise, put the widget in absolute position.
$.addClass( elem, 'absolute' );
}
$.css( elem, {
height: v.v + v.u
} );
} );
DB.prop( this, 'menu' )( function ( v ) {
loaded.then( function () {
$.clear( menu );
v.forEach( function ( itm ) {
if ( itm === '-' ) {
$.add( menu, $.div( 'sep', [ itm ] ) );
} else {
// Expand itm.
if ( typeof itm === 'string' ) {
itm = {
id: itm
};
}
if ( typeof itm.icon !== 'string' ) {
itm.icon = itm.id;
}
if ( typeof itm.tip !== 'string' ) {
itm.tip = _( 'tip-' + itm.id );
}
if ( typeof itm.key === 'string' ) {
itm.tip = ( itm.tip || '' ) + " [" + itm.key + "]";
loaded.then( function () {
that.squire.setKeyHandler( itm.key.toLowerCase(), function ( self, event, range ) {
event.preventDefault();
onMenu.call( that, itm, range );
} );
} );
}
var icon = new Icon( { size: '2rem', content: itm.icon } );
var tip = itm.tip;
if ( tip && tip != 'icon-' + itm.id ) {
icon.$.setAttribute( 'title', tip );
}
$.addClass( icon, 'icon' );
$.on( icon, {
down: $.addClass.bind( $, icon, 'down' ),
up: $.removeClass.bind( $, icon, 'down' ),
tap: onMenu.bind( that, itm )
} );
$.add( menu, icon );
}
} );
} );
} );
DB.propString( this, 'value' )( function ( v ) {
loaded.then( function () {
if ( iframeHasChanged ) {
iframeHasChanged = false;
} else {
that.squire.setHTML( v );
}
} );
} );
DB.propAddClass( this, 'wide' );
DB.propRemoveClass( this, 'visible', 'hide' );
DB.extend( {
value: "",
menu: DEFAULT_MENU,
height: 240,
visible: true
}, opts, this );
var initialHeight = this.height.v;
$.on( slider, {
down: function () {
initialHeight = that.height.v;
},
drag: function ( evt ) {
if ( that.height.u === 'px' ) {
// Drag only if height is in pixels.
that.height = Math.max( 200, initialHeight + evt.dy );
}
}
} );
// Managing fullscreen display.
var fullscreen = new Fx.Fullscreen( {
target: elem,
// When the component is put fullscreen, it is temporarly removed from the DOM. This causes the IFrame to be reloaded,
// and the result is that we lost content. That's why we use `postponedHTML`.
onBeforeReplace: function () {
postponedHTML = that.squire.getHTML();
}
} );
};
WysiwygEditor.prototype.insertHTML = function ( html ) {
this.squire.insertHTML( html );
this.squire.focus();
};
WysiwygEditor.prototype.insertImage = function ( url ) {
if ( typeof url === 'string' ) {
this.squire.insertHTML( "<img src=" + JSON.stringify( url ) + "/>" );
this.squire.focus();
} else {
var that = this;
Prompt( _( 'image-url' ), function ( href ) {
if ( href && href.trim().length > 0 ) {
this.squire.insertHTML( "<img src=" + JSON.stringify( url ) + "/>" );
that.squire.focus();
}
} );
}
};
/**
* @return void
*/
WysiwygEditor.prototype.processButton = function ( id ) {
console.error( "Button \"" + id + "\" has no code to process it!" );
};
function setHeader() {
this.modifyBlocks( function ( root ) {
var e = root.firstChild;
var name = e.nodeName;
if ( name.charAt( 0 ).toUpperCase() == 'H' && name.length == 2 ) {
var level = Math.min( 3, parseInt( name.charAt( 1 ) ) );
level = ( level + 1 ) % 4;
var header = root.ownerDocument.createElement( level ? 'H' + level : 'DIV' );
header.innerHTML = e.innerHTML;
$.replace( header, e );
} else {
var h1 = root.ownerDocument.createElement( 'H1' );
h1.innerHTML = e.outerHTML;
$.replace( h1, e );
}
return root;
} );
}
function setFontFaceSizeColor() {
var that = this;
var optFontName = {};
[ 'josefin-sans', 'serif', 'sans-serif', 'monospace' ].forEach( function ( fontname ) {
optFontName[ fontname ] = $.div( {
style: 'font-family:' + fontname
}, [ _( 'sentence' ) ] );
} );
var selFontName = new Combo( { label: _( 'font-name' ), wide: true, content: optFontName } );
var selFontSize = new Combo( {
label: _( 'font-size' ),
content: {
'70': '70 %',
'80': '80 %',
'90': '90 %',
'100': '100 %',
'110': '110 %',
'130': '130 %',
'160': '160 %',
'200': '200 %'
}
} );
selFontSize.value = '100';
var selFontColor = new InputColor( _( 'font-color' ) );
selFontColor.val( 'black' );
Modal.confirm( $.div( [
selFontName,
new Flex( { content: [ selFontSize, selFontColor ] } )
] ), function () {
that.setFontFace( selFontName.value );
that.setFontSize( selFontSize.value + "%" );
that.setTextColour( selFontColor.val() );
} );
}
function setFloat( float ) {
var selection = this.getSelection();
if ( selection ) {
var container = selection.commonAncestorContainer;
var images = container.querySelectorAll( 'img' );
Array.prototype.forEach.call( images, function ( img ) {
img.style.float = float;
} );
}
}
function makeLink() {
var that = this;
if ( this.hasFormat( 'a' ) ) {
this.changeFormat( null, {
tag: 'a'
}, null, true );
} else {
Prompt( _( 'tip-link' ), function ( href ) {
if ( href && href.trim().length > 0 ) {
that.makeLink( href, { target: '_blank' } );
}
} );
}
}
/**
* `this` is the squire object.
*/
function onMenu( item ) {
var squire = this.squire;
var id = item.id;
switch ( id ) {
case 'undo':
squire.undo();
break;
case 'redo':
squire.redo();
break;
case 'eraser':
squire.removeAllFormatting();
break;
case 'link':
makeLink.call( squire );
break;
case 'image':
this.insertImage();
break;
case 'format-bold':
if ( squire.hasFormat( 'b' ) ) {
squire.removeBold();
} else {
squire.bold();
}
break;
case 'format-italic':
if ( squire.hasFormat( 'i' ) || squire.hasFormat( 'em' ) ) {
squire.removeItalic();
} else {
squire.italic();
}
break;
case 'format-underline':
if ( squire.hasFormat( 'u' ) ) {
squire.removeUnderline();
} else {
squire.underline();
}
break;
case 'format-align-left':
squire.setTextAlignment( 'left' );
break;
case 'format-align-center':
squire.setTextAlignment( 'center' );
break;
case 'format-align-right':
squire.setTextAlignment( 'right' );
break;
case 'format-align-justify':
squire.setTextAlignment( 'justify' );
break;
case 'font':
setFontFaceSizeColor.call( squire );
break;
case 'format-float-left':
setFloat.call( squire, 'left' );
break;
case 'format-float-center':
setFloat.call( squire, 'center' );
break;
case 'format-float-right':
setFloat.call( squire, 'right' );
break;
case 'format-float-none':
setFloat.call( squire, 'none' );
break;
case 'format-header':
setHeader.call( squire );
break;
default:
this.processButton( id );
}
this.focus = true;
}
Object.defineProperty( WysiwygEditor, 'DEFAULT_MENU', {
value: DEFAULT_MENU,
writable: false,
configurable: false,
enumerable: true
} );
module.exports = WysiwygEditor;