├── README.md ├── worknotes.md ├── demo ├── code.js ├── styles.css └── index.html ├── LICENSE ├── bookmarks.js └── source.opml /README.md: -------------------------------------------------------------------------------- 1 | # bookmarksMenu 2 | 3 | Client JavaScript code that manages a Bookmarks menu. 4 | 5 | ### Demo 6 | 7 | A demo app is included. 8 | 9 | To test the app, choose Add bookmark from the Bookmarks menu. 10 | 11 | It will confirm that you want to add a bookmark to the app itself and if you confirm, it is added. 12 | 13 | It saves your menu to localStorage, restores it when the app is reloaded. 14 | 15 | -------------------------------------------------------------------------------- /worknotes.md: -------------------------------------------------------------------------------- 1 | #### 1/15/24; 10:32:34 AM by DW 2 | 3 | We're now using the latest Concord. 4 | 5 | And the latest outlinedialog. 6 | 7 | #### 12/8/23; 12:28:51 PM by DW 8 | 9 | The hard-coded styles are a problem if you want to produce a mobile version. 10 | 11 | Add an option to not add styles. FeedLand is going to do it this way. 12 | 13 | #### 9/30/23; 3:46:47 PM by DW 14 | 15 | Increased font size and lineheight. 16 | 17 | Manage concord.handleEvents. 18 | 19 | #### 12/29/22 by DW 20 | 21 | Portable bookmarks. Make it easy to add the feature to any JavaScript app. 22 | 23 | -------------------------------------------------------------------------------- /demo/code.js: -------------------------------------------------------------------------------- 1 | var appPrefs = { 2 | } 3 | var myBookmarksMenu; 4 | 5 | function getSavedBookmarks () { 6 | if (localStorage.savedBookmarks === undefined) { 7 | return (undefined); 8 | } 9 | else { 10 | return (localStorage.savedBookmarks); 11 | } 12 | } 13 | function saveBookmarks (opmltext) { 14 | localStorage.savedBookmarks = opmltext; 15 | } 16 | 17 | function startup () { 18 | console.log ("startup"); 19 | 20 | var options = { 21 | opmltext: getSavedBookmarks (), 22 | saveBookmarks, 23 | idList: "idBookmarksList", 24 | maxMenuItemChars: 20 25 | }; 26 | 27 | myBookmarksMenu = new bookMarksMenu (options); 28 | myBookmarksMenu.start (); 29 | 30 | hitCounter (); 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dave Winer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Ubuntu; 3 | font-size: 18px; 4 | background-color: white; 5 | } 6 | .divPageBody { 7 | width: 800px; 8 | margin-top: 30px; 9 | margin-left: auto; 10 | margin-right: auto; 11 | margin-bottom: 400px; 12 | } 13 | .divPageBody p { 14 | line-height: 140%; 15 | } 16 | .hovering { 17 | background-color: whitesmoke; 18 | cursor: pointer; 19 | } 20 | .divUrlInput { 21 | margin-bottom: 30px; 22 | } 23 | .divUrlInput input { 24 | margin-top: 11px; 25 | margin-right: 5px; 26 | height: 32px; 27 | width: 600px; 28 | } 29 | .divUrlInput .btnGo { 30 | height: 32px; 31 | } 32 | .divTitleText { 33 | font-size: 16px; 34 | font-weight: bold; 35 | line-height: 1.25em; 36 | margin-bottom: 10px; 37 | } 38 | .divBodyText { 39 | font-size: 13px; 40 | color: #9e9e9e; 41 | line-height: 18px; 42 | } 43 | 44 | .divBigBox { 45 | height: 300px; 46 | width: 300px; 47 | background-color: gainsboro; 48 | border: 1px dotted gray; 49 | margin-top: 25px; 50 | tabindex: -1; 51 | } 52 | 53 | .divMenubar .dropdown-menu li { 54 | font-size: 14px; 55 | padding-left: 0; 56 | padding-right: 0; 57 | font-weight: normal; 58 | cursor: pointer; 59 | } 60 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bookmark feature 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 | 46 |
47 |
48 |
49 |
50 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /bookmarks.js: -------------------------------------------------------------------------------- 1 | function bookMarksMenu (options) { 2 | console.log ("bookMarksMenu"); 3 | 4 | const me = this; 5 | const defaultOptions = { 6 | opmltext: undefined, 7 | saveBookmarks: function () { 8 | }, 9 | idList: "idBookmarksList", 10 | maxMenuItemChars: 30, 11 | flAddBookmarkCommand: true, 12 | flCanInsertStyles: true, //12/8/23 by DW 13 | whereToAppend: $("body"), //1/12/24 by DW 14 | editBookmarksText: "Edit bookmarks...", //1/13/24 by DW 15 | getBookmarkTitle: function () { 16 | return (document.title); 17 | }, 18 | handleMenuChoice: function (item) { //1/13/24 by DW 19 | console.log ("handleMenuChoice, atts == " + jsonStringify (item)); 20 | if (item.url === undefined) { 21 | alertDialog ("Can't open the bookmark because there is no \"url\" attribute."); 22 | } 23 | else { 24 | window.open (item.url); 25 | } 26 | }, 27 | isItemChecked: function (item) { //1/13/24 by DW 28 | return (false); 29 | } 30 | }; 31 | for (var x in defaultOptions) { 32 | if (options [x] === undefined) { 33 | options [x] = defaultOptions [x]; 34 | } 35 | } 36 | 37 | const nowstring = getNowstring (); 38 | const check = ""; 39 | const emptyMenu = { 40 | opml: { 41 | head: { 42 | title: "Bookmarks", 43 | dateCreated: nowstring, 44 | dateModified: nowstring 45 | }, 46 | body: { 47 | subs: [ 48 | { 49 | text: "" 50 | } 51 | ] 52 | } 53 | } 54 | }; 55 | var theMenuOutline; 56 | 57 | function getNowstring () { 58 | return (new Date ().toGMTString ()); 59 | } 60 | function addAndEditNewBookmark (title, atts) { 61 | editBookmarks (function () { 62 | opFirstSummit (); 63 | opInsert (title, up); 64 | opSetAtts (atts); 65 | opSetOneAtt ("type", "bookmark"); 66 | opSetOneAtt ("icon", "bookmark"); 67 | opSetOneAtt ("created", getNowstring ()); 68 | }); 69 | } 70 | function haveBookmarks () { //return true iff there are any bookmarks 71 | const flHaveBookmarks = theMenuOutline.opml.body.subs.length > 0; 72 | return (flHaveBookmarks); 73 | } 74 | function isItemChecked (item) { 75 | return (options.isItemChecked (item)); 76 | } 77 | 78 | function buildMenu () { 79 | const theList = $("#" + options.idList); 80 | theList.empty (); 81 | 82 | function addDivider (theList) { 83 | theList.append ($("
  • ")); 84 | } 85 | function addBookmarkLevel (listInOutline, listInDom) { 86 | if (listInOutline !== undefined) { //1/12/24 by DW 87 | listInOutline.forEach (function (item) { 88 | var itemtext = trimWhitespace (item.text); 89 | itemtext = maxStringLength (itemtext, options.maxMenuItemChars, false, true); 90 | if (item.subs === undefined) { 91 | if (itemtext == "-") { 92 | addDivider (listInDom); 93 | } 94 | else { 95 | const myCheck = (isItemChecked (item)) ? check : ""; 96 | const menuItem = $("
  • " + myCheck + itemtext + "
  • "); 97 | listInDom.append (menuItem); 98 | menuItem.click (function () { 99 | options.handleMenuChoice (item); 100 | }); 101 | } 102 | } 103 | else { 104 | var liMenuItem = $("
  • " + itemtext + "
  • "); 105 | var ulSubMenu = $(""); 106 | listInDom.append (liMenuItem); 107 | addBookmarkLevel (item.subs, ulSubMenu); 108 | liMenuItem.append (ulSubMenu); 109 | } 110 | }); 111 | } 112 | } 113 | 114 | if (options.flAddBookmarkCommand) { 115 | const addBookmarkCommand = $("
  • Add bookmark...
  • "); 116 | addBookmarkCommand.click (function () { 117 | var title = options.getBookmarkTitle (); 118 | confirmDialog ("Add \"" + title + "\" to the Bookmarks menu?", function () { 119 | const atts = { 120 | url: location.href 121 | }; 122 | addAndEditNewBookmark (title, atts); 123 | }); 124 | }); 125 | theList.append (addBookmarkCommand); 126 | addDivider (theList); 127 | } 128 | 129 | addBookmarkLevel (theMenuOutline.opml.body.subs, theList); 130 | 131 | addDivider (theList); 132 | const editBookmarksCommand = $("
  • " + options.editBookmarksText + "
  • "); 133 | editBookmarksCommand.click (function () { 134 | editBookmarks (undefined); 135 | }); 136 | theList.append (editBookmarksCommand); 137 | } 138 | function editBookmarks (afterOpenCallback) { 139 | console.log ("editBookmarks"); 140 | const styles = (getBoolean (options.flCanInsertStyles)) ? ".divOutlineDialog {width: 400px; left: 50%;}\n" : ""; 141 | appPrefs.outlineFontSize = 14; 142 | appPrefs.outlineLineHeight = 20; 143 | 144 | appPrefs.outlineFontSize = 16; //9/30/23 by DW 145 | appPrefs.outlineLineHeight = 26; 146 | 147 | const opmltext = opmlStringify (theMenuOutline); 148 | flEnableBackgroundTasks = false; 149 | 150 | function afterOpen () { 151 | if (afterOpenCallback !== undefined) { 152 | afterOpenCallback (); 153 | } 154 | $(opGetActiveOutliner ()).concord ({ //12/31/22 by DW 155 | callbacks: { 156 | opExpand: function () { 157 | var atts = opGetAtts (); 158 | if (atts.url !== undefined) { 159 | window.open (atts.url); 160 | } 161 | } 162 | } 163 | }); 164 | } 165 | 166 | var flRestoreConcordHandleEvents = false; //9/30/23 by DW 167 | if (!concord.handleEvents) { 168 | concord.handleEvents = true; 169 | flRestoreConcordHandleEvents = true; 170 | } 171 | 172 | const outlineDialogOptions = { 173 | title: "Bookmarks", 174 | flReadOnly: false, 175 | whereToAppend: options.whereToAppend, //1/12/24 by DW 176 | divDialogStyles: "divBookmarksDialog", 177 | opmltext, 178 | afterOpenCallback: afterOpen 179 | }; 180 | outlineDialog (outlineDialogOptions, function (flSave, opmltext) { 181 | flEnableBackgroundTasks = true; 182 | if (flRestoreConcordHandleEvents) { //9/30/23 by DW 183 | flRestoreConcordHandleEvents = false; 184 | } 185 | if (flSave) { 186 | options.opmltext = opmltext; 187 | saveBookmarks (); 188 | theMenuOutline = opml.parse (opmltext); 189 | buildMenu (); 190 | } 191 | }); 192 | } 193 | function saveBookmarks () { 194 | options.saveBookmarks (options.opmltext); 195 | } 196 | function updateMenuOutline (theNewMenuOutline) { //1/13/24 by DW -- a caller outside can add something to the menu 197 | theMenuOutline = theNewMenuOutline; 198 | options.opmltext = opml.stringify (theMenuOutline); 199 | buildMenu (); 200 | } 201 | 202 | me.start = function (callback) { 203 | if (options.opmltext === undefined) { 204 | theMenuOutline = emptyMenu; 205 | options.opmltext = opmlStringify (emptyMenu); 206 | saveBookmarks (); 207 | } 208 | else { 209 | theMenuOutline = opml.parse (options.opmltext); 210 | } 211 | buildMenu (); 212 | }; 213 | me.addAndEdit = addAndEditNewBookmark; 214 | me.haveBookmarks = haveBookmarks; 215 | me.updateMenuOutline = updateMenuOutline; //1/13/24 by DW 216 | me.buildMenu = buildMenu; //1/13/24 by DW 217 | me.editBookmarks = editBookmarks; //6/7/24 by DW 218 | } 219 | -------------------------------------------------------------------------------- /source.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | nodeEditor: bookmarksMenu 18 | Sat, 31 Dec 2022 15:11:23 GMT 19 | Fri, 07 Jun 2024 14:05:58 GMT 20 | Dave Winer 21 | http://davewiner.com/ 22 | 1, 3, 10, 19, 20, 29, 30, 32, 33, 34, 67, 74, 77, 89, 90, 91, 102, 103, 104, 105, 106, 107, 109, 111 23 | 49 24 | 131 25 | 686 26 | 1082 27 | 1899 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | --------------------------------------------------------------------------------