TextWidget.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. 'use strict';
  2. /*
  3. *+------------------------------------------------------------------------+
  4. *| Licensed Materials - Property of IBM
  5. *| IBM Cognos Products: Dashboard
  6. *| (C) Copyright IBM Corp. 2015, 2020
  7. *|
  8. *| US Government Users Restricted Rights - Use, duplication or disclosure
  9. *| restricted by GSA ADP Schedule Contract with IBM Corp.
  10. *+------------------------------------------------------------------------+
  11. */
  12. define(['jquery', './StaticWidget', '../../../lib/@waca/core-client/js/core-client/utils/Utils', '../../../lib/@waca/core-client/js/core-client/utils/BidiUtil', '../../../dashboard/util/TextEditor', '../../../app/nls/StringResources', 'underscore', '../../../app/util/EventChainLocal', './TextPlaceholderView', './IconPlaceholderView', '../../../lib/@waca/dashboard-common/dist/api/PropertiesProviderAPI', '../../../lib/@waca/dashboard-common/dist/core/APIFactory'], function ($, StaticWidget, Utils, BidiUtil, TextEditor, resources, _, EventChainLocal, TextPlaceholderView, IconPlaceholderView, PropertiesProviderAPI, APIFactory) {
  13. var getDefaultSpec = function getDefaultSpec(name, options) {
  14. if (!options) {
  15. options = {
  16. style: 'responsive',
  17. placeholder: {
  18. showIcon: false,
  19. text: resources.get('textPlaceHolder')
  20. }
  21. };
  22. }
  23. var widgetContent = '<div class="staticContent">' + TextEditor.getTextHTML(options) + '</div>';
  24. var spec = {
  25. model: {
  26. type: 'text',
  27. name: resources.get('textPlaceHolder'),
  28. content: widgetContent
  29. },
  30. layoutProperties: {
  31. style: {
  32. width: '200px',
  33. height: '50px'
  34. }
  35. }
  36. };
  37. if (options.style === 'responsive') {
  38. spec.model.isResponsive = true;
  39. }
  40. if (options.placeholder) {
  41. spec.model.placeholder = options.placeholder;
  42. // build avatarHtml from placeholder text so avatar won't be blank
  43. if (!options.text) {
  44. spec.model.avatarHtml = '<div class="staticContent">' + TextEditor.getTextHTML(_.extend({ text: options.placeholder.text }, options)) + '</div>';
  45. }
  46. }
  47. return Promise.resolve(spec);
  48. };
  49. /**
  50. * The text widget object is a widget for displaying text strings on the canvas. In authoring mode, the text can be edited inline,
  51. * but in consumption mode becomes a read-only element of the canvas.
  52. */
  53. var TextWidget = null;
  54. TextWidget = StaticWidget.extend({
  55. _didCleanseContent: false,
  56. init: function init(options) {
  57. TextWidget.inherited('init', this, arguments);
  58. if (options && options.initialConfigJSON && options.initialConfigJSON.placeholder) {
  59. var placeholder = options.initialConfigJSON.placeholder;
  60. var PlaceholderView = placeholder.iconType ? IconPlaceholderView : TextPlaceholderView;
  61. this.placeholder = new PlaceholderView(_.extend({ el: this.$el }, placeholder));
  62. this.$el.on('primaryaction', this.onSelect.bind(this));
  63. }
  64. },
  65. destroy: function destroy() {
  66. this.textEditor.toggleEditing(false);
  67. this.model.off('change:content', this.textEditor.applyContent, this.textEditor);
  68. this.content.off('change:property', this._onPropertyChange, this);
  69. this.$el.off('primaryaction', this.onSelect.bind(this));
  70. this.textEditor.selectionUnbindNodeEvents();
  71. this.textEditor.destroy();
  72. TextWidget.inherited('destroy', this, arguments);
  73. },
  74. _onPropertyChange: function _onPropertyChange() {
  75. this.updateModelContent();
  76. },
  77. onContainerReady: function onContainerReady() {
  78. var _this = this;
  79. TextWidget.inherited('onContainerReady', this, arguments);
  80. this.$el.append(this._cleanContent(this.model.get('content')));
  81. var colorDefault = {
  82. 'color': 'responsiveColor'
  83. };
  84. var defaults = this.initialConfigJSON && this.initialConfigJSON.defaults ? _.extend(this.initialConfigJSON.defaults, colorDefault) : colorDefault;
  85. this.textEditor = new TextEditor({
  86. 'node': this.getWidgetStyleNode(),
  87. 'container': this.$widgetContainer,
  88. 'widget': this,
  89. 'toolbarNode': this.$widgetContainer,
  90. 'isResponsive': this.model.isResponsive,
  91. 'supportsLists': true,
  92. 'initialState': defaults
  93. });
  94. BidiUtil.initElementForBidi(this.getWidgetStyleNode().get(0));
  95. this.model.on('change:content', this.textEditor.applyContent, this.textEditor);
  96. this.content.on('change:property', this._onPropertyChange, this);
  97. this.addColorProperties(['content']);
  98. // The model is ready; if we cleansed the content on load, do a forced/silent save.
  99. if (this._didCleanseContent) {
  100. this._didCleanseContent = false;
  101. this._silentUpdate();
  102. }
  103. this.textEditor.initContentEditable('p > span');
  104. this.textEditor.selectionBindNodeEvents();
  105. if (this.placeholder) {
  106. this.placeholder.render();
  107. this.updatePlaceholder();
  108. }
  109. this._updateAriaInfo();
  110. //Setting the model to the current HTML so we properly deal with upgrading from old dashboards
  111. this.model.set({ content: this.getHtmlRender() }, { silent: true, payloadData: { skipUndoRedo: true } });
  112. // todo lifecycle_cleanup -- this should be turned into a feature.
  113. var propertiesAPI = APIFactory.createAPI(this, [PropertiesProviderAPI]);
  114. this.content.getFeature('Properties').registerProvider(propertiesAPI);
  115. // TODO: replace this with a better fix after release
  116. // if custom fonts are in the save TextWidget, they will not resize properly on first load
  117. // need the setTimeout to allow the fonts to download
  118. // 900ms was the lowest timing that still worked on a slow internet connection
  119. return new Promise(function (resolve) {
  120. setTimeout(function () {
  121. _this.fillText();
  122. resolve();
  123. }, 900);
  124. });
  125. },
  126. _updateAriaInfo: function _updateAriaInfo() {
  127. this.updateDescription(this.getLabel());
  128. var editableNodeId = this.model.id + 'Editable';
  129. var editableNode = this.$el.find('.note-editable').get(0);
  130. if (editableNode) {
  131. editableNode.setAttribute('id', editableNodeId);
  132. this.$widgetContainer.attr('aria-labelledby', function () {
  133. return $(this).attr('aria-labelledby') + ' ' + editableNodeId;
  134. });
  135. }
  136. },
  137. onShow: function onShow() {
  138. this.fillText();
  139. },
  140. _shouldShowPlaceholder: function _shouldShowPlaceholder() {
  141. if (this.initialConfigJSON.placeholder.iconType === 'list') {
  142. return !this.textEditor.hasText();
  143. } else {
  144. return !this.textEditor.hasContent();
  145. }
  146. },
  147. updatePlaceholder: function updatePlaceholder() {
  148. if (this._shouldShowPlaceholder()) {
  149. this._showPlaceholder();
  150. } else {
  151. this._hidePlaceholder();
  152. }
  153. },
  154. _showPlaceholder: function _showPlaceholder() {
  155. this.$widgetContainer.addClass('placeholder');
  156. this.textEditor.hide();
  157. this.placeholder.show();
  158. },
  159. _hidePlaceholder: function _hidePlaceholder() {
  160. var immediateEditing = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
  161. this.$widgetContainer.removeClass('placeholder');
  162. this.placeholder.hide();
  163. this.textEditor.show(immediateEditing);
  164. },
  165. onSelect: function onSelect(event) {
  166. var isDescendant = function isDescendant(node, className) {
  167. while (node) {
  168. if (node.classList && _.contains(node.classList, className)) {
  169. return true;
  170. }
  171. node = node.parentNode;
  172. }
  173. return false;
  174. };
  175. if (this.isAuthoringMode) {
  176. if (this.placeholder && this.placeholder.isShowing()) {
  177. // Prevent widget ODT from displaying/updating
  178. new EventChainLocal(event).setProperty('preventDefaultContextBar', true);
  179. this._hidePlaceholder(true);
  180. } else if (this.placeholder && this._shouldShowPlaceholder() && isDescendant(event.target, 'moveHandle')) {
  181. this._showPlaceholder();
  182. } else if (this.textEditor.isEditing()) {
  183. new EventChainLocal(event).setProperty('preventDefaultContextBar', true);
  184. }
  185. }
  186. },
  187. onChromeSelected: function onChromeSelected(isGroupSelect) {
  188. TextWidget.inherited('onChromeSelected', this, arguments);
  189. if (this.isAuthoringMode && !isGroupSelect) {
  190. this.textEditor.attachEnterEditEvents();
  191. }
  192. },
  193. onChromeDeselected: function onChromeDeselected() {
  194. TextWidget.inherited('onChromeDeselected', this, arguments);
  195. if (this.placeholder && !this.placeholder.isShowing() && this._shouldShowPlaceholder()) {
  196. this._showPlaceholder();
  197. }
  198. if (this.isAuthoringMode) {
  199. this.textEditor.toggleEditing(false);
  200. this.textEditor.detachEnterEditEvents();
  201. }
  202. },
  203. onAuthoringMode: function onAuthoringMode() {
  204. TextWidget.inherited('onAuthoringMode', this, arguments);
  205. },
  206. onConsumeMode: function onConsumeMode() {
  207. TextWidget.inherited('onConsumeMode', this, arguments);
  208. this.textEditor.toggleEditing(false);
  209. },
  210. onEnterContainer: function onEnterContainer() {
  211. this.textEditor.toggleEditing(true);
  212. if (this.placeholder && this.placeholder.isShowing()) {
  213. this._hidePlaceholder(true);
  214. }
  215. },
  216. _cleanContent: function _cleanContent(content) {
  217. var cleansedContent = TextEditor.cleanContentElements(content);
  218. if (!cleansedContent || !this.isValidHtmlContent(cleansedContent)) {
  219. // TODO: Re-evaluate this, as our placeholders are now externalized. What is truly needed here?
  220. // Provide a fallback to default spec when content is unusable
  221. var textOptions = {
  222. 'text': resources.get('textPlaceHolder'),
  223. 'style': 'responsive'
  224. };
  225. cleansedContent = '<div class="staticContent">' + TextEditor.getTextHTML(textOptions) + '</div>';
  226. }
  227. if (cleansedContent !== content) {
  228. this._didCleanseContent = true;
  229. }
  230. return cleansedContent;
  231. },
  232. _silentUpdate: function _silentUpdate() {
  233. this.onPropertyUpdate({
  234. transactionId: null // Want a silent save (no undo)
  235. });
  236. },
  237. /**
  238. * Implementation of {@link PropertiesProviderAPI#getPropertyLayoutList}
  239. * TODO: need to add unit test
  240. */
  241. getPropertyLayoutList: function getPropertyLayoutList() {
  242. var propertyLayoutList = TextWidget.inherited('getPropertyLayoutList', this, arguments) || [];
  243. var textEditorPropertyLayoutList = this.textEditor.getPropertyLayoutList();
  244. if (textEditorPropertyLayoutList) {
  245. propertyLayoutList.push.apply(propertyLayoutList, textEditorPropertyLayoutList);
  246. }
  247. return propertyLayoutList;
  248. },
  249. getPropertyList: function getPropertyList() {
  250. var propertyList = [];
  251. var textEditorPropertyList = this.textEditor.getPropertyList();
  252. if (textEditorPropertyList) {
  253. propertyList.push.apply(propertyList, textEditorPropertyList);
  254. }
  255. return propertyList;
  256. },
  257. selectAll: function selectAll() {
  258. document.getSelection().selectAllChildren(this.getWidgetStyleNode().get(0));
  259. },
  260. /**
  261. * Based on properties, generate the HTML for the static content
  262. */
  263. getHtmlRender: function getHtmlRender() {
  264. return this.getWidgetStyleNode().get(0) ? this.getWidgetStyleNode().get(0).outerHTML : '';
  265. },
  266. fillText: function fillText() {
  267. if (!this.placeholder || !this.placeholder.isShowing()) {
  268. this.textEditor.fillText();
  269. } else if (this.placeholder.fillText && this.placeholder.isShowing()) {
  270. this.placeholder.fillText();
  271. }
  272. },
  273. /*
  274. * Helpers.
  275. */
  276. resize: function resize() {
  277. this.fillText();
  278. if (this.placeholder && this.placeholder.resize) {
  279. this.placeholder.resize();
  280. }
  281. },
  282. registerEventGroup: function registerEventGroup() {
  283. // Text widgets are not added to event groups
  284. },
  285. /**
  286. * Updates the model version of the markup of this widget
  287. * @param updatedHtml An optional parameter of the markup.
  288. * This is to avoid regenerating the markup if it is already known.
  289. */
  290. updateModelContent: function updateModelContent(updatedHtml, transactionId) {
  291. // regenerate the HTML if not passed
  292. if (!updatedHtml) {
  293. updatedHtml = TextEditor.cleanContentElements(this.getHtmlRender());
  294. } else {
  295. updatedHtml = TextEditor.cleanContentElements(updatedHtml);
  296. }
  297. // update the model to persist the changes. Use transactionId to combine undos
  298. var data = {};
  299. if (transactionId) {
  300. _.extend(data, {
  301. payloadData: {
  302. undoRedoTransactionId: transactionId
  303. }
  304. });
  305. } else {
  306. _.extend(data, {
  307. silent: true
  308. });
  309. }
  310. var widgetTitle = this.textEditor.getTitleFromHtml(this.getHtmlRender());
  311. this.set({
  312. content: updatedHtml,
  313. name: widgetTitle,
  314. isResponsive: this.textEditor.isResponsive
  315. }, data);
  316. },
  317. getLabel: function getLabel() {
  318. return resources.get('textWidgetTitle');
  319. }
  320. });
  321. TextWidget.getDefaultSpec = getDefaultSpec;
  322. return TextWidget;
  323. });
  324. //# sourceMappingURL=TextWidget.js.map