2015-11-28 23:10:25 +01:00
/ * *
* Some UI experiments for CodeX Editor
* @ author Savchenko Peter ( vk . com / specc )
* /
2015-12-25 15:06:43 +01:00
/ *
* TODO
* выделение нескольких блоков и нажатие энтера - вместо замены новой стро
*
* * /
2015-12-09 01:34:32 +01:00
var ce = function ( settings ) {
2015-11-28 23:10:25 +01:00
2015-12-10 22:26:44 +01:00
this . resultTextarea = document . getElementById ( "codex_editor" ) ;
2015-11-28 23:10:25 +01:00
if ( typeof this . resultTextarea == undefined || this . resultTextarea == null ) {
2015-12-09 01:34:32 +01:00
console . warn ( 'Textarea not found with ID %o' , this . textareaId ) ;
2015-11-28 23:10:25 +01:00
return this ;
}
2015-12-10 22:26:44 +01:00
/* Prepare settings */
if ( "undefined" == typeof settings ) settings = this . defaultSettings ;
2015-12-09 01:34:32 +01:00
else {
// todo just merge settings with defaults
}
2015-12-10 22:26:44 +01:00
settings . tools = settings . tools || this . allTools ;
this . settings = settings ;
2015-12-09 01:34:32 +01:00
2015-11-28 23:10:25 +01:00
/** Making a wrapper and interface */
this . makeInterface ( ) ;
2016-01-01 22:01:37 +01:00
/ *
* Импорт содержимого textarea в редактор
* * /
this . importHtml ( ) ;
2015-11-28 23:10:25 +01:00
/** Bind all events */
this . bindEvents ( ) ;
2015-12-09 01:34:32 +01:00
} ;
2015-11-28 23:10:25 +01:00
2015-12-10 22:26:44 +01:00
// All posible tools
2016-07-01 20:34:29 +02:00
ce . prototype . allTools = [ 'header' , 'picture' , 'list' , 'quote' , 'code' , 'link' , 'twitter' , 'instagram' , 'smile' ] ;
2015-12-10 22:26:44 +01:00
// Default settings configuration
ce . prototype . defaultSettings = {
} ;
// Add this class when open tool bar for css animation
ce . prototype . BUTTONS _TOGGLED _CLASSNANE = 'buttons_toggled' ;
// Default tool bar is closed
ce . prototype . toolbarOpened = false ;
// Key event constants
2015-12-17 23:41:36 +01:00
ce . prototype . key = { TAB : 9 , ENTER : 13 , BACKSPACE : 8 , DELETE : 46 , SPACE : 32 , ESC : 27 , CTRL : 17 , META : 91 , SHIFT : 16 , ALT : 18 , LEFT : 37 , UP : 38 , DOWN : 40 , RIGHT : 39 } ;
2015-12-10 22:26:44 +01:00
2015-11-28 23:10:25 +01:00
/ * *
2015-12-10 22:26:44 +01:00
* Editor interface drawing
* calls one time in editor constructor
2015-11-28 23:10:25 +01:00
* /
ce . prototype . makeInterface = function ( ) {
var wrapper = this . make . editorWrapper ( ) ,
2015-12-09 01:34:32 +01:00
firstNode = this . make . textNode ( 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quia nihil repellendus aut cupiditate reprehenderit sapiente magnam nobis doloremque eaque! Sint nobis assumenda nisi ducimus minima illo tenetur, cumque facilis.' ) ,
2015-12-16 22:36:19 +01:00
toolbar = this . make . toolbar ( ) ,
editableWrapper ;
2015-11-28 23:10:25 +01:00
2015-12-09 01:34:32 +01:00
this . wrapper = wrapper ;
2015-12-16 22:36:19 +01:00
this . editableWrapper = editableWrapper = wrapper . getElementsByClassName ( "ce_content" ) [ 0 ] ;
2015-12-09 01:34:32 +01:00
this . toolbar = toolbar ;
2015-12-10 22:26:44 +01:00
this . toolbarButtons = this . make . toolbarButtons ( this . allTools , this . settings . tools ) ;
2015-12-09 01:34:32 +01:00
2015-12-17 23:41:36 +01:00
toolbar . appendChild ( this . toolbarButtons ) ;
2015-12-10 22:26:44 +01:00
/** Add first node and tool bar*/
2015-12-16 22:36:19 +01:00
editableWrapper . appendChild ( firstNode ) ;
2015-12-09 01:34:32 +01:00
wrapper . appendChild ( toolbar ) ;
2015-11-28 23:10:25 +01:00
/** Insert Editor after initial textarea. Hide textarea */
this . resultTextarea . parentNode . insertBefore ( wrapper , this . resultTextarea . nextSibling ) ;
2016-01-01 22:12:06 +01:00
this . resultTextarea . hidden = true ;
2015-11-28 23:10:25 +01:00
2015-12-10 22:26:44 +01:00
this . focusNode ( firstNode ) ;
2015-12-09 01:34:32 +01:00
} ;
2015-11-28 23:10:25 +01:00
2015-12-29 19:44:32 +01:00
/ *
* Экспорт разметки в итоговый текстареа
* пока по кнопке "экспорт" , потом можно сделать на каждое изменение в редакторе ( надо ли это ? )
* * /
ce . prototype . exportHtml = function ( ) {
2016-01-01 22:12:06 +01:00
this . resultTextarea . innerHTML = this . editableWrapper . innerHTML ;
this . resultTextarea . value = this . editableWrapper . innerHTML ;
2015-12-29 19:44:32 +01:00
return false ;
} ;
2016-01-01 22:01:37 +01:00
/ * *
2015-12-29 19:44:32 +01:00
* Импорт разметки из итоговой текстареа
* пока по кнопке "импорт" , потом можно сделать на каждое изменение в редакторе ( надо ли это ? )
2016-01-01 22:01:37 +01:00
*
* TODO
* 1 ) удалить лишние узлы , работа с которыми не предполагается в рамках редактора
* 2 ) удалить скрипты , стили
* 3 ) поочищать содержимое узлов от мусора - должен остаться только текст , теги форматирования ( жирность и тд ) и переносы строк ( или их тоже убираем ? )
2015-12-29 19:44:32 +01:00
* * /
ce . prototype . importHtml = function ( ) {
2016-01-01 22:01:37 +01:00
var node , body , i , nodeType , tmp ;
/ *
* Парсим содержимое textarea .
* Создаем новый документ , получаем указатель на контенейр body .
* * /
tmp = new DOMParser ( ) . parseFromString ( this . resultTextarea . value , "text/html" ) ;
body = tmp . getElementsByTagName ( "body" ) [ 0 ] ;
/ *
* Обходим корневые узлы . Проставляем им класс и тип узла .
* * /
for ( i = 0 ; i < body . children . length ; i ++ ) {
node = body . children . item ( i ) ;
if ( ! node . classList . contains ( "node" ) )
node . classList . add ( "node" ) ;
switch ( node . tagName ) {
case "P" :
nodeType = "text" ;
break ;
case "H1" :
case "H2" :
case "H3" :
case "H4" :
case "H5" :
case "H6" :
nodeType = "header" ;
break ;
case "UL" :
nodeType = "list" ;
break ;
case "IMG" :
nodeType = "picture" ;
break ;
case "CODE" :
nodeType = "code" ;
break ;
}
node . dataset [ "type" ] = nodeType ;
}
this . editableWrapper . innerHTML = body . innerHTML ;
2015-12-29 19:44:32 +01:00
} ;
2015-11-28 23:10:25 +01:00
/ * *
* All events binds in one place
* /
ce . prototype . bindEvents = function ( ) {
2015-12-16 22:36:19 +01:00
var _this = this ,
selectedNodeClass = "selected" ;
2015-11-28 23:10:25 +01:00
2015-12-29 19:44:32 +01:00
/ *
2016-01-01 22:12:06 +01:00
* Экспорт разметки в итоговый textarea по нажатию на кнопку "сохранить" .
* Кнопка сохранения должна иметь , так же как и textarea , особенный ID .
2015-12-29 19:44:32 +01:00
* * /
2016-01-01 22:12:06 +01:00
document . getElementById ( "codex_editor_export_btn" ) . addEventListener ( 'click' , function ( ) {
2015-12-29 19:44:32 +01:00
_this . exportHtml . apply ( _this )
} ) ;
2015-11-28 23:10:25 +01:00
/** All keydowns on Window */
2015-12-16 22:36:19 +01:00
document . addEventListener ( 'keydown' , function ( event ) {
2015-11-28 23:10:25 +01:00
_this . globalKeydownCallback ( event ) ;
} , false ) ;
2015-12-16 22:36:19 +01:00
2015-12-29 22:41:19 +01:00
/** All mouseover on Window */
2015-12-17 23:41:36 +01:00
document . addEventListener ( 'mouseover' , function ( event ) {
2015-12-29 22:41:19 +01:00
_this . globalMouseOverCallback ( event ) ;
} , false ) ;
2015-12-17 23:41:36 +01:00
2015-12-16 22:36:19 +01:00
2015-12-29 22:41:19 +01:00
/** All mouseout on Window */
document . addEventListener ( 'mouseout' , function ( event ) {
_this . globalMouseOutCallback ( event ) ;
2015-12-17 23:41:36 +01:00
} , false ) ;
2015-12-29 22:41:19 +01:00
} ;
2015-12-29 21:18:32 +01:00
2015-12-17 23:41:36 +01:00
2015-12-29 22:41:19 +01:00
/ * *
* All window mouseover handles here
* /
ce . prototype . globalMouseOverCallback = function ( event ) {
var sender = event . target ;
if ( sender . classList . contains ( "node" ) && ! this . toolbar . isOpened ) {
var toolbar = this . toolbar ;
2015-12-17 23:41:36 +01:00
2015-12-29 22:41:19 +01:00
toolbar . style . top = sender . offsetTop + "px" ;
2015-12-16 22:36:19 +01:00
2015-12-29 22:41:19 +01:00
toolbar . classList . add ( "show" ) ;
}
2015-12-09 01:34:32 +01:00
} ;
2015-11-28 23:10:25 +01:00
2015-12-29 22:41:19 +01:00
/ * *
* All window mouseout handles here
* /
ce . prototype . globalMouseOutCallback = function ( event ) {
var sender = event . target ;
if ( ! this . toolbar . isOpened ) {
var toolbar = this . toolbar ;
toolbar . classList . remove ( "show" ) ;
}
} ;
2015-12-10 22:26:44 +01:00
/ * *
* Sets focus to node conteneditable child
* todo depending on node type
* /
ce . prototype . focusNode = function ( node ) {
2015-12-16 22:36:19 +01:00
node . focus ( ) ;
2015-12-29 21:18:32 +01:00
2015-12-16 22:36:19 +01:00
if ( typeof window . getSelection != "undefined" && typeof document . createRange != "undefined" ) {
var range = document . createRange ( ) ;
range . selectNodeContents ( node ) ;
range . collapse ( false ) ;
var sel = window . getSelection ( ) ;
sel . removeAllRanges ( ) ;
sel . addRange ( range ) ;
} else if ( typeof document . body . createTextRange != "undefined" ) {
var textRange = document . body . createTextRange ( ) ;
textRange . moveToElementText ( node ) ;
textRange . collapse ( false ) ;
textRange . select ( ) ;
}
2015-12-10 22:26:44 +01:00
} ;
2015-12-29 19:44:32 +01:00
/ *
2015-12-29 21:18:32 +01:00
* Определяет , есть ли выделенный текст
2015-12-29 19:44:32 +01:00
* * /
ce . prototype . isTextSelected = function ( ) {
return ! ! window . getSelection ( ) . toString ( )
2015-12-29 21:18:32 +01:00
} ;
2015-12-29 19:44:32 +01:00
2015-12-29 22:41:19 +01:00
/ *
* Определяет , относится ли нажатая кнопка к навигационным
* * /
ce . prototype . isNavigationKey = function ( keyCode ) {
return keyCode == this . key . LEFT || keyCode == this . key . UP || keyCode == this . key . DOWN || keyCode == this . key . RIGHT
} ;
2015-11-28 23:10:25 +01:00
/ * *
* All window keydowns handles here
* /
ce . prototype . globalKeydownCallback = function ( event ) {
2015-12-25 15:06:43 +01:00
2015-12-29 22:41:19 +01:00
/ * *
* Обработка клавиш на панеле добавления
* /
this . processToolBarKeyPressed ( event ) ;
2015-12-25 15:06:43 +01:00
2015-12-29 22:41:19 +01:00
//
switch ( event . keyCode ) {
case this . key . TAB : this . tabKeyPressed ( event ) ; break ; // TAB
case this . key . ENTER : this . enterKeyPressed ( event ) ; break ; // Enter
}
2015-12-25 15:06:43 +01:00
2015-12-29 22:41:19 +01:00
} ;
2015-12-25 15:06:43 +01:00
2015-12-29 22:41:19 +01:00
/ * *
* Обрабатывает нажатие клавиш при открытой панеле добавления
* /
ce . prototype . processToolBarKeyPressed = function ( event ) {
if ( this . toolbar . isOpened ) {
2015-12-25 15:06:43 +01:00
2015-12-29 22:41:19 +01:00
if ( this . isNavigationKey ( event . which ) ) {
2015-12-25 15:06:43 +01:00
2015-12-29 22:41:19 +01:00
this . moveToolBarButtonFocus ( event . which == this . key . LEFT || event . which == this . key . UP ) ;
2015-12-17 23:41:36 +01:00
event . preventDefault ( ) ;
2015-12-25 15:06:43 +01:00
2015-12-29 22:41:19 +01:00
} else if ( event . which == this . key . ENTER ) {
// will process later
2015-12-17 23:41:36 +01:00
} else if ( event . which != this . key . TAB && event . which != this . key . SHIFT ) {
2015-12-29 22:41:19 +01:00
this . closeToolBar ( ) ;
2015-12-17 23:41:36 +01:00
}
2015-11-28 23:10:25 +01:00
}
2015-12-29 22:41:19 +01:00
} ;
2015-11-28 23:10:25 +01:00
2015-12-17 23:41:36 +01:00
2015-12-29 22:41:19 +01:00
/ * *
* Closes tool bar ( plus btn )
* /
ce . prototype . closeToolBar = function ( ) {
var _this = this ,
toolbar = this . toolbar ;
toolbar . isOpened = false ;
this . focusedToolbarBtn . classList . remove ( "focused" ) ;
this . focusedToolbarBtn = false ;
// repair buttons animation - just add css class async
setTimeout ( function ( ) {
toolbar . classList . remove ( "show" ) ;
toolbar . classList . remove ( _this . BUTTONS _TOGGLED _CLASSNANE ) ;
} ) ;
2015-12-09 01:34:32 +01:00
} ;
2015-11-28 23:10:25 +01:00
2015-12-29 22:41:19 +01:00
2015-12-25 15:06:43 +01:00
/ * *
* Returns node which is currently focused
* /
2015-12-29 21:18:32 +01:00
ce . prototype . getFocusedNode = function ( ) {
2015-12-25 15:06:43 +01:00
var sel = window . getSelection ( ) ;
return sel . anchorNode . tagName ? sel . anchorNode : sel . focusNode . parentElement ;
2015-12-29 21:18:32 +01:00
} ;
2015-12-25 15:06:43 +01:00
2015-11-28 23:10:25 +01:00
/ * *
2015-12-09 01:34:32 +01:00
*
2015-11-28 23:10:25 +01:00
* /
ce . prototype . tabKeyPressed = function ( event ) {
2015-12-09 01:34:32 +01:00
// check if currently focused in contenteditable element
2015-12-10 22:26:44 +01:00
if ( "BODY" == event . target . tagName ) return ;
2015-11-28 23:10:25 +01:00
2015-12-17 23:41:36 +01:00
var _this = this ;
2015-12-09 01:34:32 +01:00
2015-12-17 23:41:36 +01:00
var toolbar = this . toolbar ;
2015-12-09 01:34:32 +01:00
2015-12-17 23:41:36 +01:00
if ( ! toolbar . isOpened ) {
var sel = window . getSelection ( ) ;
var curNode = sel . anchorNode . tagName ? sel . anchorNode : sel . focusNode . parentElement ;
2015-12-16 22:36:19 +01:00
2015-12-17 23:41:36 +01:00
toolbar . style . top = curNode . offsetTop + "px" ;
2015-12-16 22:36:19 +01:00
2015-12-17 23:41:36 +01:00
if ( ! toolbar . classList . contains ( _this . BUTTONS _TOGGLED _CLASSNANE ) ) {
// repair buttons animation - just add css class async
setTimeout ( function ( ) {
toolbar . classList . add ( _this . BUTTONS _TOGGLED _CLASSNANE )
toolbar . isOpened = true ;
} ) ;
2015-12-09 01:34:32 +01:00
}
2015-12-17 23:41:36 +01:00
}
2015-11-28 23:10:25 +01:00
2015-12-17 23:41:36 +01:00
//
this . moveToolBarButtonFocus ( event . shiftKey ) ;
2015-11-28 23:10:25 +01:00
2015-12-17 23:41:36 +01:00
event . preventDefault ( ) ;
2015-12-09 01:34:32 +01:00
} ;
2015-11-28 23:10:25 +01:00
2015-12-29 21:18:32 +01:00
/ * *
* Перемещает фокус на следующую кнопку в панеле добавления ( плюс )
2015-12-17 23:41:36 +01:00
* * /
ce . prototype . moveToolBarButtonFocus = function ( focusPrev ) {
var allButtons = this . toolbarButtons ;
2015-12-29 21:18:32 +01:00
var focusedQuery = allButtons . getElementsByClassName ( "focused" ) ;
2015-12-17 23:41:36 +01:00
var focused ;
if ( focusedQuery . length > 0 ) {
2015-12-29 21:18:32 +01:00
focused = focusedQuery [ 0 ] ;
2015-12-17 23:41:36 +01:00
2015-12-29 21:18:32 +01:00
focused . classList . remove ( "focused" ) ;
2015-12-17 23:41:36 +01:00
if ( focusPrev ) focused = focused . previousSibling ;
else focused = focused . nextSibling ;
if ( ! focused ) {
if ( focusPrev ) focused = allButtons . lastChild ;
else focused = allButtons . firstChild ;
}
2015-12-29 21:18:32 +01:00
focused . classList . add ( "focused" ) ;
2015-12-17 23:41:36 +01:00
} else {
focused = allButtons . firstChild ;
2015-12-29 21:18:32 +01:00
focused . classList . add ( "focused" ) ;
2015-12-17 23:41:36 +01:00
}
this . focusedToolbarBtn = focused ;
2015-12-29 21:18:32 +01:00
} ;
2015-12-17 23:41:36 +01:00
2015-11-28 23:10:25 +01:00
/ * *
* Handle Enter key . Adds new Node ;
* /
ce . prototype . enterKeyPressed = function ( event ) {
2015-12-29 22:41:19 +01:00
var _this = this ,
curNode = this . getFocusedNode ( ) ;
2015-12-17 23:41:36 +01:00
2015-12-29 22:41:19 +01:00
/ *
* обработка выбранной кнопки тулбара
* * /
2015-12-17 23:41:36 +01:00
if ( this . toolbar . isOpened ) {
2015-12-29 22:41:19 +01:00
switch ( this . focusedToolbarBtn . dataset [ "type" ] ) {
2015-12-17 23:41:36 +01:00
2015-12-29 22:41:19 +01:00
case "header" :
var header = this . make . headerNode ( ) ;
2015-12-09 01:34:32 +01:00
2015-12-29 22:41:19 +01:00
if ( curNode . textContent ) {
2015-12-25 15:06:43 +01:00
2015-12-29 22:41:19 +01:00
header . textContent = curNode . textContent ;
curNode . textContent = "" ;
2015-12-25 15:06:43 +01:00
2015-12-29 22:41:19 +01:00
// insert before, if curNode is paragraph or header or some other text-editable node
if ( curNode . dataset [ "type" ] == "text" ) {
curNode . parentNode . insertBefore ( header , curNode ) ;
curNode . remove ( ) ;
}
// else insert header node after
else
curNode . parentNode . insertBefore ( header , curNode . nextSibling ) ;
} else {
curNode . parentNode . insertBefore ( header , curNode ) ;
curNode . remove ( ) ;
2015-12-25 15:06:43 +01:00
2015-12-29 22:41:19 +01:00
}
2015-12-25 15:06:43 +01:00
2015-12-29 22:41:19 +01:00
this . focusNode ( header ) ;
break ;
}
this . closeToolBar ( ) ;
// TODO do the same by mouse clicking on any toolbar btn
2015-12-16 22:36:19 +01:00
2015-12-29 21:18:32 +01:00
event . preventDefault ( ) ;
2015-12-29 22:41:19 +01:00
}
/ *
* Перехват создания нового параграфа при нахождении в заголовке .
* По - умолчанию создается просто div .
* * /
else {
if ( curNode . dataset [ "type" ] == "header" && ! this . isTextSelected ( ) ) {
var newNode = this . make . textNode ( ) ;
/** Add node */
this . editableWrapper . insertBefore ( newNode , curNode . nextSibling ) ;
/** Set auto focus */
setTimeout ( function ( ) {
_this . focusNode ( newNode ) ;
} ) ;
event . preventDefault ( ) ;
}
2015-12-29 21:18:32 +01:00
}
2015-12-09 01:34:32 +01:00
} ;
2015-11-28 23:10:25 +01:00
/ * *
* Creates HTML elements
* /
2015-12-09 01:34:32 +01:00
ce . prototype . make = function ( ) {
2015-11-28 23:10:25 +01:00
/** Empty toolbar with toggler */
function toolbar ( ) {
var bar = document . createElement ( 'div' ) ;
bar . className += 'add_buttons' ;
/** Toggler button*/
bar . innerHTML = '<span class="toggler">' +
2015-12-09 01:34:32 +01:00
'<i class="plus_btn ce_icon-plus-circled-1"></i>' +
2015-11-28 23:10:25 +01:00
'</span>' ;
return bar ;
}
2015-12-10 22:26:44 +01:00
// Creates one button with given type
2015-11-28 23:10:25 +01:00
function toolbarButton ( type ) {
var button = document . createElement ( 'button' ) ;
button . dataset . type = type ;
button . innerHTML = '<i class="ce_icon-' + type + '"></i>' ;
return button ;
2015-12-10 22:26:44 +01:00
}
// Creates all tool bar buttons from editor settings
// allTools, usedTools - needs becose cant get them from editor object - bad context
function toolbarButtons ( allTools , usedTools ) {
var toolbarButtons = document . createElement ( "span" ) ;
toolbarButtons . classList . add ( "buttons" ) ;
// Walk base buttons list - save buttons origin sorting
allTools . forEach ( function ( item ) {
if ( usedTools . indexOf ( item ) >= 0 ) toolbarButtons . appendChild ( this . toolbarButton ( item ) ) ;
} , this ) ;
return toolbarButtons ;
2015-11-28 23:10:25 +01:00
}
/ * *
* Paragraph node
* @ todo set unique id with prefix
* /
2015-12-09 01:34:32 +01:00
function textNode ( content ) {
2015-11-28 23:10:25 +01:00
2015-12-16 22:36:19 +01:00
var node = document . createElement ( 'p' ) ;
node . classList . add ( "node" ) ;
2015-12-25 15:06:43 +01:00
node . dataset [ "type" ] = "text" ;
2015-12-16 22:36:19 +01:00
node . innerHTML = content || '' ;
2015-11-28 23:10:25 +01:00
return node ;
}
2015-12-29 22:41:19 +01:00
/ * *
* Header node
* /
function headerNode ( content ) {
var node = document . createElement ( 'h2' ) ;
node . classList . add ( "node" ) ;
node . dataset [ "type" ] = "header" ;
node . innerHTML = content || '' ;
return node ;
}
2015-11-28 23:10:25 +01:00
function editorWrapper ( ) {
2015-12-16 22:36:19 +01:00
var wrapper = document . createElement ( 'div' ) ,
editable _wrapper = document . createElement ( 'div' ) ;
editable _wrapper . className += 'ce_content' ;
editable _wrapper . setAttribute ( "contenteditable" , "true" ) ;
2015-11-28 23:10:25 +01:00
wrapper . className += 'codex_editor' ;
2015-12-16 22:36:19 +01:00
wrapper . appendChild ( editable _wrapper ) ;
2015-11-28 23:10:25 +01:00
return wrapper ;
}
var ceMake = function ( ) {
2015-12-10 22:26:44 +01:00
this . toolbar = toolbar ;
this . toolbarButtons = toolbarButtons ;
this . toolbarButton = toolbarButton ;
this . editorWrapper = editorWrapper ;
2015-12-29 22:41:19 +01:00
this . textNode = textNode ;
this . headerNode = headerNode ;
2015-12-09 01:34:32 +01:00
} ;
2015-11-28 23:10:25 +01:00
return new ceMake ( ) ;
2015-12-29 21:18:32 +01:00
} ( ) ;