diff --git a/plugin.json b/plugin.json index 74cbf94..10adefe 100644 --- a/plugin.json +++ b/plugin.json @@ -15,7 +15,7 @@ "enabled":true } , "settings":{ - "enabled":false + "enabled":true } } ,"widget":{ diff --git a/src/control/content/controlState.js b/src/control/content/controlState.js new file mode 100644 index 0000000..a3224f0 --- /dev/null +++ b/src/control/content/controlState.js @@ -0,0 +1,4 @@ +// eslint-disable-next-line no-unused-vars +const ControlState = { + settings: null, +}; diff --git a/src/control/content/index.html b/src/control/content/index.html index 37cd540..3f7c7e9 100644 --- a/src/control/content/index.html +++ b/src/control/content/index.html @@ -2,179 +2,48 @@ - - - - - + - + + - + + + + + + + + + + + + + + -
- -
-
-
- Text -
+
+ +
+
+
+ Content
-
-
-
- -
+
+
+
+
+
- - +
diff --git a/src/control/content/index.js b/src/control/content/index.js new file mode 100644 index 0000000..a5b7df1 --- /dev/null +++ b/src/control/content/index.js @@ -0,0 +1,161 @@ +let textPluginApp = angular.module('textPlugin', ['ui.tinymce']); + +textPluginApp.controller('textPluginCtrl', ['$scope', function ($scope) { + var datastoreInitialized = false; + + $scope.searchEngineIndexing = false; + + $scope.editorOptions = { + plugins: 'advlist autolink link image lists charmap print preview', + skin: 'lightgray', + trusted: true, + theme: 'modern', + format: 'html', + convert_urls: false, + relative_urls: false + + }; + + $scope.data = { + content: { + carouselImages: [], text: "

 

" + }, design: { + backgroundImage: null, backgroundBlur: 0, selectedLayout: 1 + } + }; + + // create a new instance of the buildfire carousel editor + var editor = new buildfire.components.carousel.editor("#carousel"); + + /* + * Go pull any previously saved data + * */ + buildfire.datastore.get(function (err, result) { + + if (!err) { + datastoreInitialized = true; + } else { + console.error("Error: ", err); + return; + } + + if (result && result.data && !angular.equals({}, result.data) && result.id) { + if (!result.data.design) result.data.design = $scope.data.design; + $scope.data = result.data; + $scope.id = result.id; + if ($scope.data.content && $scope.data.content.carouselImages) editor.loadItems($scope.data.content.carouselImages); + if (tmrDelay) clearTimeout(tmrDelay); + } else { + $scope.data = { + content: { + text: '

The WYSIWYG (which stands for What You See Is What You Get) allows you to do some really cool stuff. You can add images like this

\ +

\ +

You can even create links like these:
Link to web content like this
Link to a phone number like this 8005551234
Link to an email like this noreply@google.com

\ +

Want to add some super cool videos about this item? You can do that too!

\ +

\ +

You can create bulleted and numbered lists like this:

\ +
    \ +
  • This is an item in a list
  • \ +
  • This is another item in a list
  • \ +
  • This is a last item in a list
  • \ +
\ +

Want more info? Check out our tutorial by clicking the help button at the top of this page.

', + + carouselImages: [{ + "action": "noAction", + "iconUrl": "http://imageserver.prod.s3.amazonaws.com/b55ee984-a8e8-11e5-88d3-124798dea82d/5db61d30-0854-11e6-8963-f5d737bc276b.jpg", + "title": "image 1" + }, { + "action": "noAction", + "iconUrl": "http://imageserver.prod.s3.amazonaws.com/b55ee984-a8e8-11e5-88d3-124798dea82d/31c88a00-0854-11e6-8963-f5d737bc276b.jpeg", + "title": "image 2" + }] + }, design: { + backgroundImage: null, backgroundBlur: 0, selectedLayout: 1 + } + }; + editor.loadItems($scope.data.content.carouselImages); + } + + /* + * watch for changes in data and trigger the saveDataWithDelay function on change + * */ + $scope.$watch('data', saveDataWithDelay, true); + + if (!$scope.$$phase && !$scope.$root.$$phase) { + $scope.$apply(); + } + }); + + AuthManager.refreshCurrentUser().then(function () { + Settings.get().then((data) => $scope.searchEngineIndexing = data.searchEngineIndexing); + }); + + /* + * Call the datastore to save the data object + */ + var saveData = function (newObj, callBack) { + if (!datastoreInitialized) { + console.error("Error with datastore didn't get called"); + return; + } + if (newObj.content.text.indexOf("src=\"//") != -1) { + newObj.content.text = newObj.content.text.replace("src=\"//", "src=\"https://") + } + if (newObj == undefined) return; + + if ($scope.frmMain.$invalid) { + console.warn('invalid data, details will not be saved'); + return; + } + + if (!newObj.content || !newObj.design) return; + + buildfire.datastore.save(newObj, function (err, result) { + if (err || !result) { + console.error('Error saving the widget details: ', err); + } + callBack(); + }); + }; + var saveSearchEngine = function (content) { + if (!$scope.searchEngineIndexing) return; + buildfire.dynamic.expressions.evaluate({expression: content}, (err, result) => { + if (err) return console.error(err); + const content = prepareSearchEngineContent(result.evaluatedExpression); + if (!content.title || !content.description) { + SearchEngineService.delete().catch(()=>{ + buildfire.dialog.toast({ + message: 'Error indexing data.', + type:'danger' + }); + }); + return; + } + SearchEngineService.save(content.title, content.description).catch(()=>{ + buildfire.dialog.toast({ + message: 'Error indexing data.', + type:'danger' + }); + }); + }) + }; + /* + * create an artificial delay so api isnt called on every character entered + * */ + var tmrDelay = null; + + var saveDataWithDelay = function (newObj, oldObj) { + if (tmrDelay) clearTimeout(tmrDelay); + if (angular.equals(newObj, oldObj)) return; + tmrDelay = setTimeout(function () { + saveData(newObj, function () {saveSearchEngine(newObj.content.text)}); + }, 500); + }; + + // this method will be called when a new item added to the list + editor.onAddItems = editor.onDeleteItem = editor.onItemChange = editor.onOrderChange = function () { + $scope.data.content.carouselImages = editor.items; + saveData($scope.data); + }; +}]); diff --git a/src/control/content/style.css b/src/control/content/style.css new file mode 100644 index 0000000..55a8022 --- /dev/null +++ b/src/control/content/style.css @@ -0,0 +1,19 @@ +body { + font-family: 'Conv_apercu_regular','Helvetica','Sans-Serif','Arial'; +} +#frMain { + height: 95vh; + flex-direction: column; +} +.tinymce-editor{ + flex: 1; +} +.tinymce-editor .main { + height: 100%; +} + +/* This rule overrides the height of the TinyMCE editor */ +.tox.tox-tinymce { + min-height: 100% !important; +} + diff --git a/src/control/settings/index.html b/src/control/settings/index.html index 97a74b4..65c089f 100644 --- a/src/control/settings/index.html +++ b/src/control/settings/index.html @@ -1,10 +1,41 @@ - + - + + + + + + + + + + + + + + + + + + - -no settings + +
+ +
+ + +

Please note: Any changes to the content will update automatically in + the search engine, but you still need to publish the app for users to see these updates.

+
+
- \ No newline at end of file + diff --git a/src/control/settings/index.js b/src/control/settings/index.js new file mode 100644 index 0000000..07837b0 --- /dev/null +++ b/src/control/settings/index.js @@ -0,0 +1,70 @@ +let textPluginApp = angular.module('textSetting', []); + +textPluginApp.controller('textSettingCtrl', ['$scope', function ($scope) { + $scope.searchEngineIndexing = false; + + $scope.handleCheckboxClick = function () { + // initialSearchEngineIndexing to Save settings only if the searchEngineIndexing value has changed + let initialSearchEngineIndexing = $scope.searchEngineIndexing; + $scope.searchEngineIndexing = !$scope.searchEngineIndexing; + + if (!$scope.searchEngineIndexing) { + buildfire.dialog.confirm({ + title: "Disable Search Engine Indexing", + message: "Are you sure you want to disable search engine indexing? All indexed data from this Text WYSIWYG will be removed, and the content will no longer be searchable.", + confirmButton: {type: 'warning', text: 'Disable'}, + }, function (err, isConfirmed) { + $scope.searchEngineIndexing = !isConfirmed; + $scope.$apply(); + + if ($scope.searchEngineIndexing !== initialSearchEngineIndexing) { + Settings.save({searchEngineIndexing: $scope.searchEngineIndexing}); + Content.get().then(function (data) { + if (data && data.data && data.data.content && data.data.content.text) + $scope.handleSearchEngine(data.data.content.text); + }); + } + }); + } else { + if ($scope.searchEngineIndexing !== initialSearchEngineIndexing) { + Settings.save({searchEngineIndexing: $scope.searchEngineIndexing}); + Content.get().then(function (data) { + if (data && data.data && data.data.content && data.data.content.text) + $scope.handleSearchEngine(data.data.content.text); + }); + } + } + }; + + $scope.handleSearchEngine = function (content) { + buildfire.dynamic.expressions.evaluate({expression: content}, (err, result) => { + if (err) return console.error(err); + const content = prepareSearchEngineContent(result.evaluatedExpression); + if (!content.title || !content.description) { + return; + } + if (!$scope.searchEngineIndexing) { + SearchEngineService.delete().catch(()=>{ + buildfire.dialog.toast({ + message: 'Error indexing data.', + type:'danger' + }); + }); + } else + SearchEngineService.save(content.title, content.description).catch(()=>{ + buildfire.dialog.toast({ + message: 'Error indexing data.', + type:'danger' + }); + }); + }) + }; + + AuthManager.refreshCurrentUser().then(function () { + Settings.get().then(function (data) { + $scope.searchEngineIndexing = data.searchEngineIndexing; + $scope.$apply(); + }); + }); +} +]); diff --git a/src/control/settings/style.css b/src/control/settings/style.css new file mode 100644 index 0000000..6aace95 --- /dev/null +++ b/src/control/settings/style.css @@ -0,0 +1,92 @@ +/* tooltip */ +.tooltip-container { + display: flex; + align-items: center; + margin-right: 13px !important; + gap: 0.25rem; + flex: 1; +} +.tooltip-container .btn-info-icon { + display: flex; + align-items: center; + justify-content: center; + margin-left: 8px; + min-width: 16px; + min-height: 16px; + width: 16px; + height: 16px; + line-height: 16px; + color: #fff; + position: relative; +} +.tooltip-container .btn-info-icon:after { + content: 'i'; + font-weight: 400; + font-size: 12px; + font-style: normal; + left: 0; + top: auto !important; + height: 100%; +} +.tooltip-container .btn-info-icon .cp-tooltip { + pointer-events: none; + background-color: rgba(0, 0, 0, .80); + color: #fff; + padding: 8px; + border-radius: 4px; + position: absolute; + left: calc(100% + 12px); + top: 0px; + cursor: auto; + font-size: 12px; + text-align: left; + width: 200px; + opacity: 0; + transform: scale(.1); + transition: opacity ease .1s, transform ease .1s; + transform-origin: left; + z-index: 100; + font-weight: 400; + line-height: 18px; + font-family: 'Conv_apercu_regular','Helvetica','Sans-Serif','Arial'; + -webkit-text-stroke: 0; +} +.top-cp-tooltip { + top: 0; +} +.bottom-cp-tooltip { + bottom: 0; +} +.tooltip-container .btn-info-icon .cp-tooltip::before { + content: ""; + position: absolute; + top: 10%; + left: -5px; + transform: rotate(90deg) translate(-50%); + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: rgba(0, 0, 0, .80) transparent transparent transparent; +} +.top-cp-tooltip::before { + top: 10px !important; +} +.bottom-cp-tooltip::before { + bottom: 0 !important; + top: unset !important; +} +.tooltip-container .btn-info-icon:hover .cp-tooltip { + opacity: 1; + transform: scale(1); +} + + +.button-switch p.info-note { + color: var(--c-gray5); + background-color: var(--c-gray1); + font-family: 'Conv_apercu_regular', 'Helvetica', 'Sans-Serif', 'Arial'; +} + +.button-switch { + flex: 2.5; +} diff --git a/src/widget/global/js/models/Setting.js b/src/widget/global/js/models/Setting.js new file mode 100644 index 0000000..8019a86 --- /dev/null +++ b/src/widget/global/js/models/Setting.js @@ -0,0 +1,28 @@ +class Setting { + /** + * Create a setting model. + * @param {object} data - model value + */ + constructor(data = {}) { + this.searchEngineIndexing = data.searchEngineIndexing || false; + this.isActive = data.isActive || true; + this.createdOn = data.createdOn || new Date(); + this.createdBy = data.createdBy || ''; + this.lastUpdatedOn = data.lastUpdatedOn || new Date(); + this.lastUpdatedBy = data.lastUpdatedBy || ''; + } + /** + * Convert the model to plain JSON + * @return {Setting} A Setting object. + */ + toJSON() { + return { + searchEngineIndexing: this.searchEngineIndexing, + isActive: this.isActive, + createdOn: this.createdOn, + createdBy: this.createdBy, + lastUpdatedOn: this.lastUpdatedOn, + lastUpdatedBy: this.lastUpdatedBy + }; + } +} diff --git a/src/widget/global/js/repositories/Content.js b/src/widget/global/js/repositories/Content.js new file mode 100644 index 0000000..c66cdd7 --- /dev/null +++ b/src/widget/global/js/repositories/Content.js @@ -0,0 +1,16 @@ +// eslint-disable-next-line no-unused-vars +class Content { + + /** + * get content data + * @returns {Promise} + */ + static get() { + return new Promise((resolve, reject) => { + buildfire.datastore.get( (err, res) => { + if (err) return reject(err); + resolve(res); + }); + }); + } +} diff --git a/src/widget/global/js/repositories/Settings.js b/src/widget/global/js/repositories/Settings.js new file mode 100644 index 0000000..8340f26 --- /dev/null +++ b/src/widget/global/js/repositories/Settings.js @@ -0,0 +1,53 @@ +// eslint-disable-next-line no-unused-vars +class Settings { + /** + * Get database collection tag + * @returns {string} + */ + static get TAG() { + return "settings"; + } + + /** + * get settings data + * @returns {Promise} + */ + static get() { + return new Promise((resolve, reject) => { + buildfire.datastore.get(Settings.TAG, (err, res) => { + if (err) return reject(err); + if (!res || !res.data || !Object.keys(res.data).length) { + const currentUser = AuthManager.currentUser; + const data = new Setting().toJSON(); + data.createdBy = currentUser?._id ? currentUser._id : ""; + Settings.save(data).then(()=>{ + ControlState.settings = data; + resolve(data); + }) + } else { + const data = new Setting(res.data).toJSON(); + ControlState.settings = data; + resolve(data); + } + }); + }); + } + + /** + * set settings data + * @param {Object} data + * @returns {Promise} + */ + static save(data) { + return new Promise((resolve, reject) => { + const currentUser = AuthManager.currentUser; + data.lastUpdatedBy = currentUser?._id ? currentUser._id : ""; + data.createdBy = data.createdBy || ControlState.settings?.createdBy; + data.createdOn = ControlState.settings?.createdOn || new Date(); + buildfire.datastore.save(new Setting(data).toJSON(), Settings.TAG, (err, res) => { + if (err) return reject(err); + return resolve(new Setting(res.data).toJSON()); + }); + }); + } +} diff --git a/src/widget/global/js/services/AuthManager.js b/src/widget/global/js/services/AuthManager.js new file mode 100644 index 0000000..26c17ef --- /dev/null +++ b/src/widget/global/js/services/AuthManager.js @@ -0,0 +1,21 @@ +const AuthManager = { + _currentUser: {}, + get currentUser() { + return AuthManager._currentUser; + }, + set currentUser(user) { + AuthManager._currentUser = user; + }, + refreshCurrentUser() { + return new Promise((resolve) => { + buildfire.auth.getCurrentUser((err, user) => { + AuthManager.currentUser = err || !user ? null : user; + resolve(); + }); + }); + }, +}; + +buildfire.auth.onLogin((user) => { + AuthManager.currentUser = user; +}, true); diff --git a/src/widget/global/js/services/searchEngine.js b/src/widget/global/js/services/searchEngine.js new file mode 100644 index 0000000..f2bd7f5 --- /dev/null +++ b/src/widget/global/js/services/searchEngine.js @@ -0,0 +1,50 @@ +const instanceId = buildfire.getContext().instanceId; + +class SearchEngineService { + /** + * Get database collection tag + * @returns {string} + */ + static get TAG() { + return 'wysiwygContent'; + } + + /** + * Get database collection key + * @returns {string} + */ + + static get KEY() { + return `wysiwyg_${instanceId}`; + } + + /** + * save search engine data + * @returns {Promise} + */ + static save(title , description) { + return new Promise((resolve, reject) => { + buildfire.services.searchEngine.save({ + tag: SearchEngineService.TAG, key: SearchEngineService.KEY, title, description + }, (err, result) => { + if (err) return console.error(err); + },); + }); + } + /** + * delete search engine data + * @param {Object} data + * @returns {Promise} + */ + static delete() { + return new Promise((resolve, reject) => { + buildfire.services.searchEngine.delete({ + tag: SearchEngineService.TAG, key: SearchEngineService.KEY + }, + (err, result) => { + if (err) return console.error(err); + } + ); + }); + } +} diff --git a/src/widget/global/js/utils.js b/src/widget/global/js/utils.js new file mode 100644 index 0000000..6de79e2 --- /dev/null +++ b/src/widget/global/js/utils.js @@ -0,0 +1,28 @@ +function prepareSearchEngineContent (el) { + let excludeTags = ["script", "style", "button"]; + let title = ""; + let description = ""; + + let tempDiv = document.createElement("div"); + tempDiv.innerHTML = el; + el = tempDiv; + function traverse(node) { + if (node.nodeType === Node.TEXT_NODE) { + description += node.textContent.trim() + " "; + } else if (node.nodeType === Node.ELEMENT_NODE && !excludeTags.includes(node.tagName.toLowerCase())) { + if (node.tagName.toLowerCase().startsWith("h") && !description.trim()) { + title = node.textContent.trim(); + } + node.childNodes.forEach(child => traverse(child)); + } + } + traverse(el); + + if (!title) { + title = description.substring(0, 40); + } + // remove extra spaces and rating characters + title = title.replace(/★/g, "").replace(/\s+/g, " ").trim(); + description = description.replace(/★/g, "").replace(/\s+/g, " ").trim(); + return { title, description }; +} diff --git a/src/widget/widget.js b/src/widget/widget.js index da91d57..daa7fd2 100644 --- a/src/widget/widget.js +++ b/src/widget/widget.js @@ -60,9 +60,12 @@ function init() { // Keep state up to date with control changes buildfire.datastore.onUpdate((result) => { - state.data = result.data; - !state.data.design ? state.data.design = defaultData.design : null; - render(); + const hasContentChanged = !!result.data.content + if (hasContentChanged) { + state.data = result.data; + !state.data.design ? state.data.design = defaultData.design : null; + render(); + } }); } diff --git a/webpack/build.config.js b/webpack/build.config.js index cd1aa3c..0d8614f 100644 --- a/webpack/build.config.js +++ b/webpack/build.config.js @@ -66,12 +66,15 @@ const WebpackConfig = { to: path.join(__dirname, '../dist/plugin.json'), } ], { - ignore: ['*.js', '*.html', '*.md'] - }), + ignore: ['*.js', '*.html', '*.md'] }), new CopyWebpackPlugin([{ from: path.join(__dirname, '../src/control'), to: path.join(__dirname, '../dist/control'), }]), + new CopyWebpackPlugin([{ + from: path.join(__dirname, '../src/widget'), + to: path.join(__dirname, '../dist/widget'), + }]), new ExtractTextPlugin('[name].css'), new ZipWebpackPlugin({ path: path.join(__dirname, '../'), diff --git a/webpack/dev.config.js b/webpack/dev.config.js index c34a8b0..0f73d2c 100644 --- a/webpack/dev.config.js +++ b/webpack/dev.config.js @@ -62,6 +62,10 @@ const WebpackConfig = { }], { ignore: ['*.js', '*.html', '*.md'] }), + new CopyWebpackPlugin([{ + from: path.join(__dirname, '../src/widget'), + to: path.join(__dirname, '../dist/widget'), + }]), new CopyWebpackPlugin([{ from: path.join(__dirname, '../src/control'), to: path.join(__dirname, '../control'),