diff --git a/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/dialogs/CheatSheetIcon.png b/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/dialogs/CheatSheetIcon.png new file mode 100644 index 00000000..6252aa91 Binary files /dev/null and b/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/dialogs/CheatSheetIcon.png differ diff --git a/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/dialogs/cheatsheet.js b/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/dialogs/cheatsheet.js new file mode 100644 index 00000000..04cd86d9 --- /dev/null +++ b/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/dialogs/cheatsheet.js @@ -0,0 +1,25 @@ +/** + * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or http://ckeditor.com/license + */ + +CKEDITOR.dialog.add( 'cheatsheet', function( editor ) { + var lang = "Cheatsheet", + imagePath = CKEDITOR.plugins.get( 'cheatsheet' ).path + 'dialogs/' + ( CKEDITOR.env.hidpi ? 'hidpi/' : '' ) + 'CheatSheetIcon.png'; + + return { + title: 'shortcuts', + contents: [{ + id: 'tab1', + label: '', + title: '', + expand: true, + padding: 0, + elements: [{ + type: 'html', + html: '

Keyboard Shortcuts

blahblah' + }] + }], + buttons: [ CKEDITOR.dialog.cancelButton ] + }; +} ); diff --git a/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/dialogs/hidpi/CheatSheetIcon.png b/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/dialogs/hidpi/CheatSheetIcon.png new file mode 100644 index 00000000..6252aa91 Binary files /dev/null and b/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/dialogs/hidpi/CheatSheetIcon.png differ diff --git a/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/dialogs/sheet.html b/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/dialogs/sheet.html new file mode 100644 index 00000000..13c25b94 --- /dev/null +++ b/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/dialogs/sheet.html @@ -0,0 +1,77 @@ +
+

Keyboard Shortcuts

+
+

Many functions in CKEditor have their equivalent keyboard shortcuts. This is one of the reasons why working with the editor is simple and efficient.

+

The list below contains available keyboard shortcuts grouped by problem areas.

+

Working with a Document

+ +

Navigation

+ +

Writing

+ +

Undo and Redo

+ +

Cut, Copy and Paste

+ +

Text Selection

+ +

Text Styling

+ +

Rich Text

+ +

Accessibility

+

On entering the toolbar you can use the Tab and Shift+Tab shortcuts to navigate between button groups and the Arrow keys to navigate between the buttons within a group.

+

+
This page was last modified on 11 May 2011, at 14:28.
+
\ No newline at end of file diff --git a/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/icons/CheatSheetIcon.png b/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/icons/CheatSheetIcon.png new file mode 100644 index 00000000..189df818 Binary files /dev/null and b/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/icons/CheatSheetIcon.png differ diff --git a/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/icons/hidpi/CheatSheetIcon.png b/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/icons/hidpi/CheatSheetIcon.png new file mode 100644 index 00000000..189df818 Binary files /dev/null and b/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/icons/hidpi/CheatSheetIcon.png differ diff --git a/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/plugin.js b/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/plugin.js new file mode 100644 index 00000000..35a3b921 --- /dev/null +++ b/htmlarea/ckeditor/ckeditor/plugins/cheatsheet/plugin.js @@ -0,0 +1,188 @@ +/** + * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or http://ckeditor.com/license + */ + +/** + * @fileOverview A Serendipity CKE-Cheatsheet plugin: cheatsheet, v. 1.2 - 2014-09-02 + */ + +CKEDITOR.plugins.add( 'cheatsheet', { + icons: 'CheatSheetIcon', // %REMOVE_LINE_CORE% + hidpi: true, // %REMOVE_LINE_CORE% + init: function( editor ) { + var command = editor.addCommand( 'CheatSheet', new CKEDITOR.dialogCommand( 'CheatSheetDialog' ) ); + command.modes = { wysiwyg: 1, source: 1 }; + command.canUndo = false; + command.readOnly = 1; + + editor.ui.addButton && editor.ui.addButton( 'CheatSheet', { + label: 'CKEDITOR Cheat Sheet', + icon : this.path + "icons/CheatSheetIcon.png", + command: 'CheatSheet', + toolbar: 'cheatsheet' + }); + + CKEDITOR.dialog.add( 'CheatSheetDialog', function( api ) + { + // CKEDITOR.dialog.definition + var dialogDefinition = + { + title : 'CKEDITOR Cheat Sheet', + minWidth: 390, + minHeight: 230, + maxWidth: 600, + maxHeight: 400, + contents : [ + { + id : 'tab1', + label : 'Cheat Sheet', + title : 'Title', + expand : true, + padding : 0, + elements : + [ + { + type : 'html', + html: '' + + '
' + + '

Keyboard Shortcuts

' + + '
' + + '

Many functions in CKEditor have their equivalent keyboard shortcuts. This is one of the reasons why working with the editor is simple and efficient.

' + + '

The list below contains available keyboard shortcuts grouped by problem areas.

' + + '

Working with a Document

' + + '
    ' + + '
  • Esc – closes a CKEditor dialog window, drop-down list, or context menu. Also moves from the context menu submenu to the parent option.
  • ' + + '
  • Enter – selects a CKEditor function from the toolbar, drop-down list, or context menu. Equivalent to the OK button in a dialog window.
  • ' + + '
  • Shift+F10, Menu/Application key – opens the element\'s context menu.
  • ' + + '
' + + '

Navigation

' + + '
    ' + + '
  • Home – jumps to the beginning of the line.
  • ' + + '
  • Ctrl+Home – jumps to the beginning of the document.
  • ' + + '
  • End – jumps to the end of the line.
  • ' + + '
  • Ctrl+End – jumps to the end of the document.
  • ' + + '
  • PgDn – scrolls down the document, approximately by the length of the editing area.
  • ' + + '
  • PgUp – scrolls up the document, approximately by the length of the editing area.
  • ' + + '
' + + '

Writing

' + + '
    ' + + '
  • Enter (Return) – ends a paragraph and starts a new one.
  • ' + + '
  • Shift+Enter – adds a line break.
  • ' + + '
  • Backspace, Del – deletes a character.
  • ' + + '
  • Ctrl+Backspace, Ctrl+Del – deletes a word.
  • ' + + '
' + + '

Undo and Redo

' + + '
    ' + + '
  • Ctrl+Z – performs the undo operation.
  • ' + + '
  • Ctrl+Y – performs the redo operation.
  • ' + + '
' + + '

Cut, Copy and Paste

' + + '
    ' + + '
  • Ctrl+X, Shift+Del – cuts a text fragment to clipboard.
  • ' + + '
  • Ctrl+C – copies a text fragment to clipboard.
  • ' + + '
  • Ctrl+V, Shift+Insert – pastes a text fragment from clipboard.
  • ' + + '
' + + '

Text Selection

' + + '
    ' + + '
  • Ctrl+A – selects all document contents.
  • ' + + '
  • Shift+Arrow – selects a text fragment by letters.
  • ' + + '
  • Ctrl+Shift+Arrow – selects a text fragment by words.
  • ' + + '
  • Shift+Home – selects a text fragment from the cursor to the beginning of the line.
  • ' + + '
  • Shift+End – selects a text fragment from the cursor to the end of the line.
  • ' + + '
  • Ctrl+Shift+Home – selects a text fragment from the cursor to the beginning of the document.
  • ' + + '
  • Ctrl+Shift+End – selects a text fragment from the cursor to the end of the document.
  • ' + + '
  • Shift+PgDn – selects a text fragment of approximately the length of the editing area starting from the cursor and going down.
  • ' + + '
  • Shift+PgUp – selects a text fragment of approximately the length of the editing area starting from the cursor and going up.
  • ' + + '
' + + '

Text Styling

' + + '
    ' + + '
  • Ctrl+B – applies bold formatting to a text fragment.
  • ' + + '
  • Ctrl+I – applies italics formatting to a text fragment.
  • ' + + '
  • Ctrl+U – applies underline formatting to a text fragment.
  • ' + + '
' + + '

Rich Text

' + + '
    ' + + '
  • Ctrl+L – opens the Link dialog window.
  • ' + + '
' + + '

Accessibility

' + + '

On entering the toolbar you can use the Tab and Shift+Tab shortcuts to navigate between button groups and the Arrow keys to navigate between the buttons within a group.

' + + '

    ' + + '
  • Alt+0 – opens Help.
  • ' + + '
  • Alt+- (minus) – collapses and restores the toolbar.
  • ' + + '
  • Alt+F10 – enters the toolbar or the tab list of the currently open dialog window.
  • ' + + '
  • Alt+F11 – enters the elements path.
  • ' + + '
  • Tab – moves to the next toolbar button group, context menu suboption, elements path element, dialog window element, or dialog window tab while in the tab list.
  • ' + + '
  • Right Arrow – moves to the next toolbar button within the group, context menu suboption, elements path element, dialog window element, or dialog window tab while in the tab list.
  • ' + + '
  • Tab or Down Arrow – moves to the next drop-down list or context menu option.
  • ' + + '
  • Shift+Tab – moves to the previous toolbar button group, context menu parent option, elements path element, dialog window element, or dialog window tab while in the tab list.
  • ' + + '
  • Left Arrow – moves to the previous toolbar button within the group, context menu parent option, elements path element, dialog window element, or dialog window tab while in the tab list.
  • ' + + '
  • Shift+Tab or Up Arrow – moves to the previous drop-down list or context menu option.
  • ' + + '
  • Space or Enter – activates a toolbar button, a context menu option, a drop-down list option, an elements path element, or a dialog window tab once selected. Also enters a context menu submenu, if it is available.
  • ' + + '
' + + '
This pages content was last modified on 11 May 2011, at 14:28.
' + + '
' + }] + }], + buttons: [ CKEDITOR.dialog.cancelButton ] + }; + + return dialogDefinition; + }); + } +}); \ No newline at end of file diff --git a/htmlarea/ckeditor/ckeditor/plugins/lineutils/plugin.js b/htmlarea/ckeditor/ckeditor/plugins/lineutils/plugin.js new file mode 100644 index 00000000..a48782a2 --- /dev/null +++ b/htmlarea/ckeditor/ckeditor/plugins/lineutils/plugin.js @@ -0,0 +1,933 @@ +/** + * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or http://ckeditor.com/license + */ + + /** + * @fileOverview A set of utilities to find and make horizontal spaces in edited contents. + */ + +'use strict'; + +( function() { + + CKEDITOR.plugins.add( 'lineutils' ); + + /** + * Determines a position relative to an element in DOM (before). + * + * @readonly + * @property {Number} [=0] + * @member CKEDITOR + */ + CKEDITOR.LINEUTILS_BEFORE = 1; + + /** + * Determines a position relative to an element in DOM (after). + * + * @readonly + * @property {Number} [=2] + * @member CKEDITOR + */ + CKEDITOR.LINEUTILS_AFTER = 2; + + /** + * Determines a position relative to an element in DOM (inside). + * + * @readonly + * @property {Number} [=4] + * @member CKEDITOR + */ + CKEDITOR.LINEUTILS_INSIDE = 4; + + /** + * An utility that traverses DOM tree and discovers elements + * (relations) matching user-defined lookups. + * + * @private + * @class CKEDITOR.plugins.lineutils.finder + * @constructor Creates a Finder class instance. + * @param {CKEDITOR.editor} editor Editor instance that Finder belongs to. + * @param {Object} def Finder's definition. + * @since 4.3 + */ + function Finder( editor, def ) { + CKEDITOR.tools.extend( this, { + editor: editor, + editable: editor.editable(), + doc: editor.document, + win: editor.window + }, def, true ); + + this.frame = this.win.getFrame(); + this.inline = this.editable.isInline(); + this.target = this[ this.inline ? 'editable' : 'doc' ]; + } + + Finder.prototype = { + /** + * Initializes searching for elements with every mousemove event fired. + * To stop searching use {@link #stop}. + * + * @param {Function} [callback] Function executed on every iteration. + */ + start: function( callback ) { + var that = this, + editor = this.editor, + doc = this.doc, + el, x, y; + + var moveBuffer = CKEDITOR.tools.eventsBuffer( 50, function() { + if ( editor.readOnly || editor.mode != 'wysiwyg' ) + return; + + that.relations = {}; + + el = new CKEDITOR.dom.element( doc.$.elementFromPoint( x, y ) ); + + that.traverseSearch( el ); + + if ( !isNaN( x + y ) ) + that.pixelSearch( el, x, y ); + + callback && callback( that.relations, x, y ); + } ); + + // Searching starting from element from point on mousemove. + this.listener = this.editable.attachListener( this.target, 'mousemove', function( evt ) { + x = evt.data.$.clientX; + y = evt.data.$.clientY; + + moveBuffer.input(); + } ); + + this.editable.attachListener( this.inline ? this.editable : this.frame, 'mouseout', function( evt ) { + moveBuffer.reset(); + } ); + }, + + /** + * Stops observing mouse events attached by {@link #start}. + */ + stop: function() { + if ( this.listener ) + this.listener.removeListener(); + }, + + /** + * Returns a range representing the relation, according to its element + * and type. + * + * @param {Object} location Location containing unique identifier and type. + * @returns {CKEDITOR.dom.range} Range representing the relation. + */ + getRange: ( function() { + var where = {}; + + where[ CKEDITOR.LINEUTILS_BEFORE ] = CKEDITOR.POSITION_BEFORE_START; + where[ CKEDITOR.LINEUTILS_AFTER ] = CKEDITOR.POSITION_AFTER_END; + where[ CKEDITOR.LINEUTILS_INSIDE ] = CKEDITOR.POSITION_AFTER_START; + + return function( location ) { + var range = this.editor.createRange(); + + range.moveToPosition( this.relations[ location.uid ].element, where[ location.type ] ); + + return range; + }; + } )(), + + /** + * Stores given relation in {@link #relations} object. Processes the relation + * to normalize and avoid duplicates. + * + * @param {CKEDITOR.dom.element} el Element of the relation. + * @param {Number} type Relation, one of `CKEDITOR.LINEUTILS_AFTER`, `CKEDITOR.LINEUTILS_BEFORE`, `CKEDITOR.LINEUTILS_INSIDE`. + */ + store: ( function() { + function merge( el, type, relations ) { + var uid = el.getUniqueId(); + + if ( uid in relations ) + relations[ uid ].type |= type; + else + relations[ uid ] = { element: el, type: type }; + } + + return function( el, type ) { + var alt; + + // Normalization to avoid duplicates: + // CKEDITOR.LINEUTILS_AFTER becomes CKEDITOR.LINEUTILS_BEFORE of el.getNext(). + if ( is( type, CKEDITOR.LINEUTILS_AFTER ) && isStatic( alt = el.getNext() ) && alt.isVisible() ) { + merge( alt, CKEDITOR.LINEUTILS_BEFORE, this.relations ); + type ^= CKEDITOR.LINEUTILS_AFTER; + } + + // Normalization to avoid duplicates: + // CKEDITOR.LINEUTILS_INSIDE becomes CKEDITOR.LINEUTILS_BEFORE of el.getFirst(). + if ( is( type, CKEDITOR.LINEUTILS_INSIDE ) && isStatic( alt = el.getFirst() ) && alt.isVisible() ) { + merge( alt, CKEDITOR.LINEUTILS_BEFORE, this.relations ); + type ^= CKEDITOR.LINEUTILS_INSIDE; + } + + merge( el, type, this.relations ); + }; + } )(), + + /** + * Traverses DOM tree towards root, checking all ancestors + * with lookup rules, avoiding duplicates. Stores positive relations + * in {@link #relations} object. + * + * @param {CKEDITOR.dom.element} el Element which is the starting point. + */ + traverseSearch: function( el ) { + var l, type, uid; + + // Go down DOM towards root (or limit). + do { + uid = el.$[ 'data-cke-expando' ]; + + // This element was already visited and checked. + if ( uid && uid in this.relations ) + continue; + + if ( el.equals( this.editable ) ) + return; + + if ( isStatic( el ) ) { + // Collect all addresses yielded by lookups for that element. + for ( l in this.lookups ) { + + if ( ( type = this.lookups[ l ]( el ) ) ) + this.store( el, type ); + } + } + } while ( !isLimit( el ) && ( el = el.getParent() ) ) + }, + + /** + * Iterates vertically pixel-by-pixel within given element starting + * from given coordinates, searching for elements in the neighbourhood. + * Once an element is found it is processed by {@link #traverseSearch}. + * + * @param {CKEDITOR.dom.element} el Element which is the starting point. + * @param {Number} [x] Horizontal mouse coordinate relative to the viewport. + * @param {Number} [y] Vertical mouse coordinate relative to the viewport. + */ + pixelSearch: ( function() { + var contains = CKEDITOR.env.ie || CKEDITOR.env.webkit ? + function( el, found ) { + return el.contains( found ); + } + : + function( el, found ) { + return !!( el.compareDocumentPosition( found ) & 16 ); + }; + + // Iterates pixel-by-pixel from starting coordinates, moving by defined + // step and getting elementFromPoint in every iteration. Iteration stops when: + // * A valid element is found. + // * Condition function returns false (i.e. reached boundaries of viewport). + // * No element is found (i.e. coordinates out of viewport). + // * Element found is ascendant of starting element. + // + // @param {Object} doc Native DOM document. + // @param {Object} el Native DOM element. + // @param {Number} xStart Horizontal starting coordinate to use. + // @param {Number} yStart Vertical starting coordinate to use. + // @param {Number} step Step of the algorithm. + // @param {Function} condition A condition relative to current vertical coordinate. + function iterate( el, xStart, yStart, step, condition ) { + var y = yStart, + tryouts = 0, + found, uid; + + while ( condition( y ) ) { + y += step; + + // If we try and we try, and still nothing's found, let's end + // that party. + if ( ++tryouts == 25 ) + return; + + found = this.doc.$.elementFromPoint( xStart, y ); + + // Nothing found. This is crazy... but... + // It might be that a line, which is in different document, + // covers that pixel (elementFromPoint is doc-sensitive). + // Better let's have another try. + if ( !found ) + continue; + + // Still in the same element. + else if ( found == el ) { + tryouts = 0; + continue; + } + + // Reached the edge of an element and found an ancestor or... + // A line, that covers that pixel. Better let's have another try. + else if ( !contains( el, found ) ) + continue; + + tryouts = 0; + + // Found a valid element. Stop iterating. + if ( isStatic( ( found = new CKEDITOR.dom.element( found ) ) ) ) + return found; + } + } + + return function( el, x, y ) { + var paneHeight = this.win.getViewPaneSize().height, + + // Try to find an element iterating *up* from the starting point. + neg = iterate.call( this, el.$, x, y, -1, function( y ) { + return y > 0; + } ), + + // Try to find an element iterating *down* from the starting point. + pos = iterate.call( this, el.$, x, y, 1, function( y ) { + return y < paneHeight; + } ); + + if ( neg ) { + this.traverseSearch( neg ); + + // Iterate towards DOM root until neg is a direct child of el. + while ( !neg.getParent().equals( el ) ) + neg = neg.getParent(); + } + + if ( pos ) { + this.traverseSearch( pos ); + + // Iterate towards DOM root until pos is a direct child of el. + while ( !pos.getParent().equals( el ) ) + pos = pos.getParent(); + } + + // Iterate forwards starting from neg and backwards from + // pos to harvest all children of el between those elements. + // Stop when neg and pos meet each other or there's none of them. + // TODO (?) reduce number of hops forwards/backwards. + while ( neg || pos ) { + if ( neg ) + neg = neg.getNext( isStatic ); + + if ( !neg || neg.equals( pos ) ) + break; + + this.traverseSearch( neg ); + + if ( pos ) + pos = pos.getPrevious( isStatic ); + + if ( !pos || pos.equals( neg ) ) + break; + + this.traverseSearch( pos ); + } + }; + } )(), + + /** + * Unline {@link #traverseSearch}, it collects **all** elements from editable's DOM tree + * and runs lookups for every one of them, collecting relations. + * + * @returns {Object} {@link #relations}. + */ + greedySearch: function() { + this.relations = {}; + + var all = this.editable.getElementsByTag( '*' ), + i = 0, + el, type, l; + + while ( ( el = all.getItem( i++ ) ) ) { + // Don't consider editable, as it might be inline, + // and i.e. checking it's siblings is pointless. + if ( el.equals( this.editable ) ) + continue; + + // Don't visit non-editable internals, for example widget's + // guts (above wrapper, below nested). Still check editable limits, + // as they are siblings with editable contents. + if ( !el.hasAttribute( 'contenteditable' ) && el.isReadOnly() ) + continue; + + if ( isStatic( el ) && el.isVisible() ) { + // Collect all addresses yielded by lookups for that element. + for ( l in this.lookups ) { + if ( ( type = this.lookups[ l ]( el ) ) ) + this.store( el, type ); + } + } + } + + return this.relations; + } + + /** + * Relations express elements in DOM that match user-defined {@link #lookups}. + * Every relation has its own `type` that determines whether + * it refers to the space before, after or inside of `element`. + * This object stores relations found by {@link #traverseSearch} or {@link #greedySearch}, structured + * in the following way: + * + * relations: { + * // Unique identifier of the element. + * Number: { + * // Element of this relation. + * element: {@link CKEDITOR.dom.element} + * // Conjunction of CKEDITOR.LINEUTILS_BEFORE, CKEDITOR.LINEUTILS_AFTER and CKEDITOR.LINEUTILS_INSIDE. + * type: Number + * }, + * ... + * } + * + * @property {Object} relations + * @readonly + */ + + /** + * A set of user-defined functions used by Finder to check if an element + * is a valid relation, belonging to {@link #relations}. + * When the criterion is met, lookup returns a logical conjunction of `CKEDITOR.LINEUTILS_BEFORE`, + * `CKEDITOR.LINEUTILS_AFTER` or `CKEDITOR.LINEUTILS_INSIDE`. + * + * Lookups are passed along with Finder's definition. + * + * lookups: { + * 'some lookup': function( el ) { + * if ( someCondition ) + * return CKEDITOR.LINEUTILS_BEFORE; + * }, + * ... + * } + * + * @property {Object} lookups + */ + }; + + + /** + * An utility that analyses relations found by + * CKEDITOR.plugins.lineutils.finder and locates them + * in the viewport as horizontal lines of specific coordinates. + * + * @private + * @class CKEDITOR.plugins.lineutils.locator + * @constructor Creates a Locator class instance. + * @param {CKEDITOR.editor} editor Editor instance that Locator belongs to. + * @since 4.3 + */ + function Locator( editor, def ) { + CKEDITOR.tools.extend( this, def, { + editor: editor + }, true ); + } + + Locator.prototype = { + /** + * Localizes Y coordinate for all types of every single relation and stores + * them in the object. + * + * @param {Object} relations {@link CKEDITOR.plugins.lineutils.finder#relations}. + * @returns {Object} {@link #locations}. + */ + locate: ( function() { + var rel, uid; + + function locateSibling( rel, type ) { + var sib = rel.element[ type === CKEDITOR.LINEUTILS_BEFORE ? 'getPrevious' : 'getNext' ](); + + // Return the middle point between siblings. + if ( sib && isStatic( sib ) ) { + rel.siblingRect = sib.getClientRect(); + + if ( type == CKEDITOR.LINEUTILS_BEFORE ) + return ( rel.siblingRect.bottom + rel.elementRect.top ) / 2; + else + return ( rel.elementRect.bottom + rel.siblingRect.top ) / 2; + } + + // If there's no sibling, use the edge of an element. + else { + if ( type == CKEDITOR.LINEUTILS_BEFORE ) + return rel.elementRect.top; + else + return rel.elementRect.bottom; + } + } + + return function( relations ) { + this.locations = {}; + + for ( uid in relations ) { + rel = relations[ uid ]; + rel.elementRect = rel.element.getClientRect(); + + if ( is( rel.type, CKEDITOR.LINEUTILS_BEFORE ) ) + this.store( uid, CKEDITOR.LINEUTILS_BEFORE, locateSibling( rel, CKEDITOR.LINEUTILS_BEFORE ) ); + + if ( is( rel.type, CKEDITOR.LINEUTILS_AFTER ) ) + this.store( uid, CKEDITOR.LINEUTILS_AFTER, locateSibling( rel, CKEDITOR.LINEUTILS_AFTER ) ); + + // The middle point of the element. + if ( is( rel.type, CKEDITOR.LINEUTILS_INSIDE ) ) + this.store( uid, CKEDITOR.LINEUTILS_INSIDE, ( rel.elementRect.top + rel.elementRect.bottom ) / 2 ); + } + + return this.locations; + }; + } )(), + + /** + * Calculates distances from every location to given vertical coordinate + * and sorts locations according to that distance. + * + * @param {Number} y The vertical coordinate used for sorting, used as a reference. + * @param {Number} [howMany] Determines the number "closest locations" to be returned. + * @returns {Array} Sorted, array representation of {@link #locations}. + */ + sort: ( function() { + var locations, sorted, + dist, uid, type, i; + + function distance( y ) { + return Math.abs( y - locations[ uid ][ type ] ); + } + + return function( y, howMany ) { + locations = this.locations; + sorted = []; + + for ( uid in locations ) { + for ( type in locations[ uid ] ) { + dist = distance( y ); + + // An array is empty. + if ( !sorted.length ) + sorted.push( { uid: +uid, type: type, dist: dist } ); + else { + // Sort the array on fly when it's populated. + for ( i = 0; i < sorted.length; i++ ) { + if ( dist < sorted[ i ].dist ) { + sorted.splice( i, 0, { uid: +uid, type: type, dist: dist } ); + break; + } + } + + // Nothing was inserted, so the distance is bigger than + // any of already calculated: push to the end. + if ( i == sorted.length ) + sorted.push( { uid: +uid, type: type, dist: dist } ); + } + } + } + + if ( typeof howMany != 'undefined' ) + return sorted.slice( 0, howMany ); + else + return sorted; + }; + } )(), + + /** + * Stores the location in a collection. + * + * @param {Number} uid Unique identifier of the relation. + * @param {Number} type One of `CKEDITOR.LINEUTILS_BEFORE`, `CKEDITOR.LINEUTILS_AFTER` and `CKEDITOR.LINEUTILS_INSIDE`. + * @param {Number} y Vertical position of the relation. + */ + store: function( uid, type, y ) { + if ( !this.locations[ uid ] ) + this.locations[ uid ] = {}; + + this.locations[ uid ][ type ] = y; + } + + /** + * @readonly + * @property {Object} locations + */ + }; + + var tipCss = { + display: 'block', + width: '0px', + height: '0px', + 'border-color': 'transparent', + 'border-style': 'solid', + position: 'absolute', + top: '-6px' + }, + + lineStyle = { + height: '0px', + 'border-top': '1px dashed red', + position: 'absolute', + 'z-index': 9999 + }, + + lineTpl = + '
' + + ' ' + + ' ' + + '
'; + + /** + * An utility that draws horizontal lines in DOM according to locations + * returned by CKEDITOR.plugins.lineutils.locator. + * + * @private + * @class CKEDITOR.plugins.lineutils.liner + * @constructor Creates a Liner class instance. + * @param {CKEDITOR.editor} editor Editor instance that Liner belongs to. + * @param {Object} def Liner's definition. + * @since 4.3 + */ + function Liner( editor, def ) { + var editable = editor.editable(); + + CKEDITOR.tools.extend( this, { + editor: editor, + editable: editable, + doc: editor.document, + win: editor.window, + container: CKEDITOR.document.getBody(), + winTop: CKEDITOR.document.getWindow() + }, def, true ); + + this.hidden = {}; + this.visible = {}; + + this.inline = editable.isInline(); + + if ( !this.inline ) + this.frame = this.win.getFrame(); + + this.queryViewport(); + + // Callbacks must be wrapped. Otherwise they're not attached + // to global DOM objects (i.e. topmost window) for every editor + // because they're treated as duplicates. They belong to the + // same prototype shared among Liner instances. + var queryViewport = CKEDITOR.tools.bind( this.queryViewport, this ), + hideVisible = CKEDITOR.tools.bind( this.hideVisible, this ), + removeAll = CKEDITOR.tools.bind( this.removeAll, this ); + + editable.attachListener( this.winTop, 'resize', queryViewport ); + editable.attachListener( this.winTop, 'scroll', queryViewport ); + + editable.attachListener( this.winTop, 'resize', hideVisible ); + editable.attachListener( this.win, 'scroll', hideVisible ); + + editable.attachListener( this.inline ? editable : this.frame, 'mouseout', function( evt ) { + var x = evt.data.$.clientX, + y = evt.data.$.clientY; + + this.queryViewport(); + + // Check if mouse is out of the element (iframe/editable). + if ( x <= this.rect.left || x >= this.rect.right || y <= this.rect.top || y >= this.rect.bottom ) + this.hideVisible(); + + // Check if mouse is out of the top-window vieport. + if ( x <= 0 || x >= this.winTopPane.width || y <= 0 || y >= this.winTopPane.height ) + this.hideVisible(); + }, this ); + + editable.attachListener( editor, 'resize', queryViewport ); + editable.attachListener( editor, 'mode', removeAll ); + editor.on( 'destroy', removeAll ); + + this.lineTpl = new CKEDITOR.template( lineTpl ).output( { + lineStyle: CKEDITOR.tools.writeCssText( + CKEDITOR.tools.extend( {}, lineStyle, this.lineStyle, true ) + ), + tipLeftStyle: CKEDITOR.tools.writeCssText( + CKEDITOR.tools.extend( {}, tipCss, { + left: '0px', + 'border-left-color': 'red', + 'border-width': '6px 0 6px 6px' + }, this.tipCss, this.tipLeftStyle, true ) + ), + tipRightStyle: CKEDITOR.tools.writeCssText( + CKEDITOR.tools.extend( {}, tipCss, { + right: '0px', + 'border-right-color': 'red', + 'border-width': '6px 6px 6px 0' + }, this.tipCss, this.tipRightStyle, true ) + ) + } ); + } + + Liner.prototype = { + /** + * Permanently removes all lines (both hidden and visible) from DOM. + */ + removeAll: function() { + var l; + + for ( l in this.hidden ) { + this.hidden[ l ].remove(); + delete this.hidden[ l ]; + } + + for ( l in this.visible ) { + this.visible[ l ].remove(); + delete this.visible[ l ]; + } + }, + + /** + * Hides a given line. + * + * @param {CKEDITOR.dom.element} line The line to be hidden. + */ + hideLine: function( line ) { + var uid = line.getUniqueId(); + + line.hide(); + + this.hidden[ uid ] = line; + delete this.visible[ uid ]; + }, + + /** + * Shows a given line. + * + * @param {CKEDITOR.dom.element} line The line to be shown. + */ + showLine: function( line ) { + var uid = line.getUniqueId(); + + line.show(); + + this.visible[ uid ] = line; + delete this.hidden[ uid ]; + }, + + /** + * Hides all visible lines. + */ + hideVisible: function() { + for ( var l in this.visible ) + this.hideLine( this.visible[ l ] ); + }, + + /** + * Shows a line at given location. + * + * @param {Object} location Location object containing unique identifier of the relation + * and its type. Usually returned by {@link CKEDITOR.plugins.lineutils.locator#sort}. + * @param {Function} [callback] A callback to be called once the line is shown. + */ + placeLine: function( location, callback ) { + var styles, line, l; + + // No style means that line would be out of viewport. + if ( !( styles = this.getStyle( location.uid, location.type ) ) ) + return; + + // Search for any visible line of a different hash first. + // It's faster to re-position visible line than to show it. + for ( l in this.visible ) { + if ( this.visible[ l ].getCustomData( 'hash' ) !== this.hash ) { + line = this.visible[ l ]; + break; + } + } + + // Search for any hidden line of a different hash. + if ( !line ) { + for ( l in this.hidden ) { + if ( this.hidden[ l ].getCustomData( 'hash' ) !== this.hash ) { + this.showLine( ( line = this.hidden[ l ] ) ); + break; + } + } + } + + // If no line available, add the new one. + if ( !line ) + this.showLine( ( line = this.addLine() ) ); + + // Mark the line with current hash. + line.setCustomData( 'hash', this.hash ); + + // Mark the line as visible. + this.visible[ line.getUniqueId() ] = line; + + line.setStyles( styles ); + + callback && callback( line ); + }, + + /** + * Creates style set to be used by the line, representing a particular + * relation (location). + * + * @param {Number} uid Unique identifier of the relation. + * @param {Number} type Type of the relation. + * @returns {Object} An object containing styles. + */ + getStyle: function( uid, type ) { + var rel = this.relations[ uid ], + loc = this.locations[ uid ][ type ], + styles = {}, + hdiff; + + // Line should be between two elements. + if ( rel.siblingRect ) + styles.width = Math.max( rel.siblingRect.width, rel.elementRect.width ); + // Line is relative to a single element. + else + styles.width = rel.elementRect.width; + + // Let's calculate the vertical position of the line. + if ( this.inline ) + styles.top = loc + this.winTopScroll.y; + else + styles.top = this.rect.top + this.winTopScroll.y + loc; + + // Check if line would be vertically out of the viewport. + if ( styles.top - this.winTopScroll.y < this.rect.top || styles.top - this.winTopScroll.y > this.rect.bottom ) + return false; + + // Now let's calculate the horizontal alignment (left and width). + if ( this.inline ) + styles.left = rel.elementRect.left; + else { + if ( rel.elementRect.left > 0 ) + styles.left = this.rect.left + rel.elementRect.left; + + // H-scroll case. Left edge of element may be out of viewport. + else { + styles.width += rel.elementRect.left; + styles.left = this.rect.left; + } + + // H-scroll case. Right edge of element may be out of viewport. + if ( ( hdiff = styles.left + styles.width - ( this.rect.left + this.winPane.width ) ) > 0 ) + styles.width -= hdiff; + } + + // Finally include horizontal scroll of the global window. + styles.left += this.winTopScroll.x; + + // Append 'px' to style values. + for ( var style in styles ) + styles[ style ] = CKEDITOR.tools.cssLength( styles[ style ] ); + + return styles; + }, + + /** + * Adds a new line to DOM. + * + * @returns {CKEDITOR.dom.element} A brand-new line. + */ + addLine: function() { + var line = CKEDITOR.dom.element.createFromHtml( this.lineTpl ); + + line.appendTo( this.container ); + + return line; + }, + + /** + * Assigns an unique hash to the instance that is later utilized + * to tell unwanted lines from new ones. This method **must** be called + * before a new set of relations is to be visualized so {@link #cleanup} + * eventually hides obsolete lines. This is because lines + * are re-used between {@link #placeLine} calls and the number of + * necessary ones may vary according to the number of relations. + * + * @param {Object} relations {@link CKEDITOR.plugins.lineutils.finder#relations}. + * @param {Object} locations {@link CKEDITOR.plugins.lineutils.locator#locations}. + */ + prepare: function( relations, locations ) { + this.relations = relations; + this.locations = locations; + this.hash = Math.random(); + }, + + /** + * Hides all visible lines that don't belong to current hash + * and no-longer represent relations (locations). + * + * See also: {@link #prepare}. + */ + cleanup: function() { + var line; + + for ( var l in this.visible ) { + line = this.visible[ l ]; + + if ( line.getCustomData( 'hash' ) !== this.hash ) + this.hideLine( line ); + } + }, + + /** + * Queries dimensions of the viewport, editable, frame etc. + * that are used for correct positioning of the line. + */ + queryViewport: function() { + this.winPane = this.win.getViewPaneSize(); + this.winTopScroll = this.winTop.getScrollPosition(); + this.winTopPane = this.winTop.getViewPaneSize(); + + if ( this.inline ) + this.rect = this.editable.getClientRect(); + else + this.rect = this.frame.getClientRect(); + } + }; + + function is( type, flag ) { + return type & flag; + } + + var floats = { left: 1, right: 1, center: 1 }, + positions = { absolute: 1, fixed: 1 }; + + function isElement( node ) { + return node && node.type == CKEDITOR.NODE_ELEMENT; + } + + function isFloated( el ) { + return !!( floats[ el.getComputedStyle( 'float' ) ] || floats[ el.getAttribute( 'align' ) ] ); + } + + function isPositioned( el ) { + return !!positions[ el.getComputedStyle( 'position' ) ]; + } + + function isLimit( node ) { + return isElement( node ) && node.getAttribute( 'contenteditable' ) == 'true'; + } + + function isStatic( node ) { + return isElement( node ) && !isFloated( node ) && !isPositioned( node ); + } + + /** + * Global namespace holding definitions and global helpers for the lineutils plugin. + * + * @private + * @class + * @singleton + * @since 4.3 + */ + CKEDITOR.plugins.lineutils = { + finder: Finder, + locator: Locator, + liner: Liner + }; +} )(); \ No newline at end of file diff --git a/htmlarea/ckeditor/ckeditor/plugins/procurator/images/procurator.png b/htmlarea/ckeditor/ckeditor/plugins/procurator/images/procurator.png new file mode 100644 index 00000000..b803e9de Binary files /dev/null and b/htmlarea/ckeditor/ckeditor/plugins/procurator/images/procurator.png differ diff --git a/htmlarea/ckeditor/ckeditor/plugins/procurator/plugin.js b/htmlarea/ckeditor/ckeditor/plugins/procurator/plugin.js new file mode 100644 index 00000000..d9f4d4f2 --- /dev/null +++ b/htmlarea/ckeditor/ckeditor/plugins/procurator/plugin.js @@ -0,0 +1,169 @@ +/** + * @license Copyright (c) from 2013, Author: Ian. All rights reserved. + */ + +/** + * @fileOverview A Serendipity wysiwyg-mode-placeholder plugin: procurator, v. 1.4 + */ + +(function (pluginName) { + var pluginName = 'procurator'; + + function createFakeElement(editor, realElement, displayName) { + + if (!displayName) var displayName = ''; + + // API createFakeParserElement(realElement, className, realElementType, isResizable) - realElementType can be flash,iframe,and? (these set their own title attribute), + var fakeElement = editor.createFakeParserElement( realElement, 'cke_procurator', 'iframe', false ), fakeStyle = fakeElement.attributes.style || ''; + // set the needed [img object] attributes + //fakeElement.attributes[ 'align' ] = 'right'; // '' is default, uncomment to default + fakeElement.attributes[ 'title' ] = 'Serendipity "' + realElement.name + '" tag placeholder'; + fakeElement.attributes[ 'alt' ] = realElement.name; + fakeElement.attributes[ 'placeholder' ] = realElement.name; + //fakeElement.attributes[ 'data-cke-real-element-type' ] = "div"; + //fakeElement.attributes[ 'contenteditable'] = "false"; // disabled, since CKEDITOR 4.3.2 must have had changed something, that the mediainsert block was replaced by an img procurator/placeholder code + //fakeElement.attributes['src'] = realElement.name+'.png'; // could be used to add an specific element name placeholder, else the /plugins/fakeobjects/images/spacer.gif is used and overruled by addCss + //console.log(fakeElement); + return fakeElement; + } + + function thisFakeElement(editor, fakeWrapper, realData, elObject) { + var fakeElement = createFakeElement(editor, fakeWrapper); + fakeElement.attributes['data-cke-realelement'] = realData; + //fakeElement.attributes['data-cke-real-element-type' ] = "div"; + if ( fakeElement ) { + delete elObject.value; + return fakeElement; + } + return elObject.value; + } + + CKEDITOR.plugins.add( pluginName, { + requires: ['fakeobjects'], + + onLoad: function() { + // CHANGES IN CKEDITOR 4 + // The "additional CSS" feature provided by CKEDITOR.editor#addCss has moved to a global CKEDITOR.addCss, with specified style rules applies document wide. + // Thus the proper way for a plugin to style it's editable content is to call CKEDITOR.addCss inside of the plugin's onLoad function, rather than it's init function in v3. + // Adds a piece of CSS code to the editor, which will be applied to the WYSIWYG editing document. This CSS would not be added to the output, and is there mainly for editor-specific editing requirements. + // Note: This function should be called before the editor is loaded to take effect. + CKEDITOR.addCss( + '.cke_procurator' + + '{' + + 'background-color: #FAFAFA;' + +// 'background-image: url(' + CKEDITOR.getUrl( this.path + 'images/procurator.png' ) + ');' +#a9a9a9 + 'background-image: url(' + CKEDITOR.tools.transparentImageData + ');' + + 'background-position: center center;' + + 'background-repeat: no-repeat;' + + 'border: 1px solid #000f2d;' + + 'border-radius: 0.3em;' + +// 'width: 220px;' + + 'height: 37px;' + + 'margin: auto 0.5em;' + + 'box-shadow: 0px 0px 5px 0px #06F;' + + 'z-index: 1;' + + '}' + ); + }, + + init: function (editor) { + // This happens on switch wysiwyg-mode to source view only and is used to remove special wysiwyg changes to the source + editor.on( 'mode', function() { + if ( editor.mode == 'source' ) { + var source = editor.getData(); + source = source.replace( /[\t]/g, ' '); // replace ckeditor added tabulators with 4 spaces + source = source.replace( /&quot;/g, '"'); // replace smarty tags quote to " switching wysiwyg to source view + source = source.replace( /&#39;/g, "'"); // replace smarty tags quote to ' switching wysiwyg to source view + //source = source.replace( /\r?\n|\r/gm, ""); // replace all newlines for + // set data back into source mode textarea + editor.setData(source); + } + }); + }, + + afterInit: function (editor) { + // Special script tags work by default, without any changes (commenting out the script to protected replavements) in ckeditor.js file, + // but we can not access (fake and replace with placeholders) these comments via the dataFilter rule element. + // This is why we have to fake it by comment dataFilter rule first + + // The dataProcessor handles data coming in to the editor and out from the editor. + var dataProcessor = editor.dataProcessor; + + // The dataProcessor.dataFilter handles incoming data, like pasting: + // filter applied to the input data when transforming it to HTML to be loaded into the editor ("on input"). + // Ckeditor in this state is enabled. + var dataFilter = dataProcessor && dataProcessor.dataFilter; + + // The dataProcessor.htmlFilter handles outgoing data, like saving, viewing the source button, or generally calling updateElement on the editor: + // filter applied to the HTML available in the editor when transforming it on the XHTML outputted by the editor ("on output"). + // If you have a rule within the htmlFilter that strips out the href attribute, then on save it will be stripped. + // Ckeditor in this state is disabled. + // var htmlFilter = dataProcessor && dataProcessor.htmlFilter; + + // var writer = editor.dataProcessor.writer; + // The way to close self closing tags inside add rules, like + // writer.selfClosingEnd = '>'; + + if (dataFilter) { + // Here we want to add a new filter rule... if the source matches this property, it will be converted + // Note the num at the end, which defines the filter priority. Higher number = higher Priority. + dataFilter.addRules({ + // comment reads html comments and adds fakes to the income-data to be displayed in wysiwyg-mode, + // which was already filtered by ckeditor - see script, noscript (and php?) tags as example + comment : function( elString, elObject ) { + var trimedStr = elString.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); // trim whitespaces at start and end + var cutstr = trimedStr.split(':'); //cut by ':' - match by array + var displayName = 'protected'; + var protectedSourceMarker = '{cke_protected}'; + + // this is case {cke_protected}{C} data + if (cutstr[0].match('quickblog')) { + var realData = ''; // return the original snipped to preserve + var fakeWrapper = new CKEDITOR.htmlParser.element( displayName ); // creates the new object + fakeWrapper.name = cutstr[0]; + elObject.value = thisFakeElement(editor, fakeWrapper, realData, elObject); + } + // this is case {cke_protected} data + if ( elString.substr( 0, protectedSourceMarker.length ) == protectedSourceMarker ) { + var realData = elString.replace(/\{cke_protected\}([\s\S]+?)/g, "$1"); // cuts the snippet from {cke_protected}(.*) to preserve + // < .cke_widget_element{' + + 'outline:2px solid yellow;' + + 'cursor:default' + + '}' + + '.cke_widget_wrapper:hover .cke_widget_editable{' + + 'outline:2px solid yellow' + + '}' + + '.cke_widget_wrapper.cke_widget_focused>.cke_widget_element,' + + // We need higher specificity than hover style. + '.cke_widget_wrapper .cke_widget_editable.cke_widget_editable_focused{' + + 'outline:2px solid #ace' + + '}' + + '.cke_widget_editable{' + + 'cursor:text' + + '}' + + '.cke_widget_drag_handler_container{' + + 'position:absolute;' + + 'width:' + DRAG_HANDLER_SIZE + 'px;' + + 'height:0;' + + // Initially drag handler should not be visible, until its position will be + // repositioned. #11177 + 'left:-9999px;' + + 'opacity:0.75;' + + 'transition:height 0s 0.2s;' + // Delay hiding drag handler. + // Prevent drag handler from being misplaced (#11198). + 'line-height:0' + + '}' + + '.cke_widget_wrapper:hover>.cke_widget_drag_handler_container{' + + 'height:' + DRAG_HANDLER_SIZE + 'px;' + + 'transition:none' + + '}' + + '.cke_widget_drag_handler_container:hover{' + + 'opacity:1' + + '}' + + 'img.cke_widget_drag_handler{' + + 'cursor:move;' + + 'width:' + DRAG_HANDLER_SIZE + 'px;' + + 'height:' + DRAG_HANDLER_SIZE + 'px;' + + 'display:inline-block' + + '}' + + '.cke_widget_mask{' + + 'position:absolute;' + + 'top:0;' + + 'left:0;' + + 'width:100%;' + + 'height:100%;' + + 'display:block' + + '}' + + '.cke_editable.cke_widget_dragging, .cke_editable.cke_widget_dragging *{' + + 'cursor:move !important' + + '}' + ); + }, + + beforeInit: function( editor ) { + /** + * An instance of widget repository. It contains all + * {@link CKEDITOR.plugins.widget.repository#registered registered widget definitions} and + * {@link CKEDITOR.plugins.widget.repository#instances initialized instances}. + * + * editor.widgets.add( 'someName', { + * // Widget definition... + * } ); + * + * editor.widgets.registered.someName; // -> Widget definition + * + * @since 4.3 + * @readonly + * @property {CKEDITOR.plugins.widget.repository} widgets + * @member CKEDITOR.editor + */ + editor.widgets = new Repository( editor ); + }, + + afterInit: function( editor ) { + addWidgetButtons( editor ); + setupContextMenu( editor ); + } + } ); + + /** + * Widget repository. It keeps track of all {@link #registered registered widget definitions} and + * {@link #instances initialized instances}. An instance of the repository is available under + * the {@link CKEDITOR.editor#widgets} property. + * + * @class CKEDITOR.plugins.widget.repository + * @mixins CKEDITOR.event + * @constructor Creates a widget repository instance. Note that the widget plugin automatically + * creates a repository instance which is available under the {@link CKEDITOR.editor#widgets} property. + * @param {CKEDITOR.editor} editor The editor instance for which the repository will be created. + */ + function Repository( editor ) { + /** + * The editor instance for which this repository was created. + * + * @readonly + * @property {CKEDITOR.editor} editor + */ + this.editor = editor; + + /** + * A hash of registered widget definitions (definition name => {@link CKEDITOR.plugins.widget.definition}). + * + * To register a definition use the {@link #add} method. + * + * @readonly + */ + this.registered = {}; + + /** + * An object containing initialized widget instances (widget id => {@link CKEDITOR.plugins.widget}). + * + * @readonly + */ + this.instances = {}; + + /** + * An array of selected widget instances. + * + * @readonly + * @property {CKEDITOR.plugins.widget[]} selected + */ + this.selected = []; + + /** + * The focused widget instance. See also {@link CKEDITOR.plugins.widget#event-focus} + * and {@link CKEDITOR.plugins.widget#event-blur} events. + * + * editor.on( 'selectionChange', function() { + * if ( editor.widgets.focused ) { + * // Do something when a widget is focused... + * } + * } ); + * + * @readonly + * @property {CKEDITOR.plugins.widget} focused + */ + this.focused = null; + + /** + * The widget instance that contains the nested editable which is currently focused. + * + * @readonly + * @property {CKEDITOR.plugins.widget} widgetHoldingFocusedEditable + */ + this.widgetHoldingFocusedEditable = null; + + this._ = { + nextId: 0, + upcasts: [], + upcastCallbacks: [], + filters: {} + }; + + setupWidgetsLifecycle( this ); + setupSelectionObserver( this ); + setupMouseObserver( this ); + setupKeyboardObserver( this ); + setupDragAndDrop( this ); + setupNativeCutAndCopy( this ); + } + + Repository.prototype = { + /** + * Minimum interval between selection checks. + * + * @private + */ + MIN_SELECTION_CHECK_INTERVAL: 500, + + /** + * Adds a widget definition to the repository. Fires the {@link CKEDITOR.editor#widgetDefinition} event + * which allows to modify the widget definition which is going to be registered. + * + * @param {String} name The name of the widget definition. + * @param {CKEDITOR.plugins.widget.definition} widgetDef Widget definition. + * @returns {CKEDITOR.plugins.widget.definition} + */ + add: function( name, widgetDef ) { + // Create prototyped copy of original widget definition, so we won't modify it. + widgetDef = CKEDITOR.tools.prototypedCopy( widgetDef ); + widgetDef.name = name; + + widgetDef._ = widgetDef._ || {}; + + this.editor.fire( 'widgetDefinition', widgetDef ); + + if ( widgetDef.template ) + widgetDef.template = new CKEDITOR.template( widgetDef.template ); + + addWidgetCommand( this.editor, widgetDef ); + addWidgetProcessors( this, widgetDef ); + + this.registered[ name ] = widgetDef; + + return widgetDef; + }, + + /** + * Adds a callback for element upcasting. Each callback will be executed + * for every element which is later tested by upcast methods. If a callback + * returns `false`, the element will not be upcasted. + * + * // Images with the "banner" class will not be upcasted (e.g. to the image widget). + * editor.widgets.addUpcastCallback( function( element ) { + * if ( element.name == 'img' && element.hasClass( 'banner' ) ) + * return false; + * } ); + * + * @param {Function} callback + * @param {CKEDITOR.htmlParser.element} callback.element + */ + addUpcastCallback: function( callback ) { + this._.upcastCallbacks.push( callback ); + }, + + /** + * Checks the selection to update widget states (selection and focus). + * + * This method is triggered by the {@link #event-checkSelection} event. + */ + checkSelection: function() { + var sel = this.editor.getSelection(), + selectedElement = sel.getSelectedElement(), + updater = stateUpdater( this ), + widget; + + // Widget is focused so commit and finish checking. + if ( selectedElement && ( widget = this.getByElement( selectedElement, true ) ) ) + return updater.focus( widget ).select( widget ).commit(); + + var range = sel.getRanges()[ 0 ]; + + // No ranges or collapsed range mean that nothing is selected, so commit and finish checking. + if ( !range || range.collapsed ) + return updater.commit(); + + // Range is not empty, so create walker checking for wrappers. + var walker = new CKEDITOR.dom.walker( range ), + wrapper; + + walker.evaluator = isDomWidgetWrapper; + + while ( ( wrapper = walker.next() ) ) + updater.select( this.getByElement( wrapper ) ); + + updater.commit(); + }, + + /** + * Checks if all widget instances are still present in the DOM. + * Destroys those instances that are not present. + * Reinitializes widgets on widget wrappers for which widget instances + * cannot be found. + * + * This method triggers the {@link #event-checkWidgets} event whose listeners + * can cancel the method's execution or modify its options. + * + * @param [options] The options object. + * @param {Boolean} [options.initOnlyNew] Initializes widgets only on newly wrapped + * widget elements (those which still have the `cke_widget_new` class). When this option is + * set to `true`, widgets which were invalidated (e.g. by replacing with a cloned DOM structure) + * will not be reinitialized. This makes the check faster. + * @param {Boolean} [options.focusInited] If only one widget is initialized by + * the method, it will be focused. + */ + checkWidgets: function( options ) { + this.fire( 'checkWidgets', CKEDITOR.tools.copy( options || {} ) ); + }, + + /** + * Removes the widget from the editor and moves the selection to the closest + * editable position if the widget was focused before. + * + * @param {CKEDITOR.plugins.widget} widget The widget instance to be deleted. + */ + del: function( widget ) { + if ( this.focused === widget ) { + var editor = widget.editor, + range = editor.createRange(), + found; + + // If haven't found place for caret on the default side, + // try to find it on the other side. + if ( !( found = range.moveToClosestEditablePosition( widget.wrapper, true ) ) ) + found = range.moveToClosestEditablePosition( widget.wrapper, false ); + + if ( found ) + editor.getSelection().selectRanges( [ range ] ); + } + + widget.wrapper.remove(); + this.destroy( widget, true ); + }, + + /** + * Destroys the widget instance. + * + * @param {CKEDITOR.plugins.widget} widget The widget instance to be destroyed. + * @param {Boolean} [offline] Whether the widget is offline (detached from the DOM tree) — + * in this case the DOM (attributes, classes, etc.) will not be cleaned up. + */ + destroy: function( widget, offline ) { + if ( this.widgetHoldingFocusedEditable === widget ) + setFocusedEditable( this, widget, null, offline ); + + widget.destroy( offline ); + delete this.instances[ widget.id ]; + this.fire( 'instanceDestroyed', widget ); + }, + + /** + * Destroys all widget instances. + * + * @param {Boolean} [offline] Whether the widgets are offline (detached from the DOM tree) — + * in this case the DOM (attributes, classes, etc.) will not be cleaned up. + */ + destroyAll: function( offline ) { + var instances = this.instances, + widget; + + for ( var id in instances ) { + widget = instances[ id ]; + this.destroy( widget, offline ); + } + }, + + /** + * Finalizes a process of widget creation. This includes: + * + * * inserting widget element into editor, + * * marking widget instance as ready (see {@link CKEDITOR.plugins.widget#event-ready}), + * * focusing widget instance. + * + * This method is used by the default widget's command and is called + * after widget's dialog (if set) is closed. It may also be used in a + * customized process of widget creation and insertion. + * + * widget.once( 'edit', function() { + * // Finalize creation only of not ready widgets. + * if ( widget.isReady() ) + * return; + * + * // Cancel edit event to prevent automatic widget insertion. + * evt.cancel(); + * + * CustomDialog.open( widget.data, function saveCallback( savedData ) { + * // Cache the container, because widget may be destroyed while saving data, + * // if this process will require some deep transformations. + * var container = widget.wrapper.getParent(); + * + * widget.setData( savedData ); + * + * // Widget will be retrieved from container and inserted into editor. + * editor.widgets.finalizeCreation( container ); + * } ); + * } ); + * + * @param {CKEDITOR.dom.element/CKEDITOR.dom.documentFragment} container The element + * or document fragment which contains widget wrapper. The container is used, so before + * finalizing creation the widget can be freely transformed (even destroyed and reinitialized). + */ + finalizeCreation: function( container ) { + var wrapper = container.getFirst(); + if ( wrapper && isDomWidgetWrapper( wrapper ) ) { + this.editor.insertElement( wrapper ); + + var widget = this.getByElement( wrapper ); + // Fire postponed #ready event. + widget.ready = true; + widget.fire( 'ready' ); + widget.focus(); + } + }, + + /** + * Finds a widget instance which contains a given element. The element will be the {@link CKEDITOR.plugins.widget#wrapper wrapper} + * of the returned widget or a descendant of this {@link CKEDITOR.plugins.widget#wrapper wrapper}. + * + * editor.widgets.getByElement( someWidget.wrapper ); // -> someWidget + * editor.widgets.getByElement( someWidget.parts.caption ); // -> someWidget + * + * // Check wrapper only: + * editor.widgets.getByElement( someWidget.wrapper, true ); // -> someWidget + * editor.widgets.getByElement( someWidget.parts.caption, true ); // -> null + * + * @param {CKEDITOR.dom.element} element The element to be checked. + * @param {Boolean} [checkWrapperOnly] If set to `true`, the method will not check wrappers' descendants. + * @returns {CKEDITOR.plugins.widget} The widget instance or `null`. + */ + getByElement: ( function() { + var validWrapperElements = { div: 1, span: 1 }; + function getWidgetId( element ) { + return element.is( validWrapperElements ) && element.data( 'cke-widget-id' ); + } + + return function( element, checkWrapperOnly ) { + if ( !element ) + return null; + + var id = getWidgetId( element ); + + // There's no need to check element parents if element is a wrapper. + if ( !checkWrapperOnly && !id ) { + var limit = this.editor.editable(); + + // Try to find a closest ascendant which is a widget wrapper. + do { + element = element.getParent(); + } while ( element && !element.equals( limit ) && !( id = getWidgetId( element ) ) ); + } + + return this.instances[ id ] || null; + }; + } )(), + + /** + * Initializes a widget on a given element if the widget has not been initialized on it yet. + * + * @param {CKEDITOR.dom.element} element The future widget element. + * @param {String/CKEDITOR.plugins.widget.definition} [widgetDef] Name of a widget or a widget definition. + * The widget definition should be previously registered by using the + * {@link CKEDITOR.plugins.widget.repository#add} method. + * @param [startupData] Widget startup data (has precedence over default one). + * @returns {CKEDITOR.plugins.widget} The widget instance or `null` if a widget could not be initialized on + * a given element. + */ + initOn: function( element, widgetDef, startupData ) { + if ( !widgetDef ) + widgetDef = this.registered[ element.data( 'widget' ) ]; + else if ( typeof widgetDef == 'string' ) + widgetDef = this.registered[ widgetDef ]; + + if ( !widgetDef ) + return null; + + // Wrap element if still wasn't wrapped (was added during runtime by method that skips dataProcessor). + var wrapper = this.wrapElement( element, widgetDef.name ); + + if ( wrapper ) { + // Check if widget wrapper is new (widget hasn't been initialized on it yet). + // This class will be removed by widget constructor to avoid locking snapshot twice. + if ( wrapper.hasClass( 'cke_widget_new' ) ) { + var widget = new Widget( this, this._.nextId++, element, widgetDef, startupData ); + + // Widget could be destroyed when initializing it. + if ( widget.isInited() ) { + this.instances[ widget.id ] = widget; + + return widget; + } else { + return null; + } + } + + // Widget already has been initialized, so try to get widget by element. + // Note - it may happen that other instance will returned than the one created above, + // if for example widget was destroyed and reinitialized. + return this.getByElement( element ); + } + + // No wrapper means that there's no widget for this element. + return null; + }, + + /** + * Initializes widgets on all elements which were wrapped by {@link #wrapElement} and + * have not been initialized yet. + * + * @param {CKEDITOR.dom.element} [container=editor.editable()] The container which will be checked for not + * initialized widgets. Defaults to editor's {@link CKEDITOR.editor#editable editable} element. + * @returns {CKEDITOR.plugins.widget[]} Array of widget instances which have been initialized. + * Note: Only first-level widgets are returned — without nested widgets. + */ + initOnAll: function( container ) { + var newWidgets = ( container || this.editor.editable() ).find( '.cke_widget_new' ), + newInstances = [], + instance; + + for ( var i = newWidgets.count(); i--; ) { + instance = this.initOn( newWidgets.getItem( i ).getFirst( isDomWidgetElement ) ); + if ( instance ) + newInstances.push( instance ); + } + + return newInstances; + }, + + /** + * Parses element classes string and returns an object + * whose keys contain class names. Skips all `cke_*` classes. + * + * This method is used by the {@link CKEDITOR.plugins.widget#getClasses} method and + * may be used when overriding that method. + * + * @since 4.4 + * @param {String} classes String (value of `class` attribute). + * @returns {Object} Object containing classes or `null` if no classes found. + */ + parseElementClasses: function( classes ) { + if ( !classes ) + return null; + + classes = CKEDITOR.tools.trim( classes ).split( /\s+/ ); + + var cl, + obj = {}, + hasClasses = 0; + + while ( ( cl = classes.pop() ) ) { + if ( cl.indexOf( 'cke_' ) == -1 ) + obj[ cl ] = hasClasses = 1; + } + + return hasClasses ? obj : null; + }, + + /** + * Wraps an element with a widget's non-editable container. + * + * If this method is called on an {@link CKEDITOR.htmlParser.element}, then it will + * also take care of fixing the DOM after wrapping (the wrapper may not be allowed in element's parent). + * + * @param {CKEDITOR.dom.element/CKEDITOR.htmlParser.element} element The widget element to be wrapped. + * @param {String} [widgetName] The name of the widget definition. Defaults to element's `data-widget` + * attribute value. + * @returns {CKEDITOR.dom.element/CKEDITOR.htmlParser.element} The wrapper element or `null` if + * the widget definition of this name is not registered. + */ + wrapElement: function( element, widgetName ) { + var wrapper = null, + widgetDef, + isInline; + + if ( element instanceof CKEDITOR.dom.element ) { + widgetDef = this.registered[ widgetName || element.data( 'widget' ) ]; + if ( !widgetDef ) + return null; + + // Do not wrap already wrapped element. + wrapper = element.getParent(); + if ( wrapper && wrapper.type == CKEDITOR.NODE_ELEMENT && wrapper.data( 'cke-widget-wrapper' ) ) + return wrapper; + + // If attribute isn't already set (e.g. for pasted widget), set it. + if ( !element.hasAttribute( 'data-cke-widget-keep-attr' ) ) + element.data( 'cke-widget-keep-attr', element.data( 'widget' ) ? 1 : 0 ); + if ( widgetName ) + element.data( 'widget', widgetName ); + + isInline = isWidgetInline( widgetDef, element.getName() ); + + wrapper = new CKEDITOR.dom.element( isInline ? 'span' : 'div' ); + wrapper.setAttributes( getWrapperAttributes( isInline ) ); + + wrapper.data( 'cke-display-name', widgetDef.pathName ? widgetDef.pathName : element.getName() ); + + // Replace element unless it is a detached one. + if ( element.getParent( true ) ) + wrapper.replace( element ); + element.appendTo( wrapper ); + } + else if ( element instanceof CKEDITOR.htmlParser.element ) { + widgetDef = this.registered[ widgetName || element.attributes[ 'data-widget' ] ]; + if ( !widgetDef ) + return null; + + wrapper = element.parent; + if ( wrapper && wrapper.type == CKEDITOR.NODE_ELEMENT && wrapper.attributes[ 'data-cke-widget-wrapper' ] ) + return wrapper; + + // If attribute isn't already set (e.g. for pasted widget), set it. + if ( !( 'data-cke-widget-keep-attr' in element.attributes ) ) + element.attributes[ 'data-cke-widget-keep-attr' ] = element.attributes[ 'data-widget' ] ? 1 : 0; + if ( widgetName ) + element.attributes[ 'data-widget' ] = widgetName; + + isInline = isWidgetInline( widgetDef, element.name ); + + wrapper = new CKEDITOR.htmlParser.element( isInline ? 'span' : 'div', getWrapperAttributes( isInline ) ); + + wrapper.attributes[ 'data-cke-display-name' ] = widgetDef.pathName ? widgetDef.pathName : element.name; + + var parent = element.parent, + index; + + // Don't detach already detached element. + if ( parent ) { + index = element.getIndex(); + element.remove(); + } + + wrapper.add( element ); + + // Insert wrapper fixing DOM (splitting parents if wrapper is not allowed inside them). + parent && insertElement( parent, index, wrapper ); + } + + return wrapper; + }, + + // Expose for tests. + _tests_getNestedEditable: getNestedEditable, + _tests_createEditableFilter: createEditableFilter + }; + + CKEDITOR.event.implementOn( Repository.prototype ); + + /** + * An event fired when a widget instance is created, but before it is fully initialized. + * + * @event instanceCreated + * @param {CKEDITOR.plugins.widget} data The widget instance. + */ + + /** + * An event fired when a widget instance was destroyed. + * + * See also {@link CKEDITOR.plugins.widget#event-destroy}. + * + * @event instanceDestroyed + * @param {CKEDITOR.plugins.widget} data The widget instance. + */ + + /** + * An event fired to trigger the selection check. + * + * See the {@link #method-checkSelection} method. + * + * @event checkSelection + */ + + /** + * An event fired by the the {@link #method-checkWidgets} method. + * + * It can be canceled in order to stop the {@link #method-checkWidgets} + * method execution or the event listener can modify the method's options. + * + * @event checkWidgets + * @param [data] + * @param {Boolean} [data.initOnlyNew] Initialize widgets only on newly wrapped + * widget elements (those which still have the `cke_widget_new` class). When this option is + * set to `true`, widgets which were invalidated (e.g. by replacing with a cloned DOM structure) + * will not be reinitialized. This makes the check faster. + * @param {Boolean} [data.focusInited] If only one widget is initialized by + * the method, it will be focused. + */ + + + /** + * An instance of a widget. Together with {@link CKEDITOR.plugins.widget.repository} these + * two classes constitute the core of the Widget System. + * + * Note that neither the repository nor the widget instances can be created by using their constructors. + * A repository instance is automatically set up by the Widget plugin and is accessible under + * {@link CKEDITOR.editor#widgets}, while widget instances are created and destroyed by the repository. + * + * To create a widget, first you need to {@link CKEDITOR.plugins.widget.repository#add register} its + * {@link CKEDITOR.plugins.widget.definition definition}: + * + * editor.widgets.add( 'simplebox', { + * upcast: function( element ) { + * // Defines which elements will become widgets. + * if ( element.hasClass( 'simplebox' ) ) + * return true; + * }, + * init: function() { + * // ... + * } + * } ); + * + * Once the widget definition is registered, widgets will be automatically + * created when loading data: + * + * editor.setData( '
foo
', function() { + * console.log( editor.widgets.instances ); // -> An object containing one instance. + * } ); + * + * It is also possible to create instances during runtime by using a command + * (if a {@link CKEDITOR.plugins.widget.definition#template} property was defined): + * + * // You can execute an automatically defined command to + * // insert a new simplebox widget or edit the one currently focused. + * editor.execCommand( 'simplebox' ); + * + * Or in a completely custom way: + * + * var element = editor.createElement( 'div' ); + * editor.insertElement( element ); + * var widget = editor.widgets.initOn( element, 'simplebox' ); + * + * @since 4.3 + * @class CKEDITOR.plugins.widget + * @mixins CKEDITOR.event + * @extends CKEDITOR.plugins.widget.definition + * @constructor Creates an instance of the widget class. Do not use it directly, but instead initialize widgets + * by using the {@link CKEDITOR.plugins.widget.repository#initOn} method or by the upcasting system. + * @param {CKEDITOR.plugins.widget.repository} widgetsRepo + * @param {Number} id Unique ID of this widget instance. + * @param {CKEDITOR.dom.element} element The widget element. + * @param {CKEDITOR.plugins.widget.definition} widgetDef Widget's registered definition. + * @param [startupData] Initial widget data. This data object will overwrite the default data and + * the data loaded from the DOM. + */ + function Widget( widgetsRepo, id, element, widgetDef, startupData ) { + var editor = widgetsRepo.editor; + + // Extend this widget with widgetDef-specific methods and properties. + CKEDITOR.tools.extend( this, widgetDef, { + /** + * The editor instance. + * + * @readonly + * @property {CKEDITOR.editor} + */ + editor: editor, + + /** + * This widget's unique (per editor instance) ID. + * + * @readonly + * @property {Number} + */ + id: id, + + /** + * Whether this widget is an inline widget (based on an inline element unless + * forced otherwise by {@link CKEDITOR.plugins.widget.definition#inline}). + * + * **Note:** This option does not allow to turn a block element into an inline widget. + * However, it makes it possible to turn an inline element into a block widget or to + * force a correct type in case when automatic recognition fails. + * + * @readonly + * @property {Boolean} + */ + inline: element.getParent().getName() == 'span', + + /** + * The widget element — the element on which the widget was initialized. + * + * @readonly + * @property {CKEDITOR.dom.element} element + */ + element: element, + + /** + * Widget's data object. + * + * The data can only be set by using the {@link #setData} method. + * Changes made to the data fire the {@link #event-data} event. + * + * @readonly + */ + data: CKEDITOR.tools.extend( {}, typeof widgetDef.defaults == 'function' ? widgetDef.defaults() : widgetDef.defaults ), + + /** + * Indicates if a widget is data-ready. Set to `true` when data from all sources + * ({@link CKEDITOR.plugins.widget.definition#defaults}, set in the + * {@link #init} method, loaded from the widget's element and startup data coming from the constructor) + * are finally loaded. This is immediately followed by the first {@link #event-data}. + * + * @readonly + */ + dataReady: false, + + /** + * Whether a widget instance was initialized. This means that: + * + * * An instance was created, + * * Its properties were set, + * * The `init` method was executed. + * + * **Note**: The first {@link #event-data} event could not be fired yet which + * means that the widget's DOM has not been set up yet. Wait for the {@link #event-ready} + * event to be notified when a widget is fully initialized and ready. + * + * **Note**: Use the {@link #isInited} method to check whether a widget is initialized and + * has not been destroyed. + * + * @readonly + */ + inited: false, + + /** + * Whether a widget instance is ready. This means that the widget is {@link #inited} and + * that its DOM was finally set up. + * + * **Note:** Use the {@link #isReady} method to check whether a widget is ready and + * has not been destroyed. + * + * @readonly + */ + ready: false, + + // Revert what widgetDef could override (automatic #edit listener). + edit: Widget.prototype.edit, + + /** + * The nested editable element which is currently focused. + * + * @readonly + * @property {CKEDITOR.plugins.widget.nestedEditable} + */ + focusedEditable: null, + + /** + * The widget definition from which this instance was created. + * + * @readonly + * @property {CKEDITOR.plugins.widget.definition} definition + */ + definition: widgetDef, + + /** + * Link to the widget repository which created this instance. + * + * @readonly + * @property {CKEDITOR.plugins.widget.repository} repository + */ + repository: widgetsRepo, + + draggable: widgetDef.draggable !== false, + + // WAAARNING: Overwrite widgetDef's priv object, because otherwise violent unicorn's gonna visit you. + _: { + downcastFn: ( widgetDef.downcast && typeof widgetDef.downcast == 'string' ) ? + widgetDef.downcasts[ widgetDef.downcast ] : widgetDef.downcast + } + }, true ); + + /** + * An object of widget component elements. + * + * For every `partName => selector` pair in {@link CKEDITOR.plugins.widget.definition#parts}, + * one `partName => element` pair is added to this object during the widget initialization. + * + * @readonly + * @property {Object} parts + */ + + /** + * The template which will be used to create a new widget element (when the widget's command is executed). + * It will be populated with {@link #defaults default values}. + * + * @readonly + * @property {CKEDITOR.template} template + */ + + /** + * The widget wrapper — a non-editable `div` or `span` element (depending on {@link #inline}) + * which is a parent of the {@link #element} and widget compontents like the drag handler and the {@link #mask}. + * It is the outermost widget element. + * + * @readonly + * @property {CKEDITOR.dom.element} wrapper + */ + + widgetsRepo.fire( 'instanceCreated', this ); + + setupWidget( this, widgetDef ); + + this.init && this.init(); + + // Finally mark widget as inited. + this.inited = true; + + setupWidgetData( this, startupData ); + + // If at some point (e.g. in #data listener) widget hasn't been destroyed + // and widget is already attached to document then fire #ready. + if ( this.isInited() && editor.editable().contains( this.wrapper ) ) { + this.ready = true; + this.fire( 'ready' ); + } + } + + Widget.prototype = { + /** + * Adds a class to the widget element. This method is used by + * the {@link #applyStyle} method and should be overriden by widgets + * which should handle classes differently (e.g. add them to other elements). + * + * **Note**: This method should not be used directly. Use the {@link #setData} method to + * set the `classes` property. Read more in the {@link #setData} documentation. + * + * See also: {@link #removeClass}, {@link #hasClass}, {@link #getClasses}. + * + * @since 4.4 + * @param {String} className The class name to be added. + */ + addClass: function( className ) { + this.element.addClass( className ); + }, + + /** + * Applies the specified style to the widget. It is highly recommended to use the + * {@link CKEDITOR.editor#applyStyle} or {@link CKEDITOR.style#apply} methods instead of + * using this method directly, because unlike editor's and style's methods, this one + * does not perform any checks. + * + * By default this method handles only classes defined in the style. It clones existing + * classes which are stored in the {@link #property-data widget data}'s `classes` property, + * adds new classes, and calls the {@link #setData} method if at least one new class was added. + * Then, using the {@link #event-data} event listener widget applies modifications passing + * new classes to the {@link #addClass} method. + * + * If you need to handle classes differently than in the default way, you can override the + * {@link #addClass} and related methods. You can also handle other style properties than `classes` + * by overriding this method. + * + * See also: {@link #checkStyleActive}, {@link #removeStyle}. + * + * @since 4.4 + * @param {CKEDITOR.style} style The custom widget style to be applied. + */ + applyStyle: function( style ) { + applyRemoveStyle( this, style, 1 ); + }, + + /** + * Checks if the specified style is applied to this widget. It is highly recommended to use the + * {@link CKEDITOR.style#checkActive} method instead of using this method directly, + * because unlike style's method, this one does not perform any checks. + * + * By default this method handles only classes defined in the style and passes + * them to the {@link #hasClass} method. You can override these methods to handle classes + * differently or to handle more of the style properties. + * + * See also: {@link #applyStyle}, {@link #removeStyle}. + * + * @since 4.4 + * @param {CKEDITOR.style} style The custom widget style to be checked. + * @returns {Boolean} Whether the style is applied to this widget. + */ + checkStyleActive: function( style ) { + var classes = getStyleClasses( style ), + cl; + + if ( !classes ) + return false; + + while ( ( cl = classes.pop() ) ) { + if ( !this.hasClass( cl ) ) + return false; + } + return true; + }, + + /** + * Destroys this widget instance. + * + * Use {@link CKEDITOR.plugins.widget.repository#destroy} when possible instead of this method. + * + * This method fires the {#event-destroy} event. + * + * @param {Boolean} [offline] Whether a widget is offline (detached from the DOM tree) — + * in this case the DOM (attributes, classes, etc.) will not be cleaned up. + */ + destroy: function( offline ) { + this.fire( 'destroy' ); + + if ( this.editables ) { + for ( var name in this.editables ) + this.destroyEditable( name, offline ); + } + + if ( !offline ) { + if ( this.element.data( 'cke-widget-keep-attr' ) == '0' ) + this.element.removeAttribute( 'data-widget' ); + this.element.removeAttributes( [ 'data-cke-widget-data', 'data-cke-widget-keep-attr' ] ); + this.element.removeClass( 'cke_widget_element' ); + this.element.replace( this.wrapper ); + } + + this.wrapper = null; + }, + + /** + * Destroys a nested editable. + * + * @param {String} editableName Nested editable name. + * @param {Boolean} [offline] See {@link #method-destroy} method. + */ + destroyEditable: function( editableName, offline ) { + var editable = this.editables[ editableName ]; + + editable.removeListener( 'focus', onEditableFocus ); + editable.removeListener( 'blur', onEditableBlur ); + this.editor.focusManager.remove( editable ); + + if ( !offline ) { + editable.removeClass( 'cke_widget_editable' ); + editable.removeClass( 'cke_widget_editable_focused' ); + editable.removeAttributes( [ 'contenteditable', 'data-cke-widget-editable', 'data-cke-enter-mode' ] ); + } + + delete this.editables[ editableName ]; + }, + + /** + * Starts widget editing. + * + * This method fires the {@link CKEDITOR.plugins.widget#event-edit} event + * which may be canceled in order to prevent it from opening a dialog window. + * + * The dialog window name is obtained from the event's data `dialog` property or + * from {@link CKEDITOR.plugins.widget.definition#dialog}. + * + * @returns {Boolean} Returns `true` if a dialog window was opened. + */ + edit: function() { + var evtData = { dialog: this.dialog }, + that = this; + + // Edit event was blocked, but there's no dialog to be automatically opened. + if ( this.fire( 'edit', evtData ) === false || !evtData.dialog ) + return false; + + this.editor.openDialog( evtData.dialog, function( dialog ) { + var showListener, + okListener; + + // Allow to add a custom dialog handler. + if ( that.fire( 'dialog', dialog ) === false ) + return; + + showListener = dialog.on( 'show', function() { + dialog.setupContent( that ); + } ); + + okListener = dialog.on( 'ok', function() { + // Commit dialog's fields, but prevent from + // firing data event for every field. Fire only one, + // bulk event at the end. + var dataChanged, + dataListener = that.on( 'data', function( evt ) { + dataChanged = 1; + evt.cancel(); + }, null, null, 0 ); + + // Create snapshot preceeding snapshot with changed widget... + // TODO it should not be required, but it is and I found similar + // code in dialog#ok listener in dialog/plugin.js. + that.editor.fire( 'saveSnapshot' ); + dialog.commitContent( that ); + + dataListener.removeListener(); + if ( dataChanged ) { + that.fire( 'data', that.data ); + that.editor.fire( 'saveSnapshot' ); + } + } ); + + dialog.once( 'hide', function() { + showListener.removeListener(); + okListener.removeListener(); + } ); + } ); + + return true; + }, + + /** + * Returns widget element classes parsed to an object. This method + * is used to populate the `classes` property of widget's {@link #property-data}. + * + * This method reuses {@link CKEDITOR.plugins.widget.repository#parseElementClasses}. + * It should be overriden if a widget should handle classes differently (e.g. on other elements). + * + * See also: {@link #removeClass}, {@link #addClass}, {@link #hasClass}. + * + * @since 4.4 + * @returns {Object} + */ + getClasses: function() { + return this.repository.parseElementClasses( this.element.getAttribute( 'class' ) ); + }, + + /** + * Checks if the widget element has specified class. This method is used by + * the {@link #checkStyleActive} method and should be overriden by widgets + * which should handle classes differently (e.g. on other elements). + * + * See also: {@link #removeClass}, {@link #addClass}, {@link #getClasses}. + * + * @since 4.4 + * @param {String} className The class to be checked. + * @param {Boolean} Whether a widget has specified class. + */ + hasClass: function( className ) { + return this.element.hasClass( className ); + }, + + /** + * Initializes a nested editable. + * + * **Note**: Only elements from {@link CKEDITOR.dtd#$editable} may become editables. + * + * @param {String} editableName The nested editable name. + * @param {CKEDITOR.plugins.widget.nestedEditable.definition} definition The definition of the nested editable. + * @returns {Boolean} Whether an editable was successfully initialized. + */ + initEditable: function( editableName, definition ) { + var editable = this.wrapper.findOne( definition.selector ); + + if ( editable && editable.is( CKEDITOR.dtd.$editable ) ) { + editable = new NestedEditable( this.editor, editable, { + filter: createEditableFilter.call( this.repository, this.name, editableName, definition ) + } ); + this.editables[ editableName ] = editable; + + editable.setAttributes( { + contenteditable: 'true', + 'data-cke-widget-editable': editableName, + 'data-cke-enter-mode': editable.enterMode + } ); + + if ( editable.filter ) + editable.data( 'cke-filter', editable.filter.id ); + + editable.addClass( 'cke_widget_editable' ); + // This class may be left when d&ding widget which + // had focused editable. Clean this class here, not in + // cleanUpWidgetElement for performance and code size reasons. + editable.removeClass( 'cke_widget_editable_focused' ); + + if ( definition.pathName ) + editable.data( 'cke-display-name', definition.pathName ); + + this.editor.focusManager.add( editable ); + editable.on( 'focus', onEditableFocus, this ); + CKEDITOR.env.ie && editable.on( 'blur', onEditableBlur, this ); + + // Finally, process editable's data. This data wasn't processed when loading + // editor's data, becuase they need to be processed separately, with its own filters and settings. + editable.setData( editable.getHtml() ); + + return true; + } + + return false; + }, + + /** + * Checks if a widget has already been initialized and has not been destroyed yet. + * + * See {@link #inited} for more details. + * + * @returns {Boolean} + */ + isInited: function() { + return !!( this.wrapper && this.inited ); + }, + + /** + * Checks if a widget is ready and has not been destroyed yet. + * + * See {@link #property-ready} for more details. + * + * @returns {Boolean} + */ + isReady: function() { + return this.isInited() && this.ready; + }, + + /** + * Focuses a widget by selecting it. + */ + focus: function() { + var sel = this.editor.getSelection(); + + // Fake the selection before focusing editor, to avoid unpreventable viewports scrolling + // on Webkit/Blink/IE which is done because there's no selection or selection was somewhere else than widget. + if ( sel ) { + var isDirty = this.editor.checkDirty(); + + sel.fake( this.wrapper ); + + !isDirty && this.editor.resetDirty(); + } + + // Always focus editor (not only when focusManger.hasFocus is false) (because of #10483). + this.editor.focus(); + }, + + /** + * Removes a class from the widget element. This method is used by + * the {@link #removeStyle} method and should be overriden by widgets + * which should handle classes differently (e.g. on other elements). + * + * **Note**: This method should not be used directly. Use the {@link #setData} method to + * set the `classes` property. Read more in the {@link #setData} documentation. + * + * See also: {@link #hasClass}, {@link #addClass}. + * + * @since 4.4 + * @param {String} className The class to be removed. + */ + removeClass: function( className ) { + this.element.removeClass( className ); + }, + + /** + * Removes the specified style from the widget. It is highly recommended to use the + * {@link CKEDITOR.editor#removeStyle} or {@link CKEDITOR.style#remove} methods instead of + * using this method directly, because unlike editor's and style's methods, this one + * does not perform any checks. + * + * Read more about how applying/removing styles works in the {@link #applyStyle} method documentation. + * + * See also {@link #checkStyleActive}, {@link #applyStyle}, {@link #getClasses}. + * + * @since 4.4 + * @param {CKEDITOR.style} style The custom widget style to be removed. + */ + removeStyle: function( style ) { + applyRemoveStyle( this, style, 0 ); + }, + + /** + * Sets widget value(s) in the {@link #property-data} object. + * If the given value(s) modifies current ones, the {@link #event-data} event is fired. + * + * this.setData( 'align', 'left' ); + * this.data.align; // -> 'left' + * + * this.setData( { align: 'right', opened: false } ); + * this.data.align; // -> 'right' + * this.data.opened; // -> false + * + * Set values are stored in {@link #element}'s attribute (`data-cke-widget-data`), + * in a JSON string, therefore {@link #property-data} should contain + * only serializable data. + * + * **Note:** A special data property, `classes`, exists. It contains an object with + * classes which were returned by the {@link #getClasses} method during the widget initialization. + * This property is then used by the {@link #applyStyle} and {@link #removeStyle} methods. + * When it is changed (the reference to object must be changed!), the widget updates its classes by + * using the {@link #addClass} and {@link #removeClass} methods. + * + * // Adding a new class. + * var classes = CKEDITOR.tools.clone( widget.data.classes ); + * classes.newClass = 1; + * widget.setData( 'classes', classes ); + * + * // Removing a class. + * var classes = CKEDITOR.tools.clone( widget.data.classes ); + * delete classes.newClass; + * widget.setData( 'classes', classes ); + * + * @param {String/Object} keyOrData + * @param {Object} value + * @chainable + */ + setData: function( key, value ) { + var data = this.data, + modified = 0; + + if ( typeof key == 'string' ) { + if ( data[ key ] !== value ) { + data[ key ] = value; + modified = 1; + } + } + else { + var newData = key; + + for ( key in newData ) { + if ( data[ key ] !== newData[ key ] ) { + modified = 1; + data[ key ] = newData[ key ]; + } + } + } + + // Block firing data event and overwriting data element before setupWidgetData is executed. + if ( modified && this.dataReady ) { + writeDataToElement( this ); + this.fire( 'data', data ); + } + + return this; + }, + + /** + * Changes the widget's focus state. This method is executed automatically after + * a widget has been focused by the {@link #method-focus} method or a selection was moved + * out of the widget. + * + * @param {Boolean} selected Whether to select or deselect this widget. + * @chainable + */ + setFocused: function( focused ) { + this.wrapper[ focused ? 'addClass' : 'removeClass' ]( 'cke_widget_focused' ); + this.fire( focused ? 'focus' : 'blur' ); + return this; + }, + + /** + * Changes the widget's select state. This method is executed automatically after + * a widget has been selected by the {@link #method-focus} method or the selection + * was moved out of widget. + * + * @param {Boolean} selected Whether to select or deselect this widget. + * @chainable + */ + setSelected: function( selected ) { + this.wrapper[ selected ? 'addClass' : 'removeClass' ]( 'cke_widget_selected' ); + this.fire( selected ? 'select' : 'deselect' ); + return this; + }, + + /** + * Repositions drag handler according to the widget's element position. Should be called from events, like mouseover. + */ + updateDragHandlerPosition: function() { + var editor = this.editor, + domElement = this.element.$, + oldPos = this._.dragHandlerOffset, + newPos = { + x: domElement.offsetLeft, + y: domElement.offsetTop - DRAG_HANDLER_SIZE + }; + + if ( oldPos && newPos.x == oldPos.x && newPos.y == oldPos.y ) + return; + + // We need to make sure that dirty state is not changed (#11487). + var initialDirty = editor.checkDirty(); + + editor.fire( 'lockSnapshot' ); + this.dragHandlerContainer.setStyles( { + top: newPos.y + 'px', + left: newPos.x + 'px' + } ); + editor.fire( 'unlockSnapshot' ); + !initialDirty && editor.resetDirty(); + + this._.dragHandlerOffset = newPos; + } + }; + + CKEDITOR.event.implementOn( Widget.prototype ); + + /** + * An event fired when a widget is ready (fully initialized). This event is fired after: + * + * * {@link #init} is called, + * * The first {@link #event-data} event is fired, + * * A widget is attached to the document. + * + * Therefore, in case of widget creation with a command which opens a dialog window, this event + * will be delayed after the dialog window is closed and the widget is finally inserted into the document. + * + * **Note**: If your widget does not use automatic dialog window binding (i.e. you open the dialog window manually) + * or another situation in which the widget wrapper is not attached to document at the time when it is + * initialized occurs, you need to take care of firing {@link #event-ready} yourself. + * + * See also {@link #property-ready} and {@link #property-inited} properties, and + * {@link #isReady} and {@link #isInited} methods. + * + * @event ready + */ + + /** + * An event fired when a widget is about to be destroyed, but before it is + * fully torn down. + * + * @event destroy + */ + + /** + * An event fired when a widget is focused. + * + * Widget can be focused by executing {@link #method-focus}. + * + * @event focus + */ + + /** + * An event fired when a widget is blurred. + * + * @event blur + */ + + /** + * An event fired when a widget is selected. + * + * @event select + */ + + /** + * An event fired when a widget is deselected. + * + * @event deselect + */ + + /** + * An event fired by the {@link #method-edit} method. It can be canceled + * in order to stop the default action (opening a dialog window and/or + * {@link CKEDITOR.plugins.widget.repository#finalizeCreation finalizing widget creation}). + * + * @event edit + * @param data + * @param {String} data.dialog Defaults to {@link CKEDITOR.plugins.widget.definition#dialog} + * and can be changed or set by the listener. + */ + + /** + * An event fired when a dialog window for widget editing is opened. + * This event can be canceled in order to handle the editing dialog in a custom manner. + * + * @event dialog + * @param {CKEDITOR.dialog} data The opened dialog window instance. + */ + + /** + * An event fired when a key is pressed on a focused widget. + * This event is forwarded from the {@link CKEDITOR.editor#key} event and + * has the ability to block editor keystrokes if it is canceled. + * + * @event key + * @param data + * @param {Number} data.keyCode A number representing the key code (or combination). + */ + + /** + * An event fired when a widget is double clicked. + * + * **Note:** If a default editing action is executed on double click (i.e. a widget has a + * {@link CKEDITOR.plugins.widget.definition#dialog dialog} defined and the {@link #event-doubleclick} event was not + * canceled), this event will be automatically canceled, so a listener added with the default priority (10) + * will not be executed. Use a listener with low priority (e.g. 5) to be sure that it will be executed. + * + * widget.on( 'doubleclick', function( evt ) { + * console.log( 'widget#doubleclick' ); + * }, null, null, 5 ); + * + * If your widget handles double click in a special way (so the default editing action is not executed), + * make sure you cancel this event, because otherwise it will be propagated to {@link CKEDITOR.editor#doubleclick} + * and another feature may step in (e.g. a Link dialog window may be opened if your widget was inside a link). + * + * @event doubleclick + * @param data + * @param {CKEDITOR.dom.element} data.element The double-clicked element. + */ + + /** + * An event fired when the context menu is opened for a widget. + * + * @event contextMenu + * @param data The object containing context menu options to be added + * for this widget. See {@link CKEDITOR.plugins.contextMenu#addListener}. + */ + + /** + * An event fired when the widget data changed. See the {@link #setData} method and the {@link #property-data} property. + * + * @event data + */ + + + + /** + * The wrapper class for editable elements inside widgets. + * + * Do not use directly. Use {@link CKEDITOR.plugins.widget.definition#editables} or + * {@link CKEDITOR.plugins.widget#initEditable}. + * + * @class CKEDITOR.plugins.widget.nestedEditable + * @extends CKEDITOR.dom.element + * @constructor + * @param {CKEDITOR.editor} editor + * @param {CKEDITOR.dom.element} element + * @param config + * @param {CKEDITOR.filter} [config.filter] + */ + function NestedEditable( editor, element, config ) { + // Call the base constructor. + CKEDITOR.dom.element.call( this, element.$ ); + this.editor = editor; + var filter = this.filter = config.filter; + + // If blockless editable - always use BR mode. + if ( !CKEDITOR.dtd[ this.getName() ].p ) + this.enterMode = this.shiftEnterMode = CKEDITOR.ENTER_BR; + else { + this.enterMode = filter ? filter.getAllowedEnterMode( editor.enterMode ) : editor.enterMode; + this.shiftEnterMode = filter ? filter.getAllowedEnterMode( editor.shiftEnterMode, true ) : editor.shiftEnterMode; + } + } + + NestedEditable.prototype = CKEDITOR.tools.extend( CKEDITOR.tools.prototypedCopy( CKEDITOR.dom.element.prototype ), { + /** + * Sets the editable data. The data will be passed through the {@link CKEDITOR.editor#dataProcessor} + * and the {@link CKEDITOR.editor#filter}. This ensures that the data was filtered and prepared to be + * edited like the {@link CKEDITOR.editor#method-setData editor data}. + * + * @param {String} data + */ + setData: function( data ) { + data = this.editor.dataProcessor.toHtml( data, { + context: this.getName(), + filter: this.filter, + enterMode: this.enterMode + } ); + this.setHtml( data ); + + this.editor.widgets.initOnAll( this ); + }, + + /** + * Gets the editable data. Like {@link #setData}, this method will process and filter the data. + * + * @returns {String} + */ + getData: function() { + return this.editor.dataProcessor.toDataFormat( this.getHtml(), { + context: this.getName(), + filter: this.filter, + enterMode: this.enterMode + } ); + } + } ); + + /** + * The editor instance. + * + * @readonly + * @property {CKEDITOR.editor} editor + */ + + /** + * The filter instance if allowed content rules were defined. + * + * @readonly + * @property {CKEDITOR.filter} filter + */ + + /** + * The enter mode active in this editable. + * It is determined from editable's name (whether it is a blockless editable), + * its allowed content rules (if defined) and the default editor's mode. + * + * @readonly + * @property {Number} enterMode + */ + + /** + * The shift enter move active in this editable. + * + * @readonly + * @property {Number} shiftEnterMode + */ + + + // + // REPOSITORY helpers ----------------------------------------------------- + // + + function addWidgetButtons( editor ) { + var widgets = editor.widgets.registered, + widget, + widgetName, + widgetButton; + + for ( widgetName in widgets ) { + widget = widgets[ widgetName ]; + + // Create button if defined. + widgetButton = widget.button; + if ( widgetButton && editor.ui.addButton ) { + editor.ui.addButton( CKEDITOR.tools.capitalize( widget.name, true ), { + label: widgetButton, + command: widget.name, + toolbar: 'insert,10' + } ); + } + } + } + + // Create a command creating and editing widget. + // + // @param editor + // @param {CKEDITOR.plugins.widget.definition} widgetDef + function addWidgetCommand( editor, widgetDef ) { + editor.addCommand( widgetDef.name, { + exec: function() { + var focused = editor.widgets.focused; + // If a widget of the same type is focused, start editing. + if ( focused && focused.name == widgetDef.name ) + focused.edit(); + // Otherwise... + // ... use insert method is was defined. + else if ( widgetDef.insert ) + widgetDef.insert(); + // ... or create a brand-new widget from template. + else if ( widgetDef.template ) { + var defaults = typeof widgetDef.defaults == 'function' ? widgetDef.defaults() : widgetDef.defaults, + element = CKEDITOR.dom.element.createFromHtml( widgetDef.template.output( defaults ) ), + instance, + wrapper = editor.widgets.wrapElement( element, widgetDef.name ), + temp = new CKEDITOR.dom.documentFragment( wrapper.getDocument() ); + + // Append wrapper to a temporary document. This will unify the environment + // in which #data listeners work when creating and editing widget. + temp.append( wrapper ); + instance = editor.widgets.initOn( element, widgetDef ); + + // Instance could be destroyed during initialization. + // In this case finalize creation if some new widget + // was left in temporary document fragment. + if ( !instance ) { + finalizeCreation(); + return; + } + + // Listen on edit to finalize widget insertion. + // + // * If dialog was set, then insert widget after dialog was successfully saved or destroy this + // temporary instance. + // * If dialog wasn't set and edit wasn't canceled, insert widget. + var editListener = instance.once( 'edit', function( evt ) { + if ( evt.data.dialog ) { + instance.once( 'dialog', function( evt ) { + var dialog = evt.data, + okListener, + cancelListener; + + // Finalize creation AFTER (20) new data was set. + okListener = dialog.once( 'ok', finalizeCreation, null, null, 20 ); + + cancelListener = dialog.once( 'cancel', function() { + editor.widgets.destroy( instance, true ); + } ); + + dialog.once( 'hide', function() { + okListener.removeListener(); + cancelListener.removeListener(); + } ); + } ); + } else { + // Dialog hasn't been set, so insert widget now. + finalizeCreation(); + } + }, null, null, 999 ); + + instance.edit(); + + // Remove listener in case someone canceled it before this + // listener was executed. + editListener.removeListener(); + } + + function finalizeCreation() { + editor.widgets.finalizeCreation( temp ); + } + }, + + refresh: function( editor, path ) { + // Disable widgets' commands inside nested editables - + // check if blockLimit is a nested editable or a descendant of any. + this.setState( getNestedEditable( editor.editable(), path.blockLimit ) ? CKEDITOR.TRISTATE_DISABLED : CKEDITOR.TRISTATE_OFF ); + }, + // A hack to force command refreshing on context change. + context: 'div', + + allowedContent: widgetDef.allowedContent, + requiredContent: widgetDef.requiredContent, + contentForms: widgetDef.contentForms, + contentTransformations: widgetDef.contentTransformations + } ); + } + + function addWidgetProcessors( widgetsRepo, widgetDef ) { + var upcast = widgetDef.upcast, + upcasts; + + if ( !upcast ) + return; + + // Multiple upcasts defined in string. + if ( typeof upcast == 'string' ) { + upcasts = upcast.split( ',' ); + while ( upcasts.length ) + widgetsRepo._.upcasts.push( [ widgetDef.upcasts[ upcasts.pop() ], widgetDef.name ] ); + } else { + // Single rule which is automatically activated. + widgetsRepo._.upcasts.push( [ upcast, widgetDef.name ] ); + } + } + + function blurWidget( widgetsRepo, widget ) { + widgetsRepo.focused = null; + + if ( widget.isInited() ) { + var isDirty = widget.editor.checkDirty(); + + // Widget could be destroyed in the meantime - e.g. data could be set. + widgetsRepo.fire( 'widgetBlurred', { widget: widget } ); + widget.setFocused( false ); + + !isDirty && widget.editor.resetDirty(); + } + } + + function checkWidgets( evt ) { + var options = evt.data; + + if ( this.editor.mode != 'wysiwyg' ) + return; + + var editable = this.editor.editable(), + instances = this.instances, + newInstances, i, count, wrapper; + + if ( !editable ) + return; + + // Remove widgets which have no corresponding elements in DOM. + for ( i in instances ) { + if ( !editable.contains( instances[ i ].wrapper ) ) + this.destroy( instances[ i ], true ); + } + + // Init on all (new) if initOnlyNew option was passed. + if ( options && options.initOnlyNew ) + newInstances = this.initOnAll(); + else { + var wrappers = editable.find( '.cke_widget_wrapper' ); + newInstances = []; + + // Create widgets on existing wrappers if they do not exists. + for ( i = 0, count = wrappers.count(); i < count; i++ ) { + wrapper = wrappers.getItem( i ); + + // Check if there's no instance for this widget and that + // wrapper is not inside some temporary element like copybin (#11088). + if ( !this.getByElement( wrapper, true ) && !findParent( wrapper, isDomTemp ) ) { + // Add cke_widget_new class because otherwise + // widget will not be created on such wrapper. + wrapper.addClass( 'cke_widget_new' ); + newInstances.push( this.initOn( wrapper.getFirst( isDomWidgetElement ) ) ); + } + } + } + + // If only single widget was initialized and focusInited was passed, focus it. + if ( options && options.focusInited && newInstances.length == 1 ) + newInstances[ 0 ].focus(); + } + + // Unwraps widget element and clean up element. + // + // This function is used to clean up pasted widgets. + // It should have similar result to widget#destroy plus + // some additional adjustments, specific for pasting. + // + // @param {CKEDITOR.htmlParser.element} el + function cleanUpWidgetElement( el ) { + var parent = el.parent; + if ( parent.type == CKEDITOR.NODE_ELEMENT && parent.attributes[ 'data-cke-widget-wrapper' ] ) + parent.replaceWith( el ); + } + + // Similar to cleanUpWidgetElement, but works on DOM and finds + // widget elements by its own. + // + // Unlike cleanUpWidgetElement it will wrap element back. + // + // @param {CKEDITOR.dom.element} container + function cleanUpAllWidgetElements( widgetsRepo, container ) { + var wrappers = container.find( '.cke_widget_wrapper' ), + wrapper, element, + i = 0, + l = wrappers.count(); + + for ( ; i < l; ++i ) { + wrapper = wrappers.getItem( i ); + element = wrapper.getFirst( isDomWidgetElement ); + // If wrapper contains widget element - unwrap it and wrap again. + if ( element.type == CKEDITOR.NODE_ELEMENT && element.data( 'widget' ) ) { + element.replace( wrapper ); + widgetsRepo.wrapElement( element ); + } else { + // Otherwise - something is wrong... clean this up. + wrapper.remove(); + } + } + } + + // Creates {@link CKEDITOR.filter} instance for given widget, editable and rules. + // + // Once filter for widget-editable pair is created it is cached, so the same instance + // will be returned when method is executed again. + // + // @param {String} widgetName + // @param {String} editableName + // @param {CKEDITOR.plugins.widget.nestedEditableDefinition} editableDefinition The nested editable definition. + // @returns {CKEDITOR.filter} Filter instance or `null` if rules are not defined. + // @context CKEDITOR.plugins.widget.repository + function createEditableFilter( widgetName, editableName, editableDefinition ) { + if ( !editableDefinition.allowedContent ) + return null; + + var editables = this._.filters[ widgetName ]; + + if ( !editables ) + this._.filters[ widgetName ] = editables = {}; + + var filter = editables[ editableName ]; + + if ( !filter ) + editables[ editableName ] = filter = new CKEDITOR.filter( editableDefinition.allowedContent ); + + return filter; + } + + // Creates an iterator function which when executed on all + // elements in DOM tree will gather elements that should be wrapped + // and initialized as widgets. + function createUpcastIterator( widgetsRepo ) { + var toBeWrapped = [], + upcasts = widgetsRepo._.upcasts, + upcastCallbacks = widgetsRepo._.upcastCallbacks; + + return { + toBeWrapped: toBeWrapped, + + iterator: function( element ) { + var upcast, upcasted, + data, + i, + upcastsLength, + upcastCallbacksLength; + + // Wrapper found - find widget element, add it to be + // cleaned up (unwrapped) and wrapped and stop iterating in this branch. + if ( 'data-cke-widget-wrapper' in element.attributes ) { + element = element.getFirst( isParserWidgetElement ); + + if ( element ) + toBeWrapped.push( [ element ] ); + + // Do not iterate over descendants. + return false; + } + // Widget element found - add it to be cleaned up (just in case) + // and wrapped and stop iterating in this branch. + else if ( 'data-widget' in element.attributes ) { + toBeWrapped.push( [ element ] ); + + // Do not iterate over descendants. + return false; + } + else if ( ( upcastsLength = upcasts.length ) ) { + // Ignore elements with data-cke-widget-upcasted to avoid multiple upcasts (#11533). + // Do not iterate over descendants. + if ( element.attributes[ 'data-cke-widget-upcasted' ] ) + return false; + + // Check element with upcast callbacks first. + // If any of them return false abort upcasting. + for ( i = 0, upcastCallbacksLength = upcastCallbacks.length; i < upcastCallbacksLength; ++i ) { + if ( upcastCallbacks[ i ]( element ) === false ) + return; + // Return nothing in order to continue iterating over ascendants. + // See http://dev.ckeditor.com/ticket/11186#comment:6 + } + + for ( i = 0; i < upcastsLength; ++i ) { + upcast = upcasts[ i ]; + data = {}; + + if ( ( upcasted = upcast[ 0 ]( element, data ) ) ) { + // If upcast function returned element, upcast this one. + // It can be e.g. a new element wrapping the original one. + if ( upcasted instanceof CKEDITOR.htmlParser.element ) + element = upcasted; + + // Set initial data attr with data from upcast method. + element.attributes[ 'data-cke-widget-data' ] = encodeURIComponent( JSON.stringify( data ) ); + element.attributes[ 'data-cke-widget-upcasted' ] = 1; + + toBeWrapped.push( [ element, upcast[ 1 ] ] ); + + // Do not iterate over descendants. + return false; + } + } + } + } + }; + } + + // Finds a first parent that matches query. + // + // @param {CKEDITOR.dom.element} element + // @param {Function} query + function findParent( element, query ) { + var parent = element; + + while ( ( parent = parent.getParent() ) ) { + if ( query( parent ) ) + return true; + } + return false; + } + + // Gets nested editable if node is its descendant or the editable itself. + // + // @param {CKEDITOR.dom.element} guard Stop ancestor search on this node (usually editor's editable). + // @param {CKEDITOR.dom.node} node Start search from this node. + // @returns {CKEDITOR.dom.element} Element or null. + function getNestedEditable( guard, node ) { + if ( !node || node.equals( guard ) ) + return null; + + if ( isDomNestedEditable( node ) ) + return node; + + return getNestedEditable( guard, node.getParent() ); + } + + function getWrapperAttributes( inlineWidget ) { + return { + // tabindex="-1" means that it can receive focus by code. + tabindex: -1, + contenteditable: 'false', + 'data-cke-widget-wrapper': 1, + 'data-cke-filter': 'off', + // Class cke_widget_new marks widgets which haven't been initialized yet. + 'class': 'cke_widget_wrapper cke_widget_new cke_widget_' + + ( inlineWidget ? 'inline' : 'block' ) + }; + } + + // Inserts element at given index. + // It will check DTD and split ancestor elements up to the first + // that can contain this element. + // + // @param {CKEDITOR.htmlParser.element} parent + // @param {Number} index + // @param {CKEDITOR.htmlParser.element} element + function insertElement( parent, index, element ) { + // Do not split doc fragment... + if ( parent.type == CKEDITOR.NODE_ELEMENT ) { + var parentAllows = CKEDITOR.dtd[ parent.name ]; + // Parent element is known (included in DTD) and cannot contain + // this element. + if ( parentAllows && !parentAllows[ element.name ] ) { + var parent2 = parent.split( index ), + parentParent = parent.parent; + + // Element will now be inserted at right parent's index. + index = parent2.getIndex(); + + // If left part of split is empty - remove it. + if ( !parent.children.length ) { + index -= 1; + parent.remove(); + } + + // If right part of split is empty - remove it. + if ( !parent2.children.length ) + parent2.remove(); + + // Try inserting as grandpas' children. + return insertElement( parentParent, index, element ); + } + } + + // Finally we can add this element. + parent.add( element, index ); + } + + // @param {CKEDITOR.htmlParser.element} + function isParserWidgetElement( element ) { + return element.type == CKEDITOR.NODE_ELEMENT && !!element.attributes[ 'data-widget' ]; + } + + // @param {CKEDITOR.dom.element} + function isDomWidgetElement( element ) { + return element.type == CKEDITOR.NODE_ELEMENT && element.hasAttribute( 'data-widget' ); + } + + // Whether for this definition and element widget should be created in inline or block mode. + function isWidgetInline( widgetDef, elementName ) { + return typeof widgetDef.inline == 'boolean' ? widgetDef.inline : !!CKEDITOR.dtd.$inline[ elementName ]; + } + + // @param {CKEDITOR.htmlParser.element} + function isParserWidgetWrapper( element ) { + return element.type == CKEDITOR.NODE_ELEMENT && element.attributes[ 'data-cke-widget-wrapper' ]; + } + + // @param {CKEDITOR.dom.element} + function isDomWidgetWrapper( element ) { + return element.type == CKEDITOR.NODE_ELEMENT && element.hasAttribute( 'data-cke-widget-wrapper' ); + } + + // @param {CKEDITOR.dom.element} + function isDomNestedEditable( node ) { + return node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'data-cke-widget-editable' ); + } + + // @param {CKEDITOR.dom.element} + function isDomTemp( element ) { + return element.hasAttribute( 'data-cke-temp' ); + } + + // @param {CKEDITOR.dom.element} + function isDomDragHandler( element ) { + return element.type == CKEDITOR.NODE_ELEMENT && element.hasAttribute( 'data-cke-widget-drag-handler' ); + } + + // @param {CKEDITOR.dom.element} + function isDomDragHandlerContainer( element ) { + return element.type == CKEDITOR.NODE_ELEMENT && element.hasClass( 'cke_widget_drag_handler_container' ); + } + + function finalizeNativeDrop( editor, sourceWidget, range ) { + // Save the snapshot with the state before moving widget. + // Focus widget, so when we'll undo the DnD, widget will be focused. + sourceWidget.focus(); + editor.fire( 'saveSnapshot' ); + + // Lock snapshot to group all steps of moving widget from the original place to the new one. + editor.fire( 'lockSnapshot', { dontUpdate: true } ); + + range.select(); + + var widgetHtml = sourceWidget.wrapper.getOuterHtml(); + sourceWidget.wrapper.remove(); + editor.widgets.destroy( sourceWidget, true ); + editor.execCommand( 'paste', widgetHtml ); + + editor.fire( 'unlockSnapshot' ); + } + + function getRangeAtDropPosition( editor, dropEvt ) { + var $evt = dropEvt.data.$, + $range, + range = editor.createRange(); + + // Make testing possible. + if ( dropEvt.data.testRange ) + return dropEvt.data.testRange; + + // Webkits. + if ( document.caretRangeFromPoint ) { + $range = editor.document.$.caretRangeFromPoint( $evt.clientX, $evt.clientY ); + range.setStart( CKEDITOR.dom.node( $range.startContainer ), $range.startOffset ); + range.collapse( true ); + } + // FF. + else if ( $evt.rangeParent ) { + range.setStart( CKEDITOR.dom.node( $evt.rangeParent ), $evt.rangeOffset ); + range.collapse( true ); + } + // IEs. + else if ( document.body.createTextRange ) { + $range = editor.document.getBody().$.createTextRange(); + $range.moveToPoint( $evt.clientX, $evt.clientY ); + var id = 'cke-temp-' + ( new Date() ).getTime(); + $range.pasteHTML( '\u200b' ); + + var span = editor.document.getById( id ); + range.moveToPosition( span, CKEDITOR.POSITION_BEFORE_START ); + span.remove(); + } else { + return null; + } + + return range; + } + + function onEditableKey( widget, keyCode ) { + var focusedEditable = widget.focusedEditable, + range; + + // CTRL+A. + if ( keyCode == CKEDITOR.CTRL + 65 ) { + var bogus = focusedEditable.getBogus(); + + range = widget.editor.createRange(); + range.selectNodeContents( focusedEditable ); + // Exclude bogus if exists. + if ( bogus ) + range.setEndAt( bogus, CKEDITOR.POSITION_BEFORE_START ); + + range.select(); + // Cancel event - block default. + return false; + } + // DEL or BACKSPACE. + else if ( keyCode == 8 || keyCode == 46 ) { + var ranges = widget.editor.getSelection().getRanges(); + + range = ranges[ 0 ]; + + // Block del or backspace if at editable's boundary. + return !( ranges.length == 1 && range.collapsed && + range.checkBoundaryOfElement( focusedEditable, CKEDITOR[ keyCode == 8 ? 'START' : 'END' ] ) ); + } + } + + function setFocusedEditable( widgetsRepo, widget, editableElement, offline ) { + var editor = widgetsRepo.editor; + + editor.fire( 'lockSnapshot' ); + + if ( editableElement ) { + var editableName = editableElement.data( 'cke-widget-editable' ), + editableInstance = widget.editables[ editableName ]; + + widgetsRepo.widgetHoldingFocusedEditable = widget; + widget.focusedEditable = editableInstance; + editableElement.addClass( 'cke_widget_editable_focused' ); + + if ( editableInstance.filter ) + editor.setActiveFilter( editableInstance.filter ); + editor.setActiveEnterMode( editableInstance.enterMode, editableInstance.shiftEnterMode ); + } else { + if ( !offline ) + widget.focusedEditable.removeClass( 'cke_widget_editable_focused' ); + + widget.focusedEditable = null; + widgetsRepo.widgetHoldingFocusedEditable = null; + editor.setActiveFilter( null ); + editor.setActiveEnterMode( null, null ); + } + + editor.fire( 'unlockSnapshot' ); + } + + function setupContextMenu( editor ) { + if ( !editor.contextMenu ) + return; + + editor.contextMenu.addListener( function( element ) { + var widget = editor.widgets.getByElement( element, true ); + + if ( widget ) + return widget.fire( 'contextMenu', {} ); + } ); + } + + // And now we've got two problems - original problem and RegExp. + // Some softeners: + // * FF tends to copy all blocks up to the copybin container. + // * IE tends to copy only the copybin, without its container. + // * We use spans on IE and blockless editors, but divs in other cases. + var pasteReplaceRegex = new RegExp( + '^' + + '(?:<(?:div|span)(?: data-cke-temp="1")?(?: id="cke_copybin")?(?: data-cke-temp="1")?>)?' + + '(?:<(?:div|span)(?: style="[^"]+")?>)?' + + ']*data-cke-copybin-start="1"[^>]*>.?([\\s\\S]+)]*data-cke-copybin-end="1"[^>]*>.?' + + '(?:)?' + + '(?:)?' + + '$' + ); + + function pasteReplaceFn( match, wrapperHtml ) { + // Avoid polluting pasted data with any whitspaces, + // what's going to break check whether only one widget was pasted. + return CKEDITOR.tools.trim( wrapperHtml ); + } + + function setupDragAndDrop( widgetsRepo ) { + var editor = widgetsRepo.editor, + lineutils = CKEDITOR.plugins.lineutils; + + editor.on( 'contentDom', function() { + var editable = editor.editable(), + // #11123 Firefox needs to listen on document, because otherwise event won't be fired. + // #11086 IE8 cannot listen on document. + dropTarget = ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) || editable.isInline() ? editable : editor.document; + + editable.attachListener( dropTarget, 'drop', function( evt ) { + var dataStr = evt.data.$.dataTransfer.getData( 'text' ), + dataObj, + sourceWidget, + range; + + if ( !dataStr ) + return; + + try { + dataObj = JSON.parse( dataStr ); + } catch ( e ) { + // Do nothing - data couldn't be parsed so it's not a CKEditor's data. + return; + } + + if ( dataObj.type != 'cke-widget' ) + return; + + evt.data.preventDefault(); + + // Something went wrong... maybe someone is dragging widgets between editors/windows/tabs/browsers/frames. + if ( dataObj.editor != editor.name || !( sourceWidget = widgetsRepo.instances[ dataObj.id ] ) ) + return; + + // Try to determine a DOM position at which drop happened. If none of methods + // which we support succeeded abort. + range = getRangeAtDropPosition( editor, evt ); + if ( !range ) + return; + + // #11132 Hack to prevent cursor loss on Firefox. Without timeout widget is + // correctly pasted but then cursor is invisible (although it works) and can be restored + // only by blurring editable. + if ( CKEDITOR.env.gecko ) + setTimeout( finalizeNativeDrop, 0, editor, sourceWidget, range ); + else + finalizeNativeDrop( editor, sourceWidget, range ); + } ); + + // Register Lineutils's utilities as properties of repo. + CKEDITOR.tools.extend( widgetsRepo, { + finder: new lineutils.finder( editor, { + lookups: { + // Element is block but not list item and not in nested editable. + 'default': function( el ) { + if ( el.is( CKEDITOR.dtd.$listItem ) ) + return; + + if ( !el.is( CKEDITOR.dtd.$block ) ) + return; + + while ( el ) { + if ( isDomNestedEditable( el ) ) + return; + + el = el.getParent(); + } + + return CKEDITOR.LINEUTILS_BEFORE | CKEDITOR.LINEUTILS_AFTER; + } + } + } ), + locator: new lineutils.locator( editor ), + liner: new lineutils.liner( editor, { + lineStyle: { + cursor: 'move !important', + 'border-top-color': '#666' + }, + tipLeftStyle: { + 'border-left-color': '#666' + }, + tipRightStyle: { + 'border-right-color': '#666' + } + } ) + }, true ); + } ); + } + + // Setup mouse observer which will trigger: + // * widget focus on widget click, + // * widget#doubleclick forwarded from editor#doubleclick. + function setupMouseObserver( widgetsRepo ) { + var editor = widgetsRepo.editor; + + editor.on( 'contentDom', function() { + var editable = editor.editable(), + evtRoot = editable.isInline() ? editable : editor.document, + widget, + mouseDownOnDragHandler; + + editable.attachListener( evtRoot, 'mousedown', function( evt ) { + var target = evt.data.getTarget(); + + // #10887 Clicking scrollbar in IE8 will invoke event with empty target object. + if ( !target.type ) + return false; + + widget = widgetsRepo.getByElement( target ); + mouseDownOnDragHandler = 0; // Reset. + + // Widget was clicked, but not editable nested in it. + if ( widget ) { + // Ignore mousedown on drag and drop handler if the widget is inline. + // Block widgets are handled by Lineutils. + if ( widget.inline && target.type == CKEDITOR.NODE_ELEMENT && target.hasAttribute( 'data-cke-widget-drag-handler' ) ) { + mouseDownOnDragHandler = 1; + return; + } + + if ( !getNestedEditable( widget.wrapper, target ) ) { + evt.data.preventDefault(); + if ( !CKEDITOR.env.ie ) + widget.focus(); + } else { + // Reset widget so mouseup listener is not confused. + widget = null; + } + } + } ); + + // Focus widget on mouseup if mousedown was fired on drag handler. + // Note: mouseup won't be fired at all if widget was dragged and dropped, so + // this code will be executed only when drag handler was clicked. + editable.attachListener( evtRoot, 'mouseup', function() { + if ( widget && mouseDownOnDragHandler ) { + mouseDownOnDragHandler = 0; + widget.focus(); + } + } ); + + // On IE it is not enough to block mousedown. If widget wrapper (element with + // contenteditable=false attribute) is clicked directly (it is a target), + // then after mouseup/click IE will select that element. + // It is not possible to prevent that default action, + // so we force fake selection after everything happened. + if ( CKEDITOR.env.ie ) { + editable.attachListener( evtRoot, 'mouseup', function() { + if ( widget ) { + setTimeout( function() { + widget.focus(); + widget = null; + } ); + } + } ); + } + } ); + + editor.on( 'doubleclick', function( evt ) { + var widget = widgetsRepo.getByElement( evt.data.element ); + + // Not in widget or in nested editable. + if ( !widget || getNestedEditable( widget.wrapper, evt.data.element ) ) + return; + + return widget.fire( 'doubleclick', { element: evt.data.element } ); + }, null, null, 1 ); + } + + // Setup editor#key observer which will forward it + // to focused widget. + function setupKeyboardObserver( widgetsRepo ) { + var editor = widgetsRepo.editor; + + editor.on( 'key', function( evt ) { + var focused = widgetsRepo.focused, + widgetHoldingFocusedEditable = widgetsRepo.widgetHoldingFocusedEditable, + ret; + + if ( focused ) + ret = focused.fire( 'key', { keyCode: evt.data.keyCode } ); + else if ( widgetHoldingFocusedEditable ) + ret = onEditableKey( widgetHoldingFocusedEditable, evt.data.keyCode ); + + return ret; + }, null, null, 1 ); + } + + // Setup copybin on native copy and cut events in order to handle copy and cut commands + // if user accepted security alert on IEs. + // Note: when copying or cutting using keystroke, copySingleWidget will be first executed + // by the keydown listener. Conflict between two calls will be resolved by copy_bin existence check. + function setupNativeCutAndCopy( widgetsRepo ) { + var editor = widgetsRepo.editor; + + editor.on( 'contentDom', function() { + var editable = editor.editable(); + + editable.attachListener( editable, 'copy', eventListener ); + editable.attachListener( editable, 'cut', eventListener ); + } ); + + function eventListener( evt ) { + if ( widgetsRepo.focused ) + copySingleWidget( widgetsRepo.focused, evt.name == 'cut' ); + } + } + + // Setup selection observer which will trigger: + // * widget select & focus on selection change, + // * nested editable focus (related properites and classes) on selection change, + // * deselecting and blurring all widgets on data, + // * blurring widget on editor blur. + function setupSelectionObserver( widgetsRepo ) { + var editor = widgetsRepo.editor; + + editor.on( 'selectionCheck', function() { + widgetsRepo.fire( 'checkSelection' ); + } ); + + widgetsRepo.on( 'checkSelection', widgetsRepo.checkSelection, widgetsRepo ); + + editor.on( 'selectionChange', function( evt ) { + var nestedEditable = getNestedEditable( editor.editable(), evt.data.selection.getStartElement() ), + newWidget = nestedEditable && widgetsRepo.getByElement( nestedEditable ), + oldWidget = widgetsRepo.widgetHoldingFocusedEditable; + + if ( oldWidget ) { + if ( oldWidget !== newWidget || !oldWidget.focusedEditable.equals( nestedEditable ) ) { + setFocusedEditable( widgetsRepo, oldWidget, null ); + + if ( newWidget && nestedEditable ) + setFocusedEditable( widgetsRepo, newWidget, nestedEditable ); + } + } + // It may happen that there's no widget even if editable was found - + // e.g. if selection was automatically set in editable although widget wasn't initialized yet. + else if ( newWidget && nestedEditable ) { + setFocusedEditable( widgetsRepo, newWidget, nestedEditable ); + } + } ); + + // Invalidate old widgets early - immediately on dataReady. + editor.on( 'dataReady', function() { + // Deselect and blur all widgets. + stateUpdater( widgetsRepo ).commit(); + } ); + + editor.on( 'blur', function() { + var widget; + + if ( ( widget = widgetsRepo.focused ) ) + blurWidget( widgetsRepo, widget ); + + if ( ( widget = widgetsRepo.widgetHoldingFocusedEditable ) ) + setFocusedEditable( widgetsRepo, widget, null ); + } ); + } + + // Set up actions like: + // * processing in toHtml/toDataFormat, + // * pasting handling, + // * insertion handling, + // * editable reload handling (setData, mode switch, undo/redo), + // * DOM invalidation handling, + // * widgets checks. + function setupWidgetsLifecycle( widgetsRepo ) { + setupWidgetsLifecycleStart( widgetsRepo ); + setupWidgetsLifecycleEnd( widgetsRepo ); + + widgetsRepo.on( 'checkWidgets', checkWidgets ); + widgetsRepo.editor.on( 'contentDomInvalidated', widgetsRepo.checkWidgets, widgetsRepo ); + } + + function setupWidgetsLifecycleEnd( widgetsRepo ) { + var editor = widgetsRepo.editor, + downcastingSessions = {}; + + // Listen before htmlDP#htmlFilter is applied to cache all widgets, because we'll + // loose data-cke-* attributes. + editor.on( 'toDataFormat', function( evt ) { + // To avoid conflicts between htmlDP#toDF calls done at the same time + // (e.g. nestedEditable#getData called during downcasting some widget) + // mark every toDataFormat event chain with the downcasting session id. + var id = CKEDITOR.tools.getNextNumber(), + toBeDowncasted = []; + evt.data.downcastingSessionId = id; + downcastingSessions[ id ] = toBeDowncasted; + + evt.data.dataValue.forEach( function( element ) { + var attrs = element.attributes, + widget, widgetElement; + + // Wrapper. + // Perform first part of downcasting (cleanup) and cache widgets, + // because after applying DP's filter all data-cke-* attributes will be gone. + if ( 'data-cke-widget-id' in attrs ) { + widget = widgetsRepo.instances[ attrs[ 'data-cke-widget-id' ] ]; + if ( widget ) { + widgetElement = element.getFirst( isParserWidgetElement ); + toBeDowncasted.push( { + wrapper: element, + element: widgetElement, + widget: widget, + editables: {} + } ); + + // If widget did not have data-cke-widget attribute before upcasting remove it. + if ( widgetElement.attributes[ 'data-cke-widget-keep-attr' ] != '1' ) + delete widgetElement.attributes[ 'data-widget' ]; + } + } + // Nested editable. + else if ( 'data-cke-widget-editable' in attrs ) { + // Save the reference to this nested editable in the closest widget to be downcasted. + // Nested editables are downcasted in the successive toDataFormat to create an opportunity + // for dataFilter's "excludeNestedEditable" option to do its job (that option relies on + // contenteditable="true" attribute) (#11372). + toBeDowncasted[ toBeDowncasted.length - 1 ].editables[ attrs[ 'data-cke-widget-editable' ] ] = element; + + // Don't check children - there won't be next wrapper or nested editable which we + // should process in this session. + return false; + } + }, CKEDITOR.NODE_ELEMENT, true ); + }, null, null, 8 ); + + // Listen after dataProcessor.htmlFilter and ACF were applied + // so wrappers securing widgets' contents are removed after all filtering was done. + editor.on( 'toDataFormat', function( evt ) { + // Ignore some unmarked sessions. + if ( !evt.data.downcastingSessionId ) + return; + + var toBeDowncasted = downcastingSessions[ evt.data.downcastingSessionId ], + toBe, widget, widgetElement, retElement, editableElement, e; + + while ( ( toBe = toBeDowncasted.shift() ) ) { + widget = toBe.widget; + widgetElement = toBe.element; + retElement = widget._.downcastFn && widget._.downcastFn.call( widget, widgetElement ); + + // Replace nested editables' content with their output data. + for ( e in toBe.editables ) { + editableElement = toBe.editables[ e ]; + + delete editableElement.attributes.contenteditable; + editableElement.setHtml( widget.editables[ e ].getData() ); + } + + // Returned element always defaults to widgetElement. + if ( !retElement ) + retElement = widgetElement; + + toBe.wrapper.replaceWith( retElement ); + } + }, null, null, 13 ); + + + editor.on( 'contentDomUnload', function() { + widgetsRepo.destroyAll( true ); + } ); + } + + function setupWidgetsLifecycleStart( widgetsRepo ) { + var editor = widgetsRepo.editor, + processedWidgetOnly, + snapshotLoaded; + + // Listen after ACF (so data are filtered), + // but before dataProcessor.dataFilter was applied (so we can secure widgets' internals). + editor.on( 'toHtml', function( evt ) { + var upcastIterator = createUpcastIterator( widgetsRepo ), + toBeWrapped; + + evt.data.dataValue.forEach( upcastIterator.iterator, CKEDITOR.NODE_ELEMENT, true ); + + // Clean up and wrap all queued elements. + while ( ( toBeWrapped = upcastIterator.toBeWrapped.pop() ) ) { + cleanUpWidgetElement( toBeWrapped[ 0 ] ); + widgetsRepo.wrapElement( toBeWrapped[ 0 ], toBeWrapped[ 1 ] ); + } + + // Used to determine whether only widget was pasted. + processedWidgetOnly = evt.data.dataValue.children.length == 1 && + isParserWidgetWrapper( evt.data.dataValue.children[ 0 ] ); + }, null, null, 8 ); + + editor.on( 'dataReady', function() { + // Clean up all widgets loaded from snapshot. + if ( snapshotLoaded ) + cleanUpAllWidgetElements( widgetsRepo, editor.editable() ); + snapshotLoaded = 0; + + // Some widgets were destroyed on contentDomUnload, + // some on loadSnapshot, but that does not include + // e.g. setHtml on inline editor or widgets removed just + // before setting data. + widgetsRepo.destroyAll( true ); + widgetsRepo.initOnAll(); + } ); + + // Set flag so dataReady will know that additional + // cleanup is needed, because snapshot containing widgets was loaded. + editor.on( 'loadSnapshot', function( evt ) { + // Primitive but sufficient check which will prevent from executing + // heavier cleanUpAllWidgetElements if not needed. + if ( ( /data-cke-widget/ ).test( evt.data ) ) + snapshotLoaded = 1; + + widgetsRepo.destroyAll( true ); + }, null, null, 9 ); + + // Handle pasted single widget. + editor.on( 'paste', function( evt ) { + evt.data.dataValue = evt.data.dataValue.replace( pasteReplaceRegex, pasteReplaceFn ); + } ); + + // Listen with high priority to check widgets after data was inserted. + editor.on( 'insertText', checkNewWidgets, null, null, 999 ); + editor.on( 'insertHtml', checkNewWidgets, null, null, 999 ); + + function checkNewWidgets() { + editor.fire( 'lockSnapshot' ); + + // Init only new for performance reason. + // Focus inited if only widget was processed. + widgetsRepo.checkWidgets( { initOnlyNew: true, focusInited: processedWidgetOnly } ); + + editor.fire( 'unlockSnapshot' ); + } + } + + // Helper for coordinating which widgets should be + // selected/deselected and which one should be focused/blurred. + function stateUpdater( widgetsRepo ) { + var currentlySelected = widgetsRepo.selected, + toBeSelected = [], + toBeDeselected = currentlySelected.slice( 0 ), + focused = null; + + return { + select: function( widget ) { + if ( CKEDITOR.tools.indexOf( currentlySelected, widget ) < 0 ) + toBeSelected.push( widget ); + + var index = CKEDITOR.tools.indexOf( toBeDeselected, widget ); + if ( index >= 0 ) + toBeDeselected.splice( index, 1 ); + + return this; + }, + + focus: function( widget ) { + focused = widget; + return this; + }, + + commit: function() { + var focusedChanged = widgetsRepo.focused !== focused, + widget, isDirty; + + widgetsRepo.editor.fire( 'lockSnapshot' ); + + if ( focusedChanged && ( widget = widgetsRepo.focused ) ) + blurWidget( widgetsRepo, widget ); + + while ( ( widget = toBeDeselected.pop() ) ) { + currentlySelected.splice( CKEDITOR.tools.indexOf( currentlySelected, widget ), 1 ); + // Widget could be destroyed in the meantime - e.g. data could be set. + if ( widget.isInited() ) { + isDirty = widget.editor.checkDirty(); + + widget.setSelected( false ); + + !isDirty && widget.editor.resetDirty(); + } + } + + if ( focusedChanged && focused ) { + isDirty = widgetsRepo.editor.checkDirty(); + + widgetsRepo.focused = focused; + widgetsRepo.fire( 'widgetFocused', { widget: focused } ); + focused.setFocused( true ); + + !isDirty && widgetsRepo.editor.resetDirty(); + } + + while ( ( widget = toBeSelected.pop() ) ) { + currentlySelected.push( widget ); + widget.setSelected( true ); + } + + widgetsRepo.editor.fire( 'unlockSnapshot' ); + } + }; + } + + + // + // WIDGET helpers --------------------------------------------------------- + // + + // LEFT, RIGHT, UP, DOWN, DEL, BACKSPACE - unblock default fake sel handlers. + var keystrokesNotBlockedByWidget = { 37: 1, 38: 1, 39: 1, 40: 1, 8: 1, 46: 1 }; + + // Applies or removes style's classes from widget. + // @param {CKEDITOR.style} style Custom widget style. + // @param {Boolean} apply Whether to apply or remove style. + function applyRemoveStyle( widget, style, apply ) { + var changed = 0, + classes = getStyleClasses( style ), + updatedClasses = widget.data.classes || {}, + cl; + + // Ee... Something is wrong with this style. + if ( !classes ) + return; + + // Clone, because we need to break reference. + updatedClasses = CKEDITOR.tools.clone( updatedClasses ); + + while ( ( cl = classes.pop() ) ) { + if ( apply ) { + if ( !updatedClasses[ cl ] ) + changed = updatedClasses[ cl ] = 1; + } else { + if ( updatedClasses[ cl ] ) { + delete updatedClasses[ cl ]; + changed = 1; + } + } + } + if ( changed ) + widget.setData( 'classes', updatedClasses ); + } + + function cancel( evt ) { + evt.cancel(); + } + + function copySingleWidget( widget, isCut ) { + var editor = widget.editor, + doc = editor.document; + + // We're still handling previous copy/cut. + // When keystroke is used to copy/cut this will also prevent + // conflict with copySingleWidget called again for native copy/cut event. + if ( doc.getById( 'cke_copybin' ) ) + return; + + // [IE] Use span for copybin and its container to avoid bug with expanding editable height by + // absolutely positioned element. + var copybinName = ( editor.blockless || CKEDITOR.env.ie ) ? 'span' : 'div', + copybin = doc.createElement( copybinName ), + copybinContainer = doc.createElement( copybinName ), + // IE8 always jumps to the end of document. + needsScrollHack = CKEDITOR.env.ie && CKEDITOR.env.version < 9; + + copybinContainer.setAttributes( { + id: 'cke_copybin', + 'data-cke-temp': '1' + } ); + + // Position copybin element outside current viewport. + copybin.setStyles( { + position: 'absolute', + width: '1px', + height: '1px', + overflow: 'hidden' + } ); + + copybin.setStyle( editor.config.contentsLangDirection == 'ltr' ? 'left' : 'right', '-5000px' ); + + copybin.setHtml( '\u200b' + widget.wrapper.getOuterHtml() + '\u200b' ); + + // Save snapshot with the current state. + editor.fire( 'saveSnapshot' ); + + // Ignore copybin. + editor.fire( 'lockSnapshot' ); + + copybinContainer.append( copybin ); + editor.editable().append( copybinContainer ); + + var listener1 = editor.on( 'selectionChange', cancel, null, null, 0 ), + listener2 = widget.repository.on( 'checkSelection', cancel, null, null, 0 ); + + if ( needsScrollHack ) { + var docElement = doc.getDocumentElement().$, + scrollTop = docElement.scrollTop; + } + + // Once the clone of the widget is inside of copybin, select + // the entire contents. This selection will be copied by the + // native browser's clipboard system. + var range = editor.createRange(); + range.selectNodeContents( copybin ); + range.select(); + + if ( needsScrollHack ) + docElement.scrollTop = scrollTop; + + setTimeout( function() { + // [IE] Focus widget before removing copybin to avoid scroll jump. + if ( !isCut ) + widget.focus(); + + copybinContainer.remove(); + + listener1.removeListener(); + listener2.removeListener(); + + editor.fire( 'unlockSnapshot' ); + + if ( isCut ) { + widget.repository.del( widget ); + editor.fire( 'saveSnapshot' ); + } + }, 100 ); // Use 100ms, so Chrome (@Mac) will be able to grab the content. + } + + // Extracts classes array from style instance. + function getStyleClasses( style ) { + var attrs = style.getDefinition().attributes, + classes = attrs && attrs[ 'class' ]; + + return classes ? classes.split( /\s+/ ) : null; + } + + // [IE] Force keeping focus because IE sometimes forgets to fire focus on main editable + // when blurring nested editable. + // @context widget + function onEditableBlur() { + var active = CKEDITOR.document.getActive(), + editor = this.editor, + editable = editor.editable(); + + // If focus stays within editor override blur and set currentActive because it should be + // automatically changed to editable on editable#focus but it is not fired. + if ( ( editable.isInline() ? editable : editor.document.getWindow().getFrame() ).equals( active ) ) + editor.focusManager.focus( editable ); + } + + // Force selectionChange when editable was focused. + // Similar to hack in selection.js#~620. + // @context widget + function onEditableFocus() { + // Gecko does not support 'DOMFocusIn' event on which we unlock selection + // in selection.js to prevent selection locking when entering nested editables. + if ( CKEDITOR.env.gecko ) + this.editor.unlockSelection(); + + // We don't need to force selectionCheck on Webkit, because on Webkit + // we do that on DOMFocusIn in selection.js. + if ( !CKEDITOR.env.webkit ) { + this.editor.forceNextSelectionCheck(); + this.editor.selectionChange( 1 ); + } + } + + // Setup listener on widget#data which will update (remove/add) classes + // by comparing newly set classes with the old ones. + function setupDataClassesListener( widget ) { + // Note: previousClasses and newClasses may be null! + // Tip: for ( cl in null ) is correct. + var previousClasses = null; + + widget.on( 'data', function() { + var newClasses = this.data.classes, + cl; + + // When setting new classes one need to remember + // that he must break reference. + if ( previousClasses == newClasses ) + return; + + for ( cl in previousClasses ) { + // Avoid removing and adding classes again. + if ( !( newClasses && newClasses[ cl ] ) ) + this.removeClass( cl ); + } + for ( cl in newClasses ) + this.addClass( cl ); + + previousClasses = newClasses; + } ); + } + + function setupDragHandler( widget ) { + if ( !widget.draggable ) + return; + + var editor = widget.editor, + // Use getLast to find wrapper's direct descendant (#12022). + container = widget.wrapper.getLast( isDomDragHandlerContainer ), + img; + + // Reuse drag handler if already exists (#11281). + if ( container ) + img = container.findOne( 'img' ); + else { + container = new CKEDITOR.dom.element( 'span', editor.document ); + container.setAttributes( { + 'class': 'cke_reset cke_widget_drag_handler_container', + // Split background and background-image for IE8 which will break on rgba(). + style: 'background:rgba(220,220,220,0.5);background-image:url(' + editor.plugins.widget.path + 'images/handle.png)' + } ); + + img = new CKEDITOR.dom.element( 'img', editor.document ); + img.setAttributes( { + 'class': 'cke_reset cke_widget_drag_handler', + 'data-cke-widget-drag-handler': '1', + src: CKEDITOR.tools.transparentImageData, + width: DRAG_HANDLER_SIZE, + title: editor.lang.widget.move, + height: DRAG_HANDLER_SIZE + } ); + widget.inline && img.setAttribute( 'draggable', 'true' ); + + container.append( img ); + widget.wrapper.append( container ); + } + + widget.wrapper.on( 'mouseenter', widget.updateDragHandlerPosition, widget ); + setTimeout( function() { + widget.on( 'data', widget.updateDragHandlerPosition, widget ); + }, 50 ); + + if ( widget.inline ) { + img.on( 'dragstart', function( evt ) { + evt.data.$.dataTransfer.setData( 'text', JSON.stringify( { type: 'cke-widget', editor: editor.name, id: widget.id } ) ); + } ); + } else { + img.on( 'mousedown', onBlockWidgetDrag, widget ); + } + + widget.dragHandlerContainer = container; + } + + function onBlockWidgetDrag() { + var finder = this.repository.finder, + locator = this.repository.locator, + liner = this.repository.liner, + editor = this.editor, + editable = editor.editable(), + listeners = [], + sorted = [], + + // Harvest all possible relations and display some closest. + relations = finder.greedySearch(), + + buffer = CKEDITOR.tools.eventsBuffer( 50, function() { + locations = locator.locate( relations ); + + // There's only a single line displayed for D&D. + sorted = locator.sort( y, 1 ); + + if ( sorted.length ) { + liner.prepare( relations, locations ); + liner.placeLine( sorted[ 0 ] ); + liner.cleanup(); + } + } ), + + locations, y; + + // Let's have the "dragging cursor" over entire editable. + editable.addClass( 'cke_widget_dragging' ); + + // Cache mouse position so it is re-used in events buffer. + listeners.push( editable.on( 'mousemove', function( evt ) { + y = evt.data.$.clientY; + buffer.input(); + } ) ); + + function onMouseUp() { + var l; + + buffer.reset(); + + // Stop observing events. + while ( ( l = listeners.pop() ) ) + l.removeListener(); + + onBlockWidgetDrop.call( this, sorted ); + } + + // Mouseup means "drop". This is when the widget is being detached + // from DOM and placed at range determined by the line (location). + listeners.push( editor.document.once( 'mouseup', onMouseUp, this ) ); + + // Mouseup may occur when user hovers the line, which belongs to + // the outer document. This is, of course, a valid listener too. + listeners.push( CKEDITOR.document.once( 'mouseup', onMouseUp, this ) ); + } + + function onBlockWidgetDrop( sorted ) { + var finder = this.repository.finder, + liner = this.repository.liner, + editor = this.editor, + editable = this.editor.editable(); + + if ( !CKEDITOR.tools.isEmpty( liner.visible ) ) { + // Retrieve range for the closest location. + var range = finder.getRange( sorted[ 0 ] ); + + // Focus widget (it could lost focus after mousedown+mouseup) + // and save this state as the one where we want to be taken back when undoing. + this.focus(); + editor.fire( 'saveSnapshot' ); + // Group all following operations in one snapshot. + editor.fire( 'lockSnapshot', { dontUpdate: 1 } ); + + // Reset the fake selection, which will be invalidated by insertElementIntoRange. + // This avoids a situation when getSelection() still returns a fake selection made + // on widget which in the meantime has been moved to other place. That could cause + // an error thrown e.g. by saveSnapshot or stateUpdater. + editor.getSelection().reset(); + + // Attach widget at the place determined by range. + editable.insertElementIntoRange( this.wrapper, range ); + + // Focus again the dropped widget. + this.focus(); + + // Unlock snapshot and save new one, which will contain all changes done + // in this method. + editor.fire( 'unlockSnapshot' ); + editor.fire( 'saveSnapshot' ); + } + + // Clean-up custom cursor for editable. + editable.removeClass( 'cke_widget_dragging' ); + + // Clean-up all remaining lines. + liner.hideVisible(); + } + + function setupEditables( widget ) { + var editableName, + editableDef, + definedEditables = widget.editables; + + widget.editables = {}; + + if ( !widget.editables ) + return; + + for ( editableName in definedEditables ) { + editableDef = definedEditables[ editableName ]; + widget.initEditable( editableName, typeof editableDef == 'string' ? { selector: editableDef } : editableDef ); + } + } + + function setupMask( widget ) { + if ( !widget.mask ) + return; + + // Reuse mask if already exists (#11281). + var img = widget.wrapper.findOne( '.cke_widget_mask' ); + + if ( !img ) { + img = new CKEDITOR.dom.element( 'img', widget.editor.document ); + img.setAttributes( { + src: CKEDITOR.tools.transparentImageData, + 'class': 'cke_reset cke_widget_mask' + } ); + widget.wrapper.append( img ); + } + + widget.mask = img; + } + + // Replace parts object containing: + // partName => selector pairs + // with: + // partName => element pairs + function setupParts( widget ) { + if ( widget.parts ) { + var parts = {}, + el, partName; + + for ( partName in widget.parts ) { + el = widget.wrapper.findOne( widget.parts[ partName ] ); + parts[ partName ] = el; + } + widget.parts = parts; + } + } + + function setupWidget( widget, widgetDef ) { + setupWrapper( widget ); + setupParts( widget ); + setupEditables( widget ); + setupMask( widget ); + setupDragHandler( widget ); + setupDataClassesListener( widget ); + + // #11145: [IE8] Non-editable content of widget is draggable. + if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) { + widget.wrapper.on( 'dragstart', function( evt ) { + var target = evt.data.getTarget(); + + // Allow text dragging inside nested editables or dragging inline widget's drag handler. + if ( !getNestedEditable( widget, target ) && !( widget.inline && isDomDragHandler( target ) ) ) + evt.data.preventDefault(); + } ); + } + + widget.wrapper.removeClass( 'cke_widget_new' ); + widget.element.addClass( 'cke_widget_element' ); + + widget.on( 'key', function( evt ) { + var keyCode = evt.data.keyCode; + + // ENTER. + if ( keyCode == 13 ) { + widget.edit(); + // CTRL+C or CTRL+X. + } else if ( keyCode == CKEDITOR.CTRL + 67 || keyCode == CKEDITOR.CTRL + 88 ) { + copySingleWidget( widget, keyCode == CKEDITOR.CTRL + 88 ); + return; // Do not preventDefault. + } else if ( keyCode in keystrokesNotBlockedByWidget || ( CKEDITOR.CTRL & keyCode ) || ( CKEDITOR.ALT & keyCode ) ) { + // Pass chosen keystrokes to other plugins or default fake sel handlers. + // Pass all CTRL/ALT keystrokes. + return; + } + + return false; + }, null, null, 999 ); + // Listen with high priority so it's possible + // to overwrite this callback. + + widget.on( 'doubleclick', function( evt ) { + if ( widget.edit() ) { + // We have to cancel event if edit method opens a dialog, otherwise + // link plugin may open extra dialog (#12140). + evt.cancel(); + } + } ); + + if ( widgetDef.data ) + widget.on( 'data', widgetDef.data ); + + if ( widgetDef.edit ) + widget.on( 'edit', widgetDef.edit ); + } + + function setupWidgetData( widget, startupData ) { + var widgetDataAttr = widget.element.data( 'cke-widget-data' ); + + if ( widgetDataAttr ) + widget.setData( JSON.parse( decodeURIComponent( widgetDataAttr ) ) ); + if ( startupData ) + widget.setData( startupData ); + + // Populate classes if they are not preset. + if ( !widget.data.classes ) + widget.setData( 'classes', widget.getClasses() ); + + // Unblock data and... + widget.dataReady = true; + + // Write data to element because this was blocked when data wasn't ready. + writeDataToElement( widget ); + + // Fire data event first time, because this was blocked when data wasn't ready. + widget.fire( 'data', widget.data ); + } + + function setupWrapper( widget ) { + // Retrieve widget wrapper. Assign an id to it. + var wrapper = widget.wrapper = widget.element.getParent(); + wrapper.setAttribute( 'data-cke-widget-id', widget.id ); + } + + function writeDataToElement( widget ) { + widget.element.data( 'cke-widget-data', encodeURIComponent( JSON.stringify( widget.data ) ) ); + } + + // + // WIDGET STYLE HANDLER --------------------------------------------------- + // + + ( function() { + + /** + * The class representing a widget style. It is an {@link CKEDITOR#STYLE_OBJECT object} like + * the styles handler for widgets. + * + * **Note:** This custom style handler does not support all methods of the {@link CKEDITOR.style} class. + * Not supported methods: {@link #applyToRange}, {@link #removeFromRange}, {@link #applyToObject}. + * + * @since 4.4 + * @class CKEDITOR.style.customHandlers.widget + * @extends CKEDITOR.style + */ + CKEDITOR.style.addCustomHandler( { + type: 'widget', + + setup: function( styleDefinition ) { + /** + * The name of widget to which this style can be applied. + * It is extracted from style definition's `widget` property. + * + * @property {String} widget + */ + this.widget = styleDefinition.widget; + }, + + apply: function( editor ) { + // Before CKEditor 4.4 wasn't a required argument, so we need to + // handle a case when it wasn't provided. + if ( !( editor instanceof CKEDITOR.editor ) ) + return; + + // Theoretically we could bypass checkApplicable, get widget from + // widgets.focused and check its name, what would be faster, but then + // this custom style would work differently than the default style + // which checks if it's applicable before applying or removeing itself. + if ( this.checkApplicable( editor.elementPath(), editor ) ) + editor.widgets.focused.applyStyle( this ); + }, + + remove: function( editor ) { + // Before CKEditor 4.4 wasn't a required argument, so we need to + // handle a case when it wasn't provided. + if ( !( editor instanceof CKEDITOR.editor ) ) + return; + + if ( this.checkApplicable( editor.elementPath(), editor ) ) + editor.widgets.focused.removeStyle( this ); + }, + + checkActive: function( elementPath, editor ) { + return this.checkElementMatch( elementPath.lastElement, 0, editor ); + }, + + checkApplicable: function( elementPath, editor ) { + // Before CKEditor 4.4 wasn't a required argument, so we need to + // handle a case when it wasn't provided. + if ( !( editor instanceof CKEDITOR.editor ) ) + return false; + + return this.checkElement( elementPath.lastElement ); + }, + + checkElementMatch: checkElementMatch, + + checkElementRemovable: checkElementMatch, + + /** + * Checks if an element is a {@link CKEDITOR.plugins.widget#wrapper wrapper} of a + * widget whose name matches the {@link #widget widget name} specified in the style definition. + * + * @param {CKEDITOR.dom.element} element + * @returns {Boolean} + */ + checkElement: function( element ) { + if ( !isDomWidgetWrapper( element ) ) + return false; + + var widgetElement = element.getFirst( isDomWidgetElement ); + return widgetElement && widgetElement.data( 'widget' ) == this.widget; + }, + + buildPreview: function( label ) { + return label || this._.definition.name; + }, + + /** + * Returns allowed content rules which should be registered for this style. + * Uses widget's {@link CKEDITOR.plugins.widget.definition#styleableElements} to make a rule + * allowing classes on specified elements or use widget's + * {@link CKEDITOR.plugins.widget.definition#styleToAllowedContentRules} method to transform a style + * into allowed content rules. + * + * @param {CKEDITOR.editor} The editor instance. + * @returns {CKEDITOR.filter.allowedContentRules} + */ + toAllowedContentRules: function( editor ) { + if ( !editor ) + return null; + + var widgetDef = editor.widgets.registered[ this.widget ], + classes, + rule = {}; + + if ( !widgetDef ) + return null; + + if ( widgetDef.styleableElements ) { + classes = this.getClassesArray(); + if ( !classes ) + return null; + + rule[ widgetDef.styleableElements ] = { + classes: classes, + propertiesOnly: true + }; + return rule; + } + if ( widgetDef.styleToAllowedContentRules ) + return widgetDef.styleToAllowedContentRules( this ); + return null; + }, + + /** + * Returns classes defined in the style in form of an array. + * + * @returns {String[]} + */ + getClassesArray: function() { + var classes = this._.definition.attributes && this._.definition.attributes[ 'class' ]; + + return classes ? CKEDITOR.tools.trim( classes ).split( /\s+/ ) : null; + }, + + /** + * Not implemented. + * + * @method applyToRange + */ + applyToRange: notImplemented, + + /** + * Not implemented. + * + * @method removeFromRange + */ + removeFromRange: notImplemented, + + /** + * Not implemented. + * + * @method applyToObject + */ + applyToObject: notImplemented + } ); + + function notImplemented() {} + + // @context style + function checkElementMatch( element, fullMatch, editor ) { + // Before CKEditor 4.4 wasn't a required argument, so we need to + // handle a case when it wasn't provided. + if ( !editor ) + return false; + + if ( !this.checkElement( element ) ) + return false; + + var widget = editor.widgets.getByElement( element, true ); + return widget && widget.checkStyleActive( this ); + } + + } )(); + + // + // EXPOSE PUBLIC API ------------------------------------------------------ + // + + CKEDITOR.plugins.widget = Widget; + Widget.repository = Repository; + Widget.nestedEditable = NestedEditable; +} )(); + +/** + * An event fired when a widget definition is registered by the {@link CKEDITOR.plugins.widget.repository#add} method. + * It is possible to modify the definition being registered. + * + * @event widgetDefinition + * @member CKEDITOR.editor + * @param {CKEDITOR.plugins.widget.definition} data Widget definition. + */ + +/** + * This is an abstract class that describes the definition of a widget. + * It is a type of {@link CKEDITOR.plugins.widget.repository#add} method's second argument. + * + * Widget instances inherit from registered widget definitions, although not in a prototypal way. + * They are simply extended with corresponding widget definitions. Note that not all properties of + * the widget definition become properties of a widget. Some, like {@link #data} or {@link #edit}, become + * widget's events listeners. + * + * @class CKEDITOR.plugins.widget.definition + * @abstract + * @mixins CKEDITOR.feature + */ + +/** + * Widget definition name. It is automatically set when the definition is + * {@link CKEDITOR.plugins.widget.repository#add registered}. + * + * @property {String} name + */ + +/** + * The method executed while initializing a widget, after a widget instance + * is created, but before it is ready. It is executed before the first + * {@link CKEDITOR.plugins.widget#event-data} is fired so it is common to + * use the `init` method to populate widget data with information loaded from + * the DOM, like for exmaple: + * + * init: function() { + * this.setData( 'width', this.element.getStyle( 'width' ) ); + * + * if ( this.parts.caption.getStyle( 'display' ) != 'none' ) + * this.setData( 'showCaption', true ); + * } + * + * @property {Function} init + */ + +/** + * The function to be used to upcast an element to this widget or a + * comma-separated list of upcast methods from the {@link #upcasts} object. + * + * The upcast function **is not** executed in the widget context (because the widget + * does not exist yet) and two arguments are passed: + * + * * `element` ({@link CKEDITOR.htmlParser.element}) – The element to be checked. + * * `data` (`Object`) – The object which can be extended with data which will then be passed to the widget. + * + * An element will be upcasted if a function returned `true` or an instance of + * a {@link CKEDITOR.htmlParser.element} if upcasting meant DOM structure changes + * (in this case the widget will be initialized on the returned element). + * + * @property {String/Function} upcast + */ + +/** + * The object containing functions which can be used to upcast this widget. + * Only those pointed by the {@link #upcast} property will be used. + * + * In most cases it is appropriate to use {@link #upcast} directly, + * because majority of widgets need just one method. + * However, in some cases the widget author may want to expose more than one variant + * and then this property may be used. + * + * upcasts: { + * // This function may upcast only figure elements. + * figure: function() { + * // ... + * }, + * // This function may upcast only image elements. + * image: function() { + * // ... + * }, + * // More variants... + * } + * + * // Then, widget user may choose which upcast methods will be enabled. + * editor.on( 'widgetDefinition', function( evt ) { + * if ( evt.data.name == 'image' ) + * evt.data.upcast = 'figure,image'; // Use both methods. + * } ); + * + * @property {Object} upcasts + */ + +/** + * The function to be used to downcast this widget or + * a name of the downcast option from the {@link #downcasts} object. + * + * The downcast funciton will be executed in the {@link CKEDITOR.plugins.widget} context + * and with `widgetElement` ({@link CKEDITOR.htmlParser.element}) argument which is + * the widget's main element. + * + * The function may return an instance of the {@link CKEDITOR.htmlParser.node} class if the widget + * needs to be downcasted to a different node than the widget's main element. + * + * @property {String/Function} downcast + */ + +/** + * The object containing functions which can be used to downcast this widget. + * Only the one pointed by the {@link #downcast} property will be used. + * + * In most cases it is appropriate to use {@link #downcast} directly, + * because majority of widgets have just one variant of downcasting (or none at all). + * However, in some cases the widget author may want to expose more than one variant + * and then this property may be used. + * + * downcasts: { + * // This downcast may transform the widget into the figure element. + * figure: function() { + * // ... + * }, + * // This downcast may transform the widget into the image element with data-* attributes. + * image: function() { + * // ... + * } + * } + * + * // Then, the widget user may choose one of the downcast options when setting up his editor. + * editor.on( 'widgetDefinition', function( evt ) { + * if ( evt.data.name == 'image' ) + * evt.data.downcast = 'figure'; + * } ); + * + * @property downcasts + */ + +/** + * If set, it will be added as the {@link CKEDITOR.plugins.widget#event-edit} event listener. + * This means that it will be executed when a widget is being edited. + * See the {@link CKEDITOR.plugins.widget#method-edit} method. + * + * @property {Function} edit + */ + +/** + * If set, it will be added as the {@link CKEDITOR.plugins.widget#event-data} event listener. + * This means that it will be executed every time the {@link CKEDITOR.plugins.widget#property-data widget data} changes. + * + * @property {Function} data + */ + +/** + * The method to be executed when the widget's command is executed in order to insert a new widget + * (widget of this type is not focused). If not defined, then the default action will be + * performed which means that: + * + * * An instance of the widget will be created in a detached {@link CKEDITOR.dom.documentFragment document fragment}, + * * The {@link CKEDITOR.plugins.widget#method-edit} method will be called to trigger widget editing, + * * The widget element will be inserted into DOM. + * + * @property {Function} insert + */ + +/** + * The name of a dialog window which will be opened on {@link CKEDITOR.plugins.widget#method-edit}. + * If not defined, then the {@link CKEDITOR.plugins.widget#method-edit} method will not perform any action and + * widget's command will insert a new widget without opening a dialog window first. + * + * @property {String} dialog + */ + +/** + * The template which will be used to create a new widget element (when the widget's command is executed). + * This string is populated with {@link #defaults default values} by using the {@link CKEDITOR.template} format. + * Therefore it has to be a valid {@link CKEDITOR.template} argument. + * + * @property {String} template + */ + +/** + * The data object which will be used to populate the data of a newly created widget. + * See {@link CKEDITOR.plugins.widget#property-data}. + * + * defaults: { + * showCaption: true, + * align: 'none' + * } + * + * @property defaults + */ + +/** + * An object containing definitions of widget components (part name => CSS selector). + * + * parts: { + * image: 'img', + * caption: 'div.caption' + * } + * + * @property parts + */ + +/** + * An object containing definitions of nested editables (editable name => {@link CKEDITOR.plugins.widget.nestedEditable.definition}). + * + * editables: { + * header: 'h1', + * content: { + * selector: 'div.content', + * allowedContent: 'p strong em; a[!href]' + * } + * } + * + * @property editables + */ + +/** + * Widget name displayed in elements path. + * + * @property {String} pathName + */ + +/** + * If set to `true`, the widget's element will be covered with a transparent mask. + * This will prevent its content from being clickable, which matters in case + * of special elements like embedded Flash or iframes that generate a separate "context". + * + * @property {Boolean} mask + */ + +/** + * If set to `true/false`, it will force the widget to be either an inline or a block widget. + * If not set, the widget type will be determined from the widget element. + * + * Widget type influences whether a block (`div`) or an inline (`span`) element is used + * for the wrapper. + * + * @property {Boolean} inline + */ + +/** + * The label for the widget toolbar button. + * + * editor.widgets.add( 'simplebox', { + * button: 'Create a simple box' + * } ); + * + * editor.widgets.add( 'simplebox', { + * button: editor.lang.simplebox.title + * } ); + * + * @property {String} button + */ + +/** + * Whether widget should be draggable. Defaults to `true`. + * If set to `false` drag handler will not be displayed when hovering widget. + * + * @property {Boolean} draggable + */ + +/** + * Names of element(s) (separated by spaces) for which the {@link CKEDITOR.filter} should allow classes + * defined in the widget styles. For example if your widget is upcasted from a simple `
` + * element, then in order to make it styleable you can set: + * + * editor.widgets.add( 'customWidget', { + * upcast: function( element ) { + * return element.name == 'div'; + * }, + * + * // ... + * + * styleableElements: 'div' + * } ); + * + * Then, when the following style is defined: + * + * { + * name: 'Thick border', type: 'widget', widget: 'customWidget', + * attributes: { 'class': 'thickBorder' } + * } + * + * a rule allowing the `thickBorder` class for `div` elements will be registered in the {@link CKEDITOR.filter}. + * + * If you need to have more freedom when transforming widget style to allowed content rules, + * you can use the {@link #styleToAllowedContentRules} callback. + * + * @since 4.4 + * @property {String} styleableElements + */ + +/** + * Function transforming custom widget's {@link CKEDITOR.style} instance into + * {@link CKEDITOR.filter.allowedContentRules}. It may be used when a static + * {@link #styleableElements} property is not enough to inform the {@link CKEDITOR.filter} + * what HTML features should be enabled when allowing the given style. + * + * In most cases, when style's classes just have to be added to element name(s) used by + * the widget element, it is recommended to use simpler {@link #styleableElements} property. + * + * In order to get parsed classes from the style definition you can use + * {@link CKEDITOR.style.customHandlers.widget#getClassesArray}. + * + * For example, if you want to use the [object format of allowed content rules](#!/guide/dev_allowed_content_rules-section-object-format), + * to specify `match` validator, your implementation could look like this: + * + * editor.widgets.add( 'customWidget', { + * // ... + * + * styleToAllowedContentRules: funciton( style ) { + * // Retrieve classes defined in the style. + * var classes = style.getClassesArray(); + * + * // Do something crazy - for example return allowed content rules in object format, + * // with custom match property and propertiesOnly flag. + * return { + * h1: { + * match: isWidgetElement, + * propertiesOnly: true, + * classes: classes + * } + * }; + * } + * } ); + * + * @since 4.4 + * @property {Function} styleToAllowedContentRules + * @param {CKEDITOR.style.customHandlers.widget} style The style to be transformed. + * @returns {CKEDITOR.filter.allowedContentRules} + */ + +/** + * This is an abstract class that describes the definition of a widget's nested editable. + * It is a type of values in the {@link CKEDITOR.plugins.widget.definition#editables} object. + * + * In the simplest case the definition is a string which is a CSS selector used to + * find an element that will become a nested editable inside the widget. Note that + * the widget element can be a nested editable, too. + * + * In the more advanced case a definition is an object with a required `selector` property. + * + * editables: { + * header: 'h1', + * content: { + * selector: 'div.content', + * allowedContent: 'p strong em; a[!href]' + * } + * } + * + * @class CKEDITOR.plugins.widget.nestedEditable.definition + * @abstract + */ + +/** + * The CSS selector used to find an element which will become a nested editable. + * + * @property {String} selector + */ + +/** + * The [Advanced Content Filter](#!/guide/dev_advanced_content_filter) rules + * which will be used to limit the content allowed in this nested editable. + * This option is similar to {@link CKEDITOR.config#allowedContent} and one can + * use it to limit the editor features available in the nested editable. + * + * @property {CKEDITOR.filter.allowedContentRules} allowedContent + */ + +/** + * Nested editable name displayed in elements path. + * + * @property {String} pathName + */ \ No newline at end of file diff --git a/htmlarea/ckeditor_s9y_config.js b/htmlarea/ckeditor_s9y_config.js new file mode 100644 index 00000000..519e075e --- /dev/null +++ b/htmlarea/ckeditor_s9y_config.js @@ -0,0 +1,272 @@ +/** + * @fileOverview The Serendipity CKEDITOR custom config file: + * ckeditor_s9y_config.js, v. 1.7, last modified 2014-11-17 by Ian + */ + +/** + * Substitute every config option to CKEDITOR in here, + * rename the file to ckeditor_custom_config.js and + * copy it to /templates/2k11/admin. + * In there change anything to be upgrade independent + * NOTE: Use at own risk! Be careful! + */ +CKEDITOR.editorConfig = function( config ) { + + // ACF - Advanced Content Filter works in two modes: + // automatic – the filter is configured by editor features (like plugins, buttons, and commands) that are enabled with configuration options + // such as CKEDITOR.config.plugins, CKEDITOR.config.extraPlugins, and CKEDITOR.config.toolbar, + // custom – the filter is configured by the CKEDITOR.config.allowedContent option and only features that match this setting are activated. + // In both modes it is possible to extend the filter configuration by using the CKEDITOR.config.extraAllowedContent setting. + // If you want to disable Advanced Content Filter, set CKEDITOR.config.allowedContent to true. + // All available editor features will be activated and input data will not be filtered. + // Allowed content rules. This setting is used when instantiating CKEDITOR.editor.filter. + // The following values are accepted: + // CKEDITOR.filter.allowedContentRules – defined rules will be added to the CKEDITOR.editor.filter. + // true – will disable the filter (data will not be filtered, all features will be activated). + // default – the filter will be configured by loaded features (toolbar items, commands, etc.). + // In all cases filter configuration may be extended by extraAllowedContent. This option may be especially useful, + // when you want to use the default allowedContent value along with some additional rules. + // + // List of regular expressions to be executed on ***input HTML***, indicating HTML source code that, when matched, must not be available in the WYSIWYG mode for editing. + + // allow + {/if} - \ No newline at end of file +