PageRenderTime 49ms CodeModel.GetById 11ms RepoModel.GetById 1ms app.codeStats 0ms

/client/src/legacy/HtmlEditorField.js

http://github.com/silverstripe/sapphire
JavaScript | 1560 lines | 1260 code | 98 blank | 202 comment | 31 complexity | 63608692eafa92bd608177e825772666 MD5 | raw file
Possible License(s): BSD-3-Clause, MIT, CC-BY-3.0, GPL-2.0, AGPL-1.0, LGPL-2.1
  1. /**
  2. * Functions for HtmlEditorFields in the back end.
  3. * Includes the JS for the ImageUpload forms.
  4. *
  5. * Relies on the jquery.form.js plugin to power the
  6. * ajax / iframe submissions
  7. */
  8. import $ from 'jQuery';
  9. import i18n from 'i18n';
  10. var ss = typeof window.ss !== 'undefined' ? window.ss : {};
  11. /**
  12. * Wrapper for HTML WYSIWYG libraries, which abstracts library internals
  13. * from interface concerns like inserting and editing links.
  14. * Caution: Incomplete and unstable API.
  15. */
  16. ss.editorWrappers = {};
  17. ss.editorWrappers.tinyMCE = (function() {
  18. // ID of editor this is assigned to
  19. var editorID;
  20. return {
  21. /**
  22. * Initialise the editor
  23. *
  24. * @param {String} ID of parent textarea domID
  25. */
  26. init: function(ID) {
  27. editorID = ID;
  28. this.create();
  29. },
  30. /**
  31. * Remove the editor and cleanup
  32. */
  33. destroy: function() {
  34. tinymce.EditorManager.execCommand('mceRemoveEditor', false, editorID);
  35. },
  36. /**
  37. * Get TinyMCE Editor instance
  38. *
  39. * @returns Editor
  40. */
  41. getInstance: function() {
  42. return tinymce.EditorManager.get(editorID);
  43. },
  44. /**(
  45. * Invoked when a content-modifying UI is opened.
  46. */
  47. onopen: function() {
  48. // NOOP
  49. },
  50. /**(
  51. * Invoked when a content-modifying UI is closed.
  52. */
  53. onclose: function() {
  54. // NOOP
  55. },
  56. /**
  57. * Get config for this data
  58. *
  59. * @returns array
  60. */
  61. getConfig: function() {
  62. var selector = "#" + editorID,
  63. config = $(selector).data('config'),
  64. self = this;
  65. // Add instance specific data to config
  66. config.selector = selector;
  67. // Ensure save events write back to textarea
  68. config.setup = function(ed) {
  69. ed.on('change', function() {
  70. self.save();
  71. });
  72. };
  73. return config;
  74. },
  75. /**
  76. * Write the HTML back to the original text area field.
  77. */
  78. save: function() {
  79. var instance = this.getInstance();
  80. instance.save();
  81. // Update change detection
  82. $(instance.getElement()).trigger("change");
  83. },
  84. /**
  85. * Create a new instance based on a textarea field.
  86. */
  87. create: function() {
  88. var config = this.getConfig();
  89. // hack to set baseURL safely
  90. if(typeof config.baseURL !== 'undefined') {
  91. tinymce.EditorManager.baseURL = config.baseURL;
  92. }
  93. tinymce.init(config);
  94. },
  95. /**
  96. * Request an update to editor content
  97. */
  98. repaint: function() {
  99. // NOOP
  100. },
  101. /**
  102. * @return boolean
  103. */
  104. isDirty: function() {
  105. return this.getInstance().isDirty();
  106. },
  107. /**
  108. * HTML representation of the edited content.
  109. *
  110. * Returns: {String}
  111. */
  112. getContent: function() {
  113. return this.getInstance().getContent();
  114. },
  115. /**
  116. * DOM tree of the edited content
  117. *
  118. * Returns: DOMElement
  119. */
  120. getDOM: function() {
  121. return this.getInstance().getElement();
  122. },
  123. /**
  124. * Returns: DOMElement
  125. */
  126. getContainer: function() {
  127. return this.getInstance().getContainer();
  128. },
  129. /**
  130. * Get the closest node matching the current selection.
  131. *
  132. * Returns: {jQuery} DOMElement
  133. */
  134. getSelectedNode: function() {
  135. return this.getInstance().selection.getNode();
  136. },
  137. /**
  138. * Select the given node within the editor DOM
  139. *
  140. * Parameters: {DOMElement}
  141. */
  142. selectNode: function(node) {
  143. this.getInstance().selection.select(node);
  144. },
  145. /**
  146. * Replace entire content
  147. *
  148. * @param {String} html
  149. * @param {Object} opts
  150. */
  151. setContent: function(html, opts) {
  152. this.getInstance().setContent(html, opts);
  153. },
  154. /**
  155. * Insert content at the current caret position
  156. *
  157. * @param {String} html
  158. * @param {Object} opts
  159. */
  160. insertContent: function(html, opts) {
  161. this.getInstance().insertContent(html, opts);
  162. },
  163. /**
  164. * Replace currently selected content
  165. *
  166. * @param {String} html
  167. */
  168. replaceContent: function(html, opts) {
  169. this.getInstance().execCommand('mceReplaceContent', false, html, opts);
  170. },
  171. /**
  172. * Insert or update a link in the content area (based on current editor selection)
  173. *
  174. * Parameters: {Object} attrs
  175. */
  176. insertLink: function(attrs, opts) {
  177. this.getInstance().execCommand("mceInsertLink", false, attrs, opts);
  178. },
  179. /**
  180. * Remove the link from the currently selected node (if any).
  181. */
  182. removeLink: function() {
  183. this.getInstance().execCommand('unlink', false);
  184. },
  185. /**
  186. * Strip any editor-specific notation from link in order to make it presentable in the UI.
  187. *
  188. * Parameters:
  189. * {Object}
  190. * {DOMElement}
  191. */
  192. cleanLink: function(href, node) {
  193. var settings = this.getConfig,
  194. cb = settings['urlconverter_callback'];
  195. if(cb) href = eval(cb + "(href, node, true);");
  196. // Turn into relative
  197. if(href.match(new RegExp('^' + tinyMCE.settings['document_base_url'] + '(.*)$'))) {
  198. href = RegExp.$1;
  199. }
  200. // Get rid of TinyMCE's temporary URLs
  201. if(href.match(/^javascript:\s*mctmp/)) href = '';
  202. return href;
  203. },
  204. /**
  205. * Creates a bookmark for the currently selected range,
  206. * which can be used to reselect this range at a later point.
  207. * @return {mixed}
  208. */
  209. createBookmark: function() {
  210. return this.getInstance().selection.getBookmark();
  211. },
  212. /**
  213. * Selects a bookmarked range previously saved through createBookmark().
  214. * @param {mixed} bookmark
  215. */
  216. moveToBookmark: function(bookmark) {
  217. this.getInstance().selection.moveToBookmark(bookmark);
  218. this.getInstance().focus();
  219. },
  220. /**
  221. * Removes any selection & de-focuses this editor
  222. */
  223. blur: function() {
  224. this.getInstance().selection.collapse();
  225. },
  226. /**
  227. * Add new undo point with the current DOM content.
  228. */
  229. addUndo: function() {
  230. this.getInstance().undoManager.add();
  231. }
  232. };
  233. });
  234. // Override this to switch editor wrappers
  235. ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
  236. $.entwine('ss', function($) {
  237. /**
  238. * Class: textarea.htmleditor
  239. *
  240. * Add tinymce to HtmlEditorFields within the CMS. Works in combination
  241. * with a TinyMCE.init() call which is prepopulated with the used HTMLEditorConfig settings,
  242. * and included in the page as an inline <script> tag.
  243. */
  244. $('textarea.htmleditor').entwine({
  245. Editor: null,
  246. /**
  247. * Constructor: onmatch
  248. */
  249. onadd: function() {
  250. var edClass = this.data('editor') || 'default',
  251. ed = ss.editorWrappers[edClass]();
  252. this.setEditor(ed);
  253. ed.init(this.attr('id'));
  254. this._super();
  255. },
  256. /**
  257. * Destructor: onunmatch
  258. */
  259. onremove: function() {
  260. this.getEditor().destroy();
  261. this._super();
  262. },
  263. /**
  264. * Make sure the editor has flushed all it's buffers before the form is submitted.
  265. */
  266. 'from .cms-edit-form': {
  267. onbeforesubmitform: function() {
  268. this.getEditor().save();
  269. this._super();
  270. }
  271. },
  272. /**
  273. * Triggers insert-link dialog
  274. * See editor_plugin_src.js
  275. */
  276. openLinkDialog: function() {
  277. this.openDialog('link');
  278. },
  279. /**
  280. * Triggers insert-media dialog
  281. * See editor_plugin_src.js
  282. */
  283. openMediaDialog: function() {
  284. this.openDialog('media');
  285. },
  286. openDialog: function(type) {
  287. var capitalize = function(text) {
  288. return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
  289. };
  290. var self = this,
  291. url = $('#cms-editor-dialogs').data('url' + capitalize(type) + 'form'),
  292. dialog = $('.htmleditorfield-' + type + 'dialog');
  293. if(dialog.length) {
  294. // Clean existing dialog for reload
  295. dialog.getForm().setElement(this);
  296. dialog.html('');
  297. dialog.addClass('loading');
  298. dialog.open();
  299. } else {
  300. // Show a placeholder for instant feedback. Will be replaced with actual
  301. // form dialog once its loaded.
  302. dialog = $('<div class="htmleditorfield-dialog htmleditorfield-' + type + 'dialog loading">');
  303. $('body').append(dialog);
  304. }
  305. $.ajax({
  306. url: url,
  307. complete: function() {
  308. dialog.removeClass('loading');
  309. },
  310. success: function(html) {
  311. dialog.html(html);
  312. dialog.getForm().setElement(self);
  313. dialog.trigger('ssdialogopen');
  314. }
  315. });
  316. }
  317. });
  318. $('.htmleditorfield-dialog').entwine({
  319. onadd: function() {
  320. // Create jQuery dialog
  321. if (!this.is('.ui-dialog-content')) {
  322. this.ssdialog({
  323. autoOpen: true,
  324. buttons: {
  325. 'insert': {
  326. text: i18n._t(
  327. 'HtmlEditorField.INSERT',
  328. 'Insert'
  329. ),
  330. 'data-icon': 'accept',
  331. class: 'ss-ui-action-constructive media-insert',
  332. click: function() {
  333. $(this).find('form').submit();
  334. }
  335. }
  336. }
  337. });
  338. }
  339. this._super();
  340. },
  341. getForm: function() {
  342. return this.find('form');
  343. },
  344. open: function() {
  345. this.ssdialog('open');
  346. },
  347. close: function() {
  348. this.ssdialog('close');
  349. },
  350. toggle: function(bool) {
  351. if(this.is(':visible')) this.close();
  352. else this.open();
  353. },
  354. onscroll: function () {
  355. this.animate({
  356. scrollTop: this.find('form').height()
  357. }, 500);
  358. }
  359. });
  360. /**
  361. * Base form implementation for interactions with an editor instance,
  362. * mostly geared towards modification and insertion of content.
  363. */
  364. $('form.htmleditorfield-form').entwine({
  365. Selection: null,
  366. // Implementation-dependent serialization of the current editor selection state
  367. Bookmark: null,
  368. // DOMElement pointing to the currently active textarea
  369. Element: null,
  370. setSelection: function(node) {
  371. return this._super($(node));
  372. },
  373. onadd: function() {
  374. // Move title from headline to (jQuery compatible) title attribute
  375. var titleEl = this.find(':header:first');
  376. this.getDialog().attr('title', titleEl.text());
  377. this._super();
  378. },
  379. onremove: function() {
  380. this.setSelection(null);
  381. this.setBookmark(null);
  382. this.setElement(null);
  383. this._super();
  384. },
  385. getDialog: function() {
  386. // TODO Refactor to listen to form events to remove two-way coupling
  387. return this.closest('.htmleditorfield-dialog');
  388. },
  389. fromDialog: {
  390. onssdialogopen: function(){
  391. var ed = this.getEditor();
  392. this.setSelection(ed.getSelectedNode());
  393. this.setBookmark(ed.createBookmark());
  394. ed.blur();
  395. this.find(':input:not(:submit)[data-skip-autofocus!="true"]')
  396. .filter(':visible:enabled')
  397. .eq(0)
  398. .focus();
  399. this.redraw();
  400. this.updateFromEditor();
  401. },
  402. onssdialogclose: function(){
  403. var ed = this.getEditor();
  404. ed.moveToBookmark(this.getBookmark());
  405. this.setSelection(null);
  406. this.setBookmark(null);
  407. this.resetFields();
  408. }
  409. },
  410. /**
  411. * @return Object ss.editorWrapper instance
  412. */
  413. getEditor: function(){
  414. return this.getElement().getEditor();
  415. },
  416. modifySelection: function(callback) {
  417. var ed = this.getEditor();
  418. ed.moveToBookmark(this.getBookmark());
  419. callback.call(this, ed);
  420. this.setSelection(ed.getSelectedNode());
  421. this.setBookmark(ed.createBookmark());
  422. ed.blur();
  423. },
  424. updateFromEditor: function() {
  425. /* NOP */
  426. },
  427. redraw: function() {
  428. /* NOP */
  429. },
  430. resetFields: function() {
  431. // Flush the tree drop down fields, as their content might get changed in other parts of the CMS, ie in Files and images
  432. this.find('.tree-holder').empty();
  433. }
  434. });
  435. /**
  436. * Inserts and edits links in an html editor, including internal/external web links,
  437. * links to files on the webserver, email addresses, and anchors in the existing html content.
  438. * Every variation has its own fields (e.g. a "target" attribute doesn't make sense for an email link),
  439. * which are toggled through a type dropdown. Variations share fields, so there's only one "title" field in the form.
  440. */
  441. $('form.htmleditorfield-linkform').entwine({
  442. // TODO Entwine doesn't respect submits triggered by ENTER key
  443. onsubmit: function(e) {
  444. this.insertLink();
  445. this.getDialog().close();
  446. return false;
  447. },
  448. resetFields: function() {
  449. this._super();
  450. // Reset the form using a native call. This will also correctly reset checkboxes and radio buttons.
  451. this[0].reset();
  452. },
  453. redraw: function() {
  454. this._super();
  455. var linkType = this.find(':input[name=LinkType]:checked').val();
  456. this.addAnchorSelector();
  457. this.resetFileField();
  458. // Toggle field visibility depending on the link type.
  459. this.find('.step2').nextAll('.field').not('.field[id$="' + linkType +'_Holder"]').hide();
  460. this.find('.field[id$="LinkType_Holder"]').show();
  461. this.find('.field[id$="' + linkType +'_Holder"]').show();
  462. if(linkType == 'internal' || linkType == 'anchor') {
  463. this.find('.field[id$="Anchor_Holder"]').show();
  464. }
  465. if(linkType == 'email') {
  466. this.find('.field[id$="Subject_Holder"]').show();
  467. } else {
  468. this.find('.field[id$="TargetBlank_Holder"]').show();
  469. }
  470. if(linkType == 'anchor') {
  471. this.find('.field[id$="AnchorSelector_Holder"]').show();
  472. }
  473. this.find('.field[id$="Description_Holder"]').show();
  474. },
  475. /**
  476. * @return Object Keys: 'href', 'target', 'title'
  477. */
  478. getLinkAttributes: function() {
  479. var href,
  480. target = null,
  481. subject = this.find(':input[name=Subject]').val(),
  482. anchor = this.find(':input[name=Anchor]').val();
  483. // Determine target
  484. if(this.find(':input[name=TargetBlank]').is(':checked')) {
  485. target = '_blank';
  486. }
  487. // All other attributes
  488. switch(this.find(':input[name=LinkType]:checked').val()) {
  489. case 'internal':
  490. href = '[sitetree_link,id=' + this.find(':input[name=internal]').val() + ']';
  491. if(anchor) {
  492. href += '#' + anchor;
  493. }
  494. break;
  495. case 'anchor':
  496. href = '#' + anchor;
  497. break;
  498. case 'file':
  499. var fileid = this.find('.ss-uploadfield .ss-uploadfield-item').attr('data-fileid');
  500. href = fileid ? '[file_link,id=' + fileid + ']' : '';
  501. break;
  502. case 'email':
  503. href = 'mailto:' + this.find(':input[name=email]').val();
  504. if(subject) {
  505. href += '?subject=' + encodeURIComponent(subject);
  506. }
  507. target = null;
  508. break;
  509. // case 'external':
  510. default:
  511. href = this.find(':input[name=external]').val();
  512. // Prefix the URL with "http://" if no prefix is found
  513. if(href.indexOf('://') == -1) href = 'http://' + href;
  514. break;
  515. }
  516. return {
  517. href : href,
  518. target : target,
  519. title : this.find(':input[name=Description]').val()
  520. };
  521. },
  522. insertLink: function() {
  523. this.modifySelection(function(ed){
  524. ed.insertLink(this.getLinkAttributes());
  525. });
  526. },
  527. removeLink: function() {
  528. this.modifySelection(function(ed){
  529. ed.removeLink();
  530. });
  531. this.resetFileField();
  532. this.close();
  533. },
  534. resetFileField: function() {
  535. // If there's an attached item, remove it
  536. var fileField = this.find('.ss-uploadfield[id$="file_Holder"]'),
  537. fileUpload = fileField.data('fileupload'),
  538. currentItem = fileField.find('.ss-uploadfield-item[data-fileid]');
  539. if(currentItem.length) {
  540. fileUpload._trigger('destroy', null, {context: currentItem});
  541. fileField.find('.ss-uploadfield-addfile').removeClass('borderTop');
  542. }
  543. },
  544. /**
  545. * Builds an anchor selector element and injects it into the DOM next to the anchor field.
  546. */
  547. addAnchorSelector: function() {
  548. // Avoid adding twice
  549. if(this.find(':input[name=AnchorSelector]').length) return;
  550. var self = this;
  551. var anchorSelector = $(
  552. '<select id="Form_EditorToolbarLinkForm_AnchorSelector" name="AnchorSelector"></select>'
  553. );
  554. this.find(':input[name=Anchor]').parent().append(anchorSelector);
  555. // Initialise the anchor dropdown.
  556. this.updateAnchorSelector();
  557. // copy the value from dropdown to the text field
  558. anchorSelector.change(function(e) {
  559. self.find(':input[name="Anchor"]').val($(this).val());
  560. });
  561. },
  562. /**
  563. * Fetch relevant anchors, depending on the link type.
  564. *
  565. * @return $.Deferred A promise of an anchor array, or an error message.
  566. */
  567. getAnchors: function() {
  568. var linkType = this.find(':input[name=LinkType]:checked').val();
  569. var dfdAnchors = $.Deferred();
  570. switch (linkType) {
  571. case 'anchor':
  572. // Fetch from the local editor.
  573. var collectedAnchors = [];
  574. var ed = this.getEditor();
  575. // name attribute is defined as CDATA, should accept all characters and entities
  576. // http://www.w3.org/TR/1999/REC-html401-19991224/struct/links.html#h-12.2
  577. if(ed) {
  578. var raw = ed.getContent()
  579. .match(/\s+(name|id)\s*=\s*(["'])([^\2\s>]*?)\2|\s+(name|id)\s*=\s*([^"']+)[\s +>]/gim);
  580. if (raw && raw.length) {
  581. for(var i = 0; i < raw.length; i++) {
  582. var indexStart = (raw[i].indexOf('id=') == -1) ? 7 : 5;
  583. collectedAnchors.push(raw[i].substr(indexStart).replace(/"$/, ''));
  584. }
  585. }
  586. }
  587. dfdAnchors.resolve(collectedAnchors);
  588. break;
  589. case 'internal':
  590. // Fetch available anchors from the target internal page.
  591. var pageId = this.find(':input[name=internal]').val();
  592. if (pageId) {
  593. $.ajax({
  594. url: $.path.addSearchParams(
  595. this.attr('action').replace('LinkForm', 'getanchors'),
  596. {'PageID': parseInt(pageId)}
  597. ),
  598. success: function(body, status, xhr) {
  599. dfdAnchors.resolve($.parseJSON(body));
  600. },
  601. error: function(xhr, status) {
  602. dfdAnchors.reject(xhr.responseText);
  603. }
  604. });
  605. } else {
  606. dfdAnchors.resolve([]);
  607. }
  608. break;
  609. default:
  610. // This type does not support anchors at all.
  611. dfdAnchors.reject(i18n._t(
  612. 'HtmlEditorField.ANCHORSNOTSUPPORTED',
  613. 'Anchors are not supported for this link type.'
  614. ));
  615. break;
  616. }
  617. return dfdAnchors.promise();
  618. },
  619. /**
  620. * Update the anchor list in the dropdown.
  621. */
  622. updateAnchorSelector: function() {
  623. var self = this;
  624. var selector = this.find(':input[name=AnchorSelector]');
  625. var dfdAnchors = this.getAnchors();
  626. // Inform the user we are loading.
  627. selector.empty();
  628. selector.append($(
  629. '<option value="" selected="1">' +
  630. i18n._t('HtmlEditorField.LOOKINGFORANCHORS', 'Looking for anchors...') +
  631. '</option>'
  632. ));
  633. dfdAnchors.done(function(anchors) {
  634. selector.empty();
  635. selector.append($(
  636. '<option value="" selected="1">' +
  637. i18n._t('HtmlEditorField.SelectAnchor') +
  638. '</option>'
  639. ));
  640. if (anchors) {
  641. for (var j = 0; j < anchors.length; j++) {
  642. selector.append($('<option value="'+anchors[j]+'">'+anchors[j]+'</option>'));
  643. }
  644. }
  645. }).fail(function(message) {
  646. selector.empty();
  647. selector.append($(
  648. '<option value="" selected="1">' +
  649. message +
  650. '</option>'
  651. ));
  652. });
  653. // Poke the selector for IE8, otherwise the changes won't be noticed.
  654. if ($.browser.msie) selector.hide().show();
  655. },
  656. /**
  657. * Updates the state of the dialog inputs to match the editor selection.
  658. * If selection does not contain a link, resets the fields.
  659. */
  660. updateFromEditor: function() {
  661. var htmlTagPattern = /<\S[^><]*>/g, fieldName, data = this.getCurrentLink();
  662. if(data) {
  663. for(fieldName in data) {
  664. var el = this.find(':input[name=' + fieldName + ']'), selected = data[fieldName];
  665. // Remove html tags in the selected text that occurs on IE browsers
  666. if(typeof(selected) == 'string') selected = selected.replace(htmlTagPattern, '');
  667. // Set values and invoke the triggers (e.g. for TreeDropdownField).
  668. if(el.is(':checkbox')) {
  669. el.prop('checked', selected).change();
  670. } else if(el.is(':radio')) {
  671. el.val([selected]).change();
  672. } else if(fieldName == 'file') {
  673. // UploadField inputs have a slightly different naming convention
  674. el = this.find(':input[name="' + fieldName + '[Uploads][]"]');
  675. // We need the UploadField "field", not just the input
  676. el = el.parents('.ss-uploadfield');
  677. // We have to wait for the UploadField to initialise
  678. (function attach(el, selected) {
  679. if( ! el.getConfig()) {
  680. setTimeout(function(){ attach(el, selected); }, 50);
  681. } else {
  682. el.attachFiles([selected]);
  683. }
  684. })(el, selected);
  685. } else {
  686. el.val(selected).change();
  687. }
  688. }
  689. }
  690. },
  691. /**
  692. * Return information about the currently selected link, suitable for population of the link form.
  693. *
  694. * Returns null if no link was currently selected.
  695. */
  696. getCurrentLink: function() {
  697. var selectedEl = this.getSelection(),
  698. href = "", target = "", title = "", action = "insert", style_class = "";
  699. // We use a separate field for linkDataSource from tinyMCE.linkElement.
  700. // If we have selected beyond the range of an <a> element, then use use that <a> element to get the link data source,
  701. // but we don't use it as the destination for the link insertion
  702. var linkDataSource = null;
  703. if(selectedEl.length) {
  704. if(selectedEl.is('a')) {
  705. // Element is a link
  706. linkDataSource = selectedEl;
  707. // TODO Limit to inline elements, otherwise will also apply to e.g. paragraphs which already contain one or more links
  708. // } else if((selectedEl.find('a').length)) {
  709. // // Element contains a link
  710. // var firstLinkEl = selectedEl.find('a:first');
  711. // if(firstLinkEl.length) linkDataSource = firstLinkEl;
  712. } else {
  713. // Element is a child of a link
  714. linkDataSource = selectedEl = selectedEl.parents('a:first');
  715. }
  716. }
  717. if(linkDataSource && linkDataSource.length) this.modifySelection(function(ed){
  718. ed.selectNode(linkDataSource[0]);
  719. });
  720. // Is anchor not a link
  721. if (!linkDataSource.attr('href')) linkDataSource = null;
  722. if (linkDataSource) {
  723. href = linkDataSource.attr('href');
  724. target = linkDataSource.attr('target');
  725. title = linkDataSource.attr('title');
  726. style_class = linkDataSource.attr('class');
  727. href = this.getEditor().cleanLink(href, linkDataSource);
  728. action = "update";
  729. }
  730. if(href.match(/^mailto:(.*)$/)) {
  731. return {
  732. LinkType: 'email',
  733. email: RegExp.$1,
  734. Description: title
  735. };
  736. } else if(href.match(/^(assets\/.*)$/) || href.match(/^\[file_link\s*(?:\s*|%20|,)?id=([0-9]+)\]?(#.*)?$/)) {
  737. return {
  738. LinkType: 'file',
  739. file: RegExp.$1,
  740. Description: title,
  741. TargetBlank: target ? true : false
  742. };
  743. } else if(href.match(/^#(.*)$/)) {
  744. return {
  745. LinkType: 'anchor',
  746. Anchor: RegExp.$1,
  747. Description: title,
  748. TargetBlank: target ? true : false
  749. };
  750. } else if(href.match(/^\[sitetree_link(?:\s*|%20|,)?id=([0-9]+)\]?(#.*)?$/i)) {
  751. return {
  752. LinkType: 'internal',
  753. internal: RegExp.$1,
  754. Anchor: RegExp.$2 ? RegExp.$2.substr(1) : '',
  755. Description: title,
  756. TargetBlank: target ? true : false
  757. };
  758. } else if(href) {
  759. return {
  760. LinkType: 'external',
  761. external: href,
  762. Description: title,
  763. TargetBlank: target ? true : false
  764. };
  765. } else {
  766. // No link/invalid link selected.
  767. return null;
  768. }
  769. }
  770. });
  771. $('form.htmleditorfield-linkform input[name=LinkType]').entwine({
  772. onclick: function(e) {
  773. this.parents('form:first').redraw();
  774. this._super();
  775. },
  776. onchange: function() {
  777. this.parents('form:first').redraw();
  778. // Update if a anchor-supporting link type is selected.
  779. var linkType = this.parent().find(':checked').val();
  780. if (linkType==='anchor' || linkType==='internal') {
  781. this.parents('form.htmleditorfield-linkform').updateAnchorSelector();
  782. }
  783. this._super();
  784. }
  785. });
  786. $('form.htmleditorfield-linkform input[name=internal]').entwine({
  787. /**
  788. * Update the anchor dropdown if a different page is selected in the "internal" dropdown.
  789. */
  790. onvalueupdated: function() {
  791. this.parents('form.htmleditorfield-linkform').updateAnchorSelector();
  792. this._super();
  793. }
  794. });
  795. $('form.htmleditorfield-linkform :submit[name=action_remove]').entwine({
  796. onclick: function(e) {
  797. this.parents('form:first').removeLink();
  798. this._super();
  799. return false;
  800. }
  801. });
  802. /**
  803. * Responsible for inserting media files, although only images are supported so far.
  804. * Allows to select one or more files, and load form fields for each file via ajax.
  805. * This allows us to tailor the form fields to the file type (e.g. different ones for images and flash),
  806. * as well as add new form fields via framework extensions.
  807. * The inputs on each of those files are used for constructing the HTML to insert into
  808. * the rich text editor. Also allows editing the properties of existing files if any are selected in the editor.
  809. * Note: Not each file has a representation on the webserver filesystem, supports insertion and editing
  810. * of remove files as well.
  811. */
  812. $('form.htmleditorfield-mediaform').entwine({
  813. toggleCloseButton: function(){
  814. var updateExisting = Boolean(this.find('.ss-htmleditorfield-file').length);
  815. this.find('.overview .action-delete')[updateExisting ? 'hide' : 'show']();
  816. },
  817. onsubmit: function() {
  818. this.modifySelection(function(ed){
  819. this.find('.ss-htmleditorfield-file').each(function() {
  820. $(this).insertHTML(ed);
  821. });
  822. });
  823. this.getDialog().close();
  824. return false;
  825. },
  826. updateFromEditor: function() {
  827. var self = this, node = this.getSelection();
  828. // TODO Depends on managed mime type
  829. if(node.is('img')) {
  830. var idOrUrl = node.data('id') || node.data('url') || node.attr('src');
  831. this.showFileView(idOrUrl).done(function(filefield) {
  832. filefield.updateFromNode(node);
  833. self.toggleCloseButton();
  834. self.redraw();
  835. });
  836. }
  837. this.redraw();
  838. },
  839. redraw: function(updateExisting) {
  840. this._super();
  841. var node = this.getSelection(),
  842. hasItems = Boolean(this.find('.ss-htmleditorfield-file').length),
  843. editingSelected = node.is('img'),
  844. insertingURL = this.hasClass('insertingURL'),
  845. header = this.find('.header-edit');
  846. // Only show second step if files are selected
  847. header[(hasItems) ? 'show' : 'hide']();
  848. // Disable "insert" button if no files are selected
  849. this.closest('ui-dialog')
  850. .find('ui-dialog-buttonpane .media-insert')
  851. .button(hasItems ? 'enable' : 'disable')
  852. .toggleClass('ui-state-disabled', !hasItems);
  853. // Hide file selection and step labels when editing an existing file
  854. this.find('.htmleditorfield-default-panel')[editingSelected || insertingURL ? 'hide' : 'show']();
  855. this.find('.htmleditorfield-web-panel')[editingSelected || !insertingURL ? 'hide' : 'show']();
  856. var mediaFormHeading = this.find('.htmleditorfield-mediaform-heading.insert');
  857. if (editingSelected) {
  858. //When editing details of a file
  859. mediaFormHeading.hide();
  860. } else if (insertingURL) {
  861. //When inserting an image from a URL
  862. mediaFormHeading
  863. .show()
  864. .text(i18n._t("HtmlEditorField.INSERTURL"))
  865. .prepend('<button class="back-button font-icon-left-open no-text" title="' + i18n._t("HtmlEditorField.BACK") + '"></button>');
  866. this.find('.htmleditorfield-web-panel input.remoteurl').focus();
  867. } else {
  868. //Default view when modal is opened
  869. mediaFormHeading
  870. .show()
  871. .text(i18n._t("HtmlEditorField.INSERTFROM"))
  872. .find('.back-button').remove();
  873. }
  874. // TODO Way too much knowledge on UploadField internals, use viewfile URL directly instead
  875. this.find('.htmleditorfield-mediaform-heading.update')[editingSelected ? 'show' : 'hide']();
  876. this.find('.ss-uploadfield-item-actions')[editingSelected ? 'hide' : 'show']();
  877. this.find('.ss-uploadfield-item-name')[editingSelected ? 'hide' : 'show']();
  878. this.find('.ss-uploadfield-item-preview')[editingSelected ? 'hide' : 'show']();
  879. this.find('.btn-toolbar .media-update')[editingSelected ? 'show' : 'hide']();
  880. this.find('.ss-uploadfield-item-editform').toggleEditForm(editingSelected);
  881. this.find('.htmleditorfield-from-cms .field.treedropdown').css('left', $('.htmleditorfield-mediaform-heading:visible').outerWidth());
  882. this.closest('.ui-dialog').addClass('ss-uploadfield-dropzone');
  883. this.closest('.ui-dialog')
  884. .find('.ui-dialog-buttonpane .media-insert .ui-button-text')
  885. .text([editingSelected ? i18n._t(
  886. 'HtmlEditorField.UPDATE',
  887. 'Update'
  888. ) : i18n._t(
  889. 'HtmlEditorField.INSERT',
  890. 'Insert'
  891. )]);
  892. },
  893. resetFields: function() {
  894. this.find('.ss-htmleditorfield-file').remove(); // Remove any existing views
  895. this.find('.ss-gridfield-items .ui-selected').removeClass('ui-selected'); // Unselect all items
  896. this.find('li.ss-uploadfield-item').remove(); // Remove all selected items
  897. this.redraw();
  898. this._super();
  899. },
  900. getFileView: function(idOrUrl) {
  901. return this.find('.ss-htmleditorfield-file[data-id=' + idOrUrl + ']');
  902. },
  903. showFileView: function(idOrUrl) {
  904. var self = this, params = (Number(idOrUrl) == idOrUrl) ? {ID: idOrUrl} : {FileURL: idOrUrl};
  905. var item = $('<div class="ss-htmleditorfield-file loading" />');
  906. this.find('.content-edit').prepend(item);
  907. var dfr = $.Deferred();
  908. $.ajax({
  909. url: $.path.addSearchParams(this.attr('action').replace(/MediaForm/, 'viewfile'), params),
  910. success: function(html, status, xhr) {
  911. var newItem = $(html).filter('.ss-htmleditorfield-file');
  912. item.replaceWith(newItem);
  913. self.redraw();
  914. dfr.resolve(newItem);
  915. },
  916. error: function() {
  917. item.remove();
  918. dfr.reject();
  919. }
  920. });
  921. return dfr.promise();
  922. }
  923. });
  924. //When 'Insert from URL' button is clicked
  925. $('form.htmleditorfield-mediaform div.ss-upload .upload-url').entwine({
  926. onclick: function () {
  927. var form = this.closest('form');
  928. form.addClass('insertingURL');
  929. form.redraw();
  930. }
  931. });
  932. //When back button is clicked while inserting URL
  933. $('form.htmleditorfield-mediaform .htmleditorfield-mediaform-heading .back-button').entwine({
  934. onclick: function() {
  935. var form = this.closest('form');
  936. form.removeClass('insertingURL');
  937. form.redraw();
  938. }
  939. });
  940. $('form.htmleditorfield-mediaform .ss-gridfield-items').entwine({
  941. onselectableselected: function(e, ui) {
  942. var form = this.closest('form'), item = $(ui.selected);
  943. if(!item.is('.ss-gridfield-item')) return;
  944. form.closest('form').showFileView(item.data('id'));
  945. form.redraw();
  946. form.parent().trigger('scroll');
  947. },
  948. onselectableunselected: function(e, ui) {
  949. var form = this.closest('form'), item = $(ui.unselected);
  950. if(!item.is('.ss-gridfield-item')) return;
  951. form.getFileView(item.data('id')).remove();
  952. form.redraw();
  953. }
  954. });
  955. /**
  956. * Show the second step after uploading an image
  957. */
  958. $('form.htmleditorfield-form.htmleditorfield-mediaform div.ss-assetuploadfield').entwine({
  959. //the UploadField div.ss-uploadfield-editandorganize is hidden in CSS,
  960. // because we use the detail view for each individual file instead
  961. onfileuploadstop: function(e) {
  962. var form = this.closest('form');
  963. //update the editFields to show those Files that are newly uploaded
  964. var editFieldIDs = [];
  965. form.find('div.content-edit').find('div.ss-htmleditorfield-file').each(function(){
  966. //get the uploaded file ID when this event triggers, signaling the upload has compeleted successfully
  967. editFieldIDs.push($(this).data('id'));
  968. });
  969. // we only want this .ss-uploadfield-files - else we get all ss-uploadfield-files wich include the ones not related to #tinymce insertmedia
  970. var uploadedFiles = $('.ss-uploadfield-files', this).children('.ss-uploadfield-item');
  971. uploadedFiles.each(function(){
  972. var uploadedID = $(this).data('fileid');
  973. if (uploadedID && $.inArray(uploadedID, editFieldIDs) == -1) {
  974. //trigger the detail view for filling out details about the file we are about to insert into TinyMCE
  975. $(this).remove(); // Remove successfully added item from the queue
  976. form.showFileView(uploadedID);
  977. }
  978. });
  979. form.parent().trigger('scroll');
  980. form.redraw();
  981. }
  982. });
  983. $('form.htmleditorfield-form.htmleditorfield-mediaform input.remoteurl').entwine({
  984. onadd: function() {
  985. this._super();
  986. this.validate();
  987. },
  988. onkeyup: function() {
  989. this.validate();
  990. },
  991. onchange: function() {
  992. this.validate();
  993. },
  994. getAddButton: function() {
  995. return this.closest('.CompositeField').find('button.add-url');
  996. },
  997. validate: function() {
  998. var val = this.val(), orig = val;
  999. val = $.trim(val);
  1000. val = val.replace(/^https?:\/\//i, '');
  1001. if (orig !== val) this.val(val);
  1002. this.getAddButton().button(!!val ? 'enable' : 'disable');
  1003. return !!val;
  1004. }
  1005. });
  1006. /**
  1007. * Show the second step after adding a URL
  1008. */
  1009. $('form.htmleditorfield-form.htmleditorfield-mediaform .add-url').entwine({
  1010. getURLField: function() {
  1011. return this.closest('.CompositeField').find('input.remoteurl');
  1012. },
  1013. onclick: function(e) {
  1014. var urlField = this.getURLField(), container = this.closest('.CompositeField'), form = this.closest('form');
  1015. if (urlField.validate()) {
  1016. container.addClass('loading');
  1017. form.showFileView('http://' + urlField.val()).done(function() {
  1018. container.removeClass('loading');
  1019. form.parent().trigger('scroll');
  1020. });
  1021. form.redraw();
  1022. }
  1023. return false;
  1024. }
  1025. });
  1026. /**
  1027. * Represents a single selected file, together with a set of form fields to edit its properties.
  1028. * Overload this based on the media type to determine how the HTML should be created.
  1029. */
  1030. $('form.htmleditorfield-mediaform .ss-htmleditorfield-file').entwine({
  1031. /**
  1032. * @return {Object} Map of HTML attributes which can be set on the created DOM node.
  1033. */
  1034. getAttributes: function() {
  1035. },
  1036. /**
  1037. * @return {Object} Map of additional properties which can be evaluated
  1038. * by the specific media type.
  1039. */
  1040. getExtraData: function() {
  1041. },
  1042. /**
  1043. * @return {String} HTML suitable for insertion into the rich text editor
  1044. */
  1045. getHTML: function() {
  1046. // Assumes UploadField markup structure
  1047. return $('<div>').append(
  1048. $('<a/>').attr({href: this.data('url')}).text(this.find('.name').text())
  1049. ).html();
  1050. },
  1051. /**
  1052. * Insert updated HTML content into the rich text editor
  1053. */
  1054. insertHTML: function(ed) {
  1055. // Insert content
  1056. ed.replaceContent(this.getHTML());
  1057. },
  1058. /**
  1059. * Updates the form values from an existing node in the editor.
  1060. *
  1061. * @param {DOMElement}
  1062. */
  1063. updateFromNode: function(node) {
  1064. },
  1065. /**
  1066. * Transforms values set on the dimensions form fields based on two constraints:
  1067. * An aspect ration, and max width/height values. Writes back to the field properties as required.
  1068. *
  1069. * @param {String} The dimension to constrain the other value by, if any ("Width" or "Height")
  1070. * @param {Int} Optional max width
  1071. * @param {Int} Optional max height
  1072. */
  1073. updateDimensions: function(constrainBy, maxW, maxH) {
  1074. var widthEl = this.find(':input[name=Width]'),
  1075. heightEl = this.find(':input[name=Height]'),
  1076. w = widthEl.val(),
  1077. h = heightEl.val(),
  1078. aspect;
  1079. // Proportionate updating of heights, using the original values
  1080. if(w && h) {
  1081. if(constrainBy) {
  1082. aspect = heightEl.getOrigVal() / widthEl.getOrigVal();
  1083. // Uses floor() and ceil() to avoid both fields constantly lowering each other's values in rounding situations
  1084. if(constrainBy == 'Width') {
  1085. if(maxW && w > maxW) w = maxW;
  1086. h = Math.floor(w * aspect);
  1087. } else if(constrainBy == 'Height') {
  1088. if(maxH && h > maxH) h = maxH;
  1089. w = Math.ceil(h / aspect);
  1090. }
  1091. } else {
  1092. if(maxW && w > maxW) w = maxW;
  1093. if(maxH && h > maxH) h = maxH;
  1094. }
  1095. widthEl.val(w);
  1096. heightEl.val(h);
  1097. }
  1098. }
  1099. });
  1100. $('form.htmleditorfield-mediaform .ss-htmleditorfield-file.image').entwine({
  1101. getAttributes: function() {
  1102. var width = this.find(':input[name=Width]').val(),
  1103. height = this.find(':input[name=Height]').val();
  1104. return {
  1105. 'src' : this.find(':input[name=URL]').val(),
  1106. 'alt' : this.find(':input[name=AltText]').val(),
  1107. 'width' : width ? parseInt(width, 10) : null,
  1108. 'height' : height ? parseInt(height, 10) : null,
  1109. 'title' : this.find(':input[name=Title]').val(),
  1110. 'class' : this.find(':input[name=CSSClass]').val(),
  1111. 'data-id' : this.find(':input[name=FileID]').val()
  1112. };
  1113. },
  1114. getExtraData: function() {
  1115. return {
  1116. 'CaptionText': this.find(':input[name=CaptionText]').val()
  1117. };
  1118. },
  1119. getHTML: function() {
  1120. /* NOP */
  1121. },
  1122. /**
  1123. * Logic similar to TinyMCE 'advimage' plugin, insertAndClose() method.
  1124. */
  1125. insertHTML: function(ed) {
  1126. var form = this.closest('form');
  1127. var node = form.getSelection();
  1128. if (!ed) ed = form.getEditor();
  1129. // Get the attributes & extra data
  1130. var attrs = this.getAttributes(), extraData = this.getExtraData();
  1131. // Find the element we are replacing - either the img, it's wrapper parent, or nothing (if creating)
  1132. var replacee = (node && node.is('img')) ? node : null;
  1133. if (replacee && replacee.parent().is('.captionImage')) replacee = replacee.parent();
  1134. // Find the img node - either the existing img or a new one, and update it
  1135. var img = (node && node.is('img')) ? node : $('<img />');
  1136. img.attr(attrs);
  1137. // Any existing figure or caption node
  1138. var container = img.parent('.captionImage'), caption = container.find('.caption');
  1139. // If we've got caption text, we need a wrapping div.captionImage and sibling p.caption
  1140. if (extraData.CaptionText) {
  1141. if (!container.length) {
  1142. container = $('<div></div>');
  1143. }
  1144. container.attr('class', 'captionImage '+attrs['class']).css('width', attrs.width);
  1145. if (!caption.length) {
  1146. caption = $('<p class="caption"></p>').appendTo(container);
  1147. }
  1148. caption.attr('class', 'caption '+attrs['class']).text(extraData.CaptionText);
  1149. }
  1150. // Otherwise forget they exist
  1151. else {
  1152. container = caption = null;
  1153. }
  1154. // The element we are replacing the replacee with
  1155. var replacer = container ? container : img;
  1156. // If we're replacing something, and it's not with itself, do so
  1157. if (replacee && replacee.not(replacer).length) {
  1158. replacee.replaceWith(replacer);
  1159. }
  1160. // If we have a wrapper element, make sure the img is the first child - img might be the
  1161. // replacee, and the wrapper the replacer, and we can't do this till after the replace has happened
  1162. if (container) {
  1163. container.prepend(img);
  1164. }
  1165. // If we don't have a replacee, then we need to insert the whole HTML
  1166. if (!replacee) {
  1167. // Otherwise insert the whole HTML content
  1168. ed.repaint();
  1169. ed.insertContent($('<div />').append(replacer).html(), {skip_undo : 1});
  1170. }
  1171. ed.addUndo();
  1172. ed.repaint();
  1173. },
  1174. updateFromNode: function(node) {
  1175. this.find(':input[name=AltText]').val(node.attr('alt'));
  1176. this.find(':input[name=Title]').val(node.attr('title'));
  1177. this.find(':input[name=CSSClass]').val(node.attr('class'));
  1178. this.find(':input[name=Width]').val(node.width());
  1179. this.find(':input[name=Height]').val(node.height());
  1180. this.find(':input[name=CaptionText]').val(node.siblings('.caption:first').text());
  1181. this.find(':input[name=FileID]').val(node.data('id'));
  1182. }
  1183. });
  1184. /**
  1185. * Insert a flash object tag into the content.
  1186. * Requires the 'media' plugin for serialization of tags into <img> placeholders.
  1187. */
  1188. $('form.htmleditorfield-mediaform .ss-htmleditorfield-file.flash').entwine({
  1189. getAttributes: function() {
  1190. var width = this.find(':input[name=Width]').val(),
  1191. height = this.find(':input[name=Height]').val();
  1192. return {
  1193. 'src' : this.find(':input[name=URL]').val(),
  1194. 'width' : width ? parseInt(width, 10) : null,
  1195. 'height' : height ? parseInt(height, 10) : null,
  1196. 'data-fileid' : this.find(':input[name=FileID]').val()
  1197. };
  1198. },
  1199. getHTML: function() {
  1200. var attrs = this.getAttributes();
  1201. // Emulate serialization from 'media' plugin
  1202. var el = tinyMCE.activeEditor.plugins.media.dataToImg({
  1203. 'type': 'flash',
  1204. 'width': attrs.width,
  1205. 'height': attrs.height,
  1206. 'params': {'src': attrs.src},
  1207. 'video': {'sources': []}
  1208. });
  1209. return $('<div />').append(el).html(); // Little hack to get outerHTML string
  1210. },
  1211. updateFromNode: function(node) {
  1212. // TODO Not implemented
  1213. }
  1214. });
  1215. /**
  1216. * Insert an Embed object tag into the content.
  1217. * Requires the 'media' plugin for serialization of tags into <img> placeholders.
  1218. */
  1219. $('form.htmleditorfield-mediaform .ss-htmleditorfield-file.embed').entwine({
  1220. getAttributes: function() {
  1221. var width = this.find(':input[name=Width]').val(),
  1222. height = this.find(':input[name=Height]').val();
  1223. return {
  1224. 'src' : this.find('.thumbnail-preview').attr('src'),
  1225. 'width' : width ? parseInt(width, 10) : null,
  1226. 'height' : height ? parseInt(height, 10) : null,
  1227. 'class' : this.find(':input[name=CSSClass]').val(),
  1228. 'alt' : this.find(':input[name=AltText]').val(),
  1229. 'title' : this.find(':input[name=Title]').val(),
  1230. 'data-fileid' : this.find(':input[name=FileID]').val()
  1231. };
  1232. },
  1233. getExtraData: function() {
  1234. var width = this.find(':input[name=Width]').val(),
  1235. height = this.find(':input[name=Height]').val();
  1236. return {
  1237. 'CaptionText': this.find(':input[name=CaptionText]').val(),
  1238. 'Url': this.find(':input[name=URL]').val(),
  1239. 'thumbnail': this.find('.thumbnail-preview').attr('src'),
  1240. 'width' : width ? parseInt(width, 10) : null,
  1241. 'height' : height ? parseInt(height, 10) : null,
  1242. 'cssclass': this.find(':input[name=CSSClass]').val()
  1243. };
  1244. },
  1245. getHTML: function() {
  1246. var el,
  1247. attrs = this.getAttributes(),
  1248. extraData = this.getExtraData(),
  1249. // imgEl = $('<img id="_ss_tmp_img" />');
  1250. imgEl = $('<img />').attr(attrs).addClass('ss-htmleditorfield-file embed');
  1251. $.each(extraData, function (key, value) {
  1252. imgEl.attr('data-' + key, value);
  1253. });
  1254. if(extraData.CaptionText) {
  1255. el = $('<div style="width: ' + attrs['width'] + 'px;" class="captionImage ' + attrs['class'] + '"><p class="caption">' + extraData.CaptionText + '</p></div>').prepend(imgEl);
  1256. } else {
  1257. el = imgEl;
  1258. }
  1259. return $('<div />').append(el).html(); // Little hack to get outerHTML string
  1260. },
  1261. updateFromNode: function(node) {
  1262. this.find(':input[name=AltText]').val(node.attr('alt'));
  1263. this.find(':input[name=Title]').val(node.attr('title'));
  1264. this.find(':input[name=Width]').val(node.width());
  1265. this.find(':input[name=Height]').val(node.height());
  1266. this.find(':input[name=Title]').val(node.attr('title'));
  1267. this.find(':input[name=CSSClass]').val(node.data('cssclass'));
  1268. this.find(':input[name=FileID]').val(node.data('fileid'));
  1269. }
  1270. });
  1271. $('form.htmleditorfield-mediaform .ss-htmleditorfield-file .dimensions :input').entwine({
  1272. OrigVal: null,
  1273. onmatch: function () {
  1274. this._super();
  1275. this.setOrigVal(parseInt(this.val(), 10));
  1276. },
  1277. onunmatch: function() {
  1278. this._super();
  1279. },
  1280. onfocusout: function(e) {
  1281. this.closest('.ss-htmleditorfield-file').updateDimensions(this.attr('name'));
  1282. }
  1283. });
  1284. /**
  1285. * Deselect item and remove the 'edit' view
  1286. */
  1287. $('form.htmleditorfield-mediaform .ss-uploadfield-item .ss-uploadfield-item-cancel').entwine({
  1288. onclick: function(e) {
  1289. var form = this.closest('form'), file = this.closest('ss-uploadfield-item');
  1290. form.find('.ss-gridfield-item[data-id=' + file.data('id') + ']').removeClass('ui-selected');
  1291. this.closest('.ss-uploadfield-item').remove();
  1292. form.redraw();
  1293. e.preventDefault();
  1294. }
  1295. });
  1296. $('div.ss-assetuploadfield .ss-uploadfield-item-edit, div.ss-assetuploadfield .ss-uploadfield-item-name').entwine({
  1297. getEditForm: function() {
  1298. return this.closest('.ss-uploadfield-item').find('.ss-uploadfield-item-editform');
  1299. },
  1300. fromEditForm: {
  1301. onchange: function(e){
  1302. var form = $(e.target);
  1303. form.removeClass('edited'); //so edited class is only there once
  1304. form.addClass('edited');
  1305. }
  1306. },
  1307. onclick: function(e) {
  1308. var editForm = this.getEditForm();
  1309. // Make sure we're in an HtmlEditorField here, or fall-back to _super(). HtmlEditorField with
  1310. // AssetUploadField doesn't use iframes, so needs its own toggleEditForm() logic
  1311. if (this.closest('.ss-uploadfield-item').hasClass('ss-htmleditorfield-file')) {
  1312. editForm.parent('ss-uploadfield-item').removeClass('ui-state-warning');
  1313. editForm.toggleEditForm();
  1314. e.preventDefault(); // Avoid a form submit
  1315. return false; // Avoid duplication from button
  1316. }
  1317. this._super(e);
  1318. }
  1319. });
  1320. $('div.ss-assetuploadfield .ss-uploadfield-item-editform').entwine({
  1321. toggleEditForm: function(bool) {
  1322. var itemInfo = this.prev('.ss-uploadfield-item-info'), status = itemInfo.find('.ss-uploadfield-item-status');
  1323. var text="";
  1324. if(bool === true || (bool !== false && this.height() === 0)) {
  1325. text = i18n._t('UploadField.Editing', "Editing ...");
  1326. this.height('auto');
  1327. itemInfo.find('.toggle-details-icon').addClass('opened');
  1328. status.removeClass('ui-state-success-text').removeClass('ui-state-warning-text');
  1329. } else {
  1330. this.height(0);
  1331. itemInfo.find('.toggle-details-icon').removeClass('opened');
  1332. if(!this.hasClass('edited')){
  1333. text = i18n._t('UploadField.NOCHANGES', 'No Changes');
  1334. status.addClass('ui-state-success-text');
  1335. }else{
  1336. text = i18n._t('UploadField.CHANGESSAVED', 'Changes Made');
  1337. this.removeClass('edited');
  1338. status.addClass('ui-state-success-text');
  1339. }
  1340. }
  1341. status.attr('title',text).text(text);
  1342. }
  1343. });
  1344. $('form.htmleditorfield-mediaform .field[id$="ParentID_Holder"] .TreeDropdownField').entwine({
  1345. onadd: function() {
  1346. this._super();
  1347. // TODO Custom event doesn't fire in IE if registered through object literal
  1348. var self = this;
  1349. this.bind('change', function() {
  1350. var fileList = self.closest('form').find('.grid-field');
  1351. fileList.setState('ParentID', self.getValue());
  1352. fileList.reload();
  1353. });
  1354. }
  1355. });
  1356. });